Various metadata-related features and improvements:
- Added support for new "NowMs" field that holds up to where no new packets are guaranteed to show up, in order to lower latency. - Added support for JSON tracks over all TS-based protocols (input and output) - Added support for AMF metadata conversion to JSON (RTMP/FLV input) - Fixed MP4 input subtitle tracks - Generalized websocket-based outputs to all support the same commands and run the same core logic - Added new "JSONLine" protocol that allows for generic direct line-by-line ingest of subtitles and/or JSON metadata tracks over a TCP socket or console standard input.
This commit is contained in:
parent
c337fff614
commit
3e2a17ff93
36 changed files with 1054 additions and 469 deletions
56
lib/amf.cpp
56
lib/amf.cpp
|
@ -6,7 +6,7 @@
|
|||
#include <sstream>
|
||||
/// Returns the std::string Indice for the current object, if available.
|
||||
/// Returns an empty string if no indice exists.
|
||||
std::string AMF::Object::Indice(){
|
||||
std::string AMF::Object::Indice() const{
|
||||
return myIndice;
|
||||
}
|
||||
|
||||
|
@ -190,6 +190,52 @@ std::string AMF::Object::Print(std::string indent){
|
|||
return st.str();
|
||||
}// print
|
||||
|
||||
JSON::Value AMF::Object::toJSON() const{
|
||||
switch (myType){
|
||||
case AMF::AMF0_NUMBER:
|
||||
case AMF::AMF0_DATE:
|
||||
case AMF::AMF0_REFERENCE:
|
||||
return numval;
|
||||
case AMF::AMF0_BOOL:
|
||||
return (bool)numval;
|
||||
case AMF::AMF0_STRING:
|
||||
case AMF::AMF0_LONGSTRING:
|
||||
case AMF::AMF0_XMLDOC: // is always a longstring
|
||||
return strval;
|
||||
case AMF::AMF0_TYPED_OBJ: // is an object, with the classname first
|
||||
case AMF::AMF0_OBJECT:
|
||||
case AMF::AMF0_ECMA_ARRAY:{
|
||||
JSON::Value ret;
|
||||
if (contents.size() > 0){
|
||||
for (std::vector<AMF::Object>::const_iterator it = contents.begin(); it != contents.end(); it++){
|
||||
ret[it->Indice()] = it->toJSON();
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
case AMF::AMF0_MOVIECLIP:
|
||||
case AMF::AMF0_OBJ_END:
|
||||
case AMF::AMF0_UPGRADE:
|
||||
case AMF::AMF0_NULL:
|
||||
case AMF::AMF0_UNDEFINED:
|
||||
case AMF::AMF0_RECORDSET:
|
||||
case AMF::AMF0_UNSUPPORTED:
|
||||
// no data to add
|
||||
return JSON::Value();
|
||||
case AMF::AMF0_DDV_CONTAINER: // only send contents
|
||||
case AMF::AMF0_STRICT_ARRAY:{
|
||||
JSON::Value ret;
|
||||
if (contents.size() > 0){
|
||||
for (std::vector<AMF::Object>::const_iterator it = contents.begin(); it != contents.end(); it++){
|
||||
ret.append(it->toJSON());
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
return JSON::Value();
|
||||
}
|
||||
|
||||
/// Packs the AMF object to a std::string for transfer over the network.
|
||||
/// If the object is a container type, this function will call itself recursively and contain all
|
||||
/// contents. Tip: When sending multiple AMF objects in one go, put them in a single
|
||||
|
@ -489,7 +535,7 @@ AMF::Object AMF::parse(std::string data){
|
|||
|
||||
/// Returns the std::string Indice for the current object, if available.
|
||||
/// Returns an empty string if no indice exists.
|
||||
std::string AMF::Object3::Indice(){
|
||||
std::string AMF::Object3::Indice() const{
|
||||
return myIndice;
|
||||
}
|
||||
|
||||
|
@ -695,10 +741,16 @@ std::string AMF::Object3::Print(std::string indent){
|
|||
/// contents. Tip: When sending multiple AMF objects in one go, put them in a single
|
||||
/// AMF::AMF0_DDV_CONTAINER for easy transfer.
|
||||
std::string AMF::Object3::Pack(){
|
||||
/// \TODO Implement
|
||||
std::string r = "";
|
||||
return r;
|
||||
}// pack
|
||||
|
||||
JSON::Value AMF::Object3::toJSON() const{
|
||||
/// \TODO Implement
|
||||
return JSON::Value();
|
||||
}
|
||||
|
||||
/// Parses a single AMF3 type - used recursively by the AMF::parse3() functions.
|
||||
/// This function updates i every call with the new position in the data.
|
||||
/// \param data The raw data to parse.
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "json.h"
|
||||
|
||||
/// Holds all AMF parsing and creation related functions and classes.
|
||||
namespace AMF{
|
||||
|
@ -55,7 +56,7 @@ namespace AMF{
|
|||
/// container type.
|
||||
class Object{
|
||||
public:
|
||||
std::string Indice();
|
||||
std::string Indice() const;
|
||||
obj0type GetType();
|
||||
double NumValue();
|
||||
std::string StrValue();
|
||||
|
@ -73,6 +74,7 @@ namespace AMF{
|
|||
Object(std::string indice, obj0type setType = AMF0_OBJECT);
|
||||
std::string Print(std::string indent = "");
|
||||
std::string Pack();
|
||||
JSON::Value toJSON() const;
|
||||
|
||||
protected:
|
||||
std::string myIndice; ///< Holds this objects indice, if any.
|
||||
|
@ -95,7 +97,7 @@ namespace AMF{
|
|||
/// container type.
|
||||
class Object3{
|
||||
public:
|
||||
std::string Indice();
|
||||
std::string Indice() const;
|
||||
obj3type GetType();
|
||||
double DblValue();
|
||||
int IntValue();
|
||||
|
@ -114,6 +116,7 @@ namespace AMF{
|
|||
Object3(std::string indice, obj3type setType = AMF3_OBJECT);
|
||||
std::string Print(std::string indent = "");
|
||||
std::string Pack();
|
||||
JSON::Value toJSON() const;
|
||||
|
||||
protected:
|
||||
std::string myIndice; ///< Holds this objects indice, if any.
|
||||
|
|
31
lib/dtsc.cpp
31
lib/dtsc.cpp
|
@ -991,13 +991,13 @@ namespace DTSC{
|
|||
setBootMsOffset(src.getMember("unixzero").asInt() - Util::unixMS() + Util::bootMS());
|
||||
}else{
|
||||
MEDIUM_MSG("No member \'unixzero\' found in DTSC::Scan. Calculating locally.");
|
||||
int64_t lastMs = 0;
|
||||
int64_t nowMs = 0;
|
||||
for (std::map<size_t, Track>::iterator it = tracks.begin(); it != tracks.end(); it++){
|
||||
if (it->second.track.getInt(it->second.trackLastmsField) > lastMs){
|
||||
lastMs = it->second.track.getInt(it->second.trackLastmsField);
|
||||
if (it->second.track.getInt(it->second.trackNowmsField) > nowMs){
|
||||
nowMs = it->second.track.getInt(it->second.trackNowmsField);
|
||||
}
|
||||
}
|
||||
setBootMsOffset(Util::bootMS() - lastMs);
|
||||
setBootMsOffset(Util::bootMS() - nowMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1243,6 +1243,9 @@ namespace DTSC{
|
|||
t.trackCodecField = t.track.getFieldData("codec");
|
||||
t.trackFirstmsField = t.track.getFieldData("firstms");
|
||||
t.trackLastmsField = t.track.getFieldData("lastms");
|
||||
t.trackNowmsField = t.track.getFieldData("nowms");
|
||||
// If there is no nowMs field, fall back to the lastMs field instead ( = old behaviour).
|
||||
if (!t.trackNowmsField){t.trackNowmsField = t.trackLastmsField;}
|
||||
t.trackBpsField = t.track.getFieldData("bps");
|
||||
t.trackMaxbpsField = t.track.getFieldData("maxbps");
|
||||
t.trackLangField = t.track.getFieldData("lang");
|
||||
|
@ -1332,6 +1335,9 @@ namespace DTSC{
|
|||
t.trackCodecField = t.track.getFieldData("codec");
|
||||
t.trackFirstmsField = t.track.getFieldData("firstms");
|
||||
t.trackLastmsField = t.track.getFieldData("lastms");
|
||||
t.trackNowmsField = t.track.getFieldData("nowms");
|
||||
// If there is no nowMs field, fall back to the lastMs field instead ( = old behaviour).
|
||||
if (!t.trackNowmsField){t.trackNowmsField = t.trackLastmsField;}
|
||||
t.trackBpsField = t.track.getFieldData("bps");
|
||||
t.trackMaxbpsField = t.track.getFieldData("maxbps");
|
||||
t.trackLangField = t.track.getFieldData("lang");
|
||||
|
@ -1542,6 +1548,11 @@ namespace DTSC{
|
|||
t.track.setString(t.trackCodecField, origAccess.getPointer("codec"));
|
||||
t.track.setInt(t.trackFirstmsField, origAccess.getInt("firstms"));
|
||||
t.track.setInt(t.trackLastmsField, origAccess.getInt("lastms"));
|
||||
if (origAccess.hasField("nowms")){
|
||||
t.track.setInt(t.trackNowmsField, origAccess.getInt("nowms"));
|
||||
}else{
|
||||
t.track.setInt(t.trackNowmsField, origAccess.getInt("lastms"));
|
||||
}
|
||||
t.track.setInt(t.trackBpsField, origAccess.getInt("bps"));
|
||||
t.track.setInt(t.trackMaxbpsField, origAccess.getInt("maxbps"));
|
||||
t.track.setString(t.trackLangField, origAccess.getPointer("lang"));
|
||||
|
@ -1807,6 +1818,9 @@ namespace DTSC{
|
|||
t.trackCodecField = t.track.getFieldData("codec");
|
||||
t.trackFirstmsField = t.track.getFieldData("firstms");
|
||||
t.trackLastmsField = t.track.getFieldData("lastms");
|
||||
t.trackNowmsField = t.track.getFieldData("nowms");
|
||||
// If there is no nowMs field, fall back to the lastMs field instead ( = old behaviour).
|
||||
if (!t.trackNowmsField){t.trackNowmsField = t.trackLastmsField;}
|
||||
t.trackBpsField = t.track.getFieldData("bps");
|
||||
t.trackMaxbpsField = t.track.getFieldData("maxbps");
|
||||
t.trackLangField = t.track.getFieldData("lang");
|
||||
|
@ -1999,6 +2013,15 @@ namespace DTSC{
|
|||
return t.track.getInt(t.trackLastmsField);
|
||||
}
|
||||
|
||||
void Meta::setNowms(size_t trackIdx, uint64_t nowms){
|
||||
DTSC::Track &t = tracks.at(trackIdx);
|
||||
t.track.setInt(t.trackNowmsField, nowms);
|
||||
}
|
||||
uint64_t Meta::getNowms(size_t trackIdx) const{
|
||||
const DTSC::Track &t = tracks.find(trackIdx)->second;
|
||||
return t.track.getInt(t.trackNowmsField);
|
||||
}
|
||||
|
||||
uint64_t Meta::getDuration(size_t trackIdx) const{
|
||||
const DTSC::Track &t = tracks.at(trackIdx);
|
||||
return t.track.getInt(t.trackLastmsField) - t.track.getInt(t.trackFirstmsField);
|
||||
|
|
|
@ -238,6 +238,7 @@ namespace DTSC{
|
|||
Util::RelAccXFieldData trackCodecField;
|
||||
Util::RelAccXFieldData trackFirstmsField;
|
||||
Util::RelAccXFieldData trackLastmsField;
|
||||
Util::RelAccXFieldData trackNowmsField;
|
||||
Util::RelAccXFieldData trackBpsField;
|
||||
Util::RelAccXFieldData trackMaxbpsField;
|
||||
Util::RelAccXFieldData trackLangField;
|
||||
|
@ -375,6 +376,9 @@ namespace DTSC{
|
|||
void setLastms(size_t trackIdx, uint64_t lastms);
|
||||
uint64_t getLastms(size_t trackIdx) const;
|
||||
|
||||
void setNowms(size_t trackIdx, uint64_t nowms);
|
||||
uint64_t getNowms(size_t trackIdx) const;
|
||||
|
||||
uint64_t getDuration(size_t trackIdx) const;
|
||||
|
||||
void setBps(size_t trackIdx, uint64_t bps);
|
||||
|
|
|
@ -825,7 +825,21 @@ void FLV::Tag::toMeta(DTSC::Meta &meta, AMF::Object &amf_storage, size_t &reTrac
|
|||
case 0x12: trackType = "meta"; break; // meta
|
||||
}
|
||||
|
||||
if (meta.getVod() && reTrack == INVALID_TRACK_ID){
|
||||
reTrack = meta.trackIDToIndex(getTrackID(), getpid());
|
||||
}
|
||||
|
||||
if (reTrack == INVALID_TRACK_ID){
|
||||
reTrack = meta.addTrack();
|
||||
meta.setID(reTrack, getTrackID());
|
||||
if (targetParams.count("lang")){
|
||||
meta.setLang(reTrack, targetParams.at("lang"));
|
||||
}
|
||||
}
|
||||
|
||||
if (data[0] == 0x12){
|
||||
meta.setType(reTrack, "meta");
|
||||
meta.setCodec(reTrack, "JSON");
|
||||
AMF::Object meta_in = AMF::parse((unsigned char *)data + 11, len - 15);
|
||||
AMF::Object *tmp = 0;
|
||||
if (meta_in.getContentP(1) && meta_in.getContentP(0) && (meta_in.getContentP(0)->StrValue() == "onMetaData")){
|
||||
|
@ -839,18 +853,6 @@ void FLV::Tag::toMeta(DTSC::Meta &meta, AMF::Object &amf_storage, size_t &reTrac
|
|||
return;
|
||||
}
|
||||
|
||||
if (meta.getVod() && reTrack == INVALID_TRACK_ID){
|
||||
reTrack = meta.trackIDToIndex(getTrackID(), getpid());
|
||||
}
|
||||
|
||||
if (reTrack == INVALID_TRACK_ID){
|
||||
reTrack = meta.addTrack();
|
||||
meta.setID(reTrack, getTrackID());
|
||||
if (targetParams.count("lang")){
|
||||
meta.setLang(reTrack, targetParams.at("lang"));
|
||||
}
|
||||
}
|
||||
|
||||
std::string codec = meta.getCodec(reTrack);
|
||||
if (data[0] == 0x08 && (codec == "" || codec != getAudioCodec() || (needsInitData() && isInitData()))){
|
||||
char audiodata = data[11];
|
||||
|
|
|
@ -1274,7 +1274,7 @@ std::set<size_t> Util::pickTracks(const DTSC::Meta &M, const std::set<size_t> tr
|
|||
/// It is necessary to follow up with a selectDefaultTracks() call to strip unsupported
|
||||
/// codecs/combinations.
|
||||
std::set<size_t> Util::findTracks(const DTSC::Meta &M, const JSON::Value &capa, const std::string &trackType, const std::string &trackVal, const std::string &UA){
|
||||
std::set<size_t> validTracks = capa?getSupportedTracks(M, capa, "", UA):M.getValidTracks();
|
||||
std::set<size_t> validTracks = capa?getSupportedTracks(M, capa, "", UA):M.getValidTracks(true);
|
||||
return pickTracks(M, validTracks, trackType, trackVal);
|
||||
}
|
||||
|
||||
|
@ -1288,7 +1288,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::string &track
|
|||
|
||||
std::set<size_t> Util::getSupportedTracks(const DTSC::Meta &M, const JSON::Value &capa,
|
||||
const std::string &type, const std::string &UA){
|
||||
std::set<size_t> validTracks = M.getValidTracks();
|
||||
std::set<size_t> validTracks = M.getValidTracks(true);
|
||||
uint64_t maxLastMs = 0;
|
||||
std::set<size_t> toRemove;
|
||||
for (std::set<size_t>::iterator it = validTracks.begin(); it != validTracks.end(); it++){
|
||||
|
@ -1415,7 +1415,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
|
|||
}
|
||||
/*LTS-END*/
|
||||
|
||||
std::set<size_t> validTracks = M.getValidTracks();
|
||||
std::set<size_t> validTracks = M.getValidTracks(true);
|
||||
if (capa){validTracks = getSupportedTracks(M, capa);}
|
||||
|
||||
// check which tracks don't actually exist
|
||||
|
@ -1624,6 +1624,7 @@ std::set<size_t> Util::wouldSelect(const DTSC::Meta &M, const std::map<std::stri
|
|||
/*LTS-START*/
|
||||
if (noSelAudio && M.getType(*trit) == "audio"){continue;}
|
||||
if (noSelVideo && M.getType(*trit) == "video"){continue;}
|
||||
if (noSelMeta && M.getType(*trit) == "meta"){continue;}
|
||||
if (noSelSub &&
|
||||
(M.getType(*trit) == "subtitle" || M.getCodec(*trit) == "subtitle")){
|
||||
continue;
|
||||
|
|
|
@ -63,6 +63,7 @@ namespace Util{
|
|||
uint64_t time;
|
||||
uint64_t offset;
|
||||
size_t partIndex;
|
||||
bool ghostPacket;
|
||||
};
|
||||
|
||||
/// Packet sorter used to determine which packet should be output next
|
||||
|
|
|
@ -1381,6 +1381,7 @@ namespace TS{
|
|||
if (codec == "ID3" || codec == "RAW"){sectionLen += M.getInit(*it).size();}
|
||||
if (codec == "AAC"){sectionLen += 4;} // length of AAC descriptor
|
||||
if (codec == "opus"){sectionLen += 10;} // 6 bytes registration desc, 4 bytes opus desc
|
||||
if (codec == "JSON"){sectionLen += 6;} // 6 bytes registration desc, 4 bytes opus desc
|
||||
std::string lang = M.getLang(*it);
|
||||
if (lang.size() == 3 && lang != "und"){
|
||||
sectionLen += 6; // language descriptor
|
||||
|
@ -1425,6 +1426,9 @@ namespace TS{
|
|||
es_info.append("\005\004Opus", 6);//registration descriptor
|
||||
es_info.append("\177\002\200", 3);//Opus descriptor
|
||||
es_info.append(1, (char)M.getChannels(*it));
|
||||
}else if (codec == "JSON"){
|
||||
entry.setStreamType(0x06);
|
||||
es_info.append("\005\004JSON", 6);//registration descriptor
|
||||
}else if (codec == "AC3"){
|
||||
entry.setStreamType(0x81);
|
||||
}else if (codec == "ID3"){
|
||||
|
|
|
@ -150,8 +150,11 @@ namespace TS{
|
|||
Stream::Stream(){
|
||||
psCache = 0;
|
||||
psCacheTid = 0;
|
||||
rParser = NONE;
|
||||
}
|
||||
|
||||
void Stream::setRawDataParser(rawDataType parser){rParser = parser;}
|
||||
|
||||
Stream::~Stream(){}
|
||||
|
||||
void Stream::parse(char *newPack, uint64_t bytePos){
|
||||
|
@ -288,6 +291,10 @@ namespace TS{
|
|||
std::string reg = desc.getRegistration();
|
||||
if (reg == "Opus"){
|
||||
pidToCodec[pid] = OPUS;
|
||||
}else if (reg == "JSON"){
|
||||
pidToCodec[pid] = JSON;
|
||||
}else if (rParser == JSON){
|
||||
pidToCodec[pid] = JSON;
|
||||
}else{
|
||||
pidToCodec.erase(pid);
|
||||
}
|
||||
|
@ -644,6 +651,13 @@ namespace TS{
|
|||
mp2Hdr[tid] = std::string(pesPayload, realPayloadSize);
|
||||
}
|
||||
}
|
||||
if (thisCodec == JSON){
|
||||
//Ignore if invalid
|
||||
if (realPayloadSize < 2){return;}
|
||||
out.push_back(DTSC::Packet());
|
||||
//Skip the first two bytes
|
||||
out.back().genericFill(timeStamp, timeOffset, tid, pesPayload+2, realPayloadSize-2, bPos, 0);
|
||||
}
|
||||
if (thisCodec == OPUS){
|
||||
size_t offset = 0;
|
||||
while (realPayloadSize > offset+1){
|
||||
|
@ -1058,6 +1072,11 @@ namespace TS{
|
|||
codec = "AC3";
|
||||
size = 16;
|
||||
}break;
|
||||
case JSON:{
|
||||
addNewTrack = true;
|
||||
type = "meta";
|
||||
codec = "JSON";
|
||||
}break;
|
||||
case OPUS:{
|
||||
addNewTrack = true;
|
||||
type = "audio";
|
||||
|
|
|
@ -22,6 +22,11 @@ namespace TS{
|
|||
OPUS = 0x060001
|
||||
};
|
||||
|
||||
enum rawDataType{
|
||||
NONE = 0,
|
||||
JSON
|
||||
};
|
||||
|
||||
class ADTSRemainder{
|
||||
private:
|
||||
char *data;
|
||||
|
@ -75,9 +80,11 @@ namespace TS{
|
|||
std::set<size_t> getActiveTracks();
|
||||
|
||||
void setLastms(size_t tid, uint64_t timestamp);
|
||||
void setRawDataParser(rawDataType parser);
|
||||
|
||||
private:
|
||||
uint64_t lastPAT;
|
||||
rawDataType rParser;
|
||||
ProgramAssociationTable associationTable;
|
||||
std::map<size_t, ADTSRemainder> remainders;
|
||||
|
||||
|
|
|
@ -840,6 +840,11 @@ namespace Util{
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Returns true if the given field exists.
|
||||
bool RelAccX::hasField(const std::string & name) const{
|
||||
return (fields.find(name) != fields.end());
|
||||
}
|
||||
|
||||
/// Returns the (max) size of the given field.
|
||||
/// For string types, returns the exact size excluding terminating null byte.
|
||||
/// For other types, returns the maximum size possible.
|
||||
|
|
|
@ -86,6 +86,7 @@ namespace Util{
|
|||
size = s;
|
||||
offset = o;
|
||||
}
|
||||
operator bool() const {return offset;}
|
||||
};
|
||||
|
||||
#define RAX_NESTED 0x01
|
||||
|
@ -158,6 +159,7 @@ namespace Util{
|
|||
bool isExit() const;
|
||||
bool isReload() const;
|
||||
bool isRecordAvailable(uint64_t recordNo) const;
|
||||
bool hasField(const std::string &name) const;
|
||||
uint32_t getSize(const std::string &name, uint64_t recordNo = 0) const;
|
||||
|
||||
char *getPointer(const std::string &name, uint64_t recordNo = 0) const;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue