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:
Thulinma 2022-11-09 10:35:07 +01:00
parent c337fff614
commit 3e2a17ff93
36 changed files with 1054 additions and 469 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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);

View file

@ -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);

View file

@ -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];

View file

@ -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;

View file

@ -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

View file

@ -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"){

View file

@ -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";

View file

@ -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;

View file

@ -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.

View file

@ -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;