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

@ -551,6 +551,7 @@ endmacro()
makeOutput(RTMP rtmp) makeOutput(RTMP rtmp)
makeOutput(DTSC dtsc) makeOutput(DTSC dtsc)
makeOutput(JSONLine jsonline)
makeOutput(OGG ogg http) makeOutput(OGG ogg http)
makeOutput(FLV flv http) makeOutput(FLV flv http)
makeOutput(HTTPMinimalServer http_minimalserver http) makeOutput(HTTPMinimalServer http_minimalserver http)

View file

@ -6,7 +6,7 @@
#include <sstream> #include <sstream>
/// Returns the std::string Indice for the current object, if available. /// Returns the std::string Indice for the current object, if available.
/// Returns an empty string if no indice exists. /// Returns an empty string if no indice exists.
std::string AMF::Object::Indice(){ std::string AMF::Object::Indice() const{
return myIndice; return myIndice;
} }
@ -190,6 +190,52 @@ std::string AMF::Object::Print(std::string indent){
return st.str(); return st.str();
}// print }// 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. /// 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 /// 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 /// 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 the std::string Indice for the current object, if available.
/// Returns an empty string if no indice exists. /// Returns an empty string if no indice exists.
std::string AMF::Object3::Indice(){ std::string AMF::Object3::Indice() const{
return myIndice; 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 /// contents. Tip: When sending multiple AMF objects in one go, put them in a single
/// AMF::AMF0_DDV_CONTAINER for easy transfer. /// AMF::AMF0_DDV_CONTAINER for easy transfer.
std::string AMF::Object3::Pack(){ std::string AMF::Object3::Pack(){
/// \TODO Implement
std::string r = ""; std::string r = "";
return r; return r;
}// pack }// pack
JSON::Value AMF::Object3::toJSON() const{
/// \TODO Implement
return JSON::Value();
}
/// Parses a single AMF3 type - used recursively by the AMF::parse3() functions. /// 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. /// This function updates i every call with the new position in the data.
/// \param data The raw data to parse. /// \param data The raw data to parse.

View file

@ -5,6 +5,7 @@
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <vector> #include <vector>
#include "json.h"
/// Holds all AMF parsing and creation related functions and classes. /// Holds all AMF parsing and creation related functions and classes.
namespace AMF{ namespace AMF{
@ -55,7 +56,7 @@ namespace AMF{
/// container type. /// container type.
class Object{ class Object{
public: public:
std::string Indice(); std::string Indice() const;
obj0type GetType(); obj0type GetType();
double NumValue(); double NumValue();
std::string StrValue(); std::string StrValue();
@ -73,6 +74,7 @@ namespace AMF{
Object(std::string indice, obj0type setType = AMF0_OBJECT); Object(std::string indice, obj0type setType = AMF0_OBJECT);
std::string Print(std::string indent = ""); std::string Print(std::string indent = "");
std::string Pack(); std::string Pack();
JSON::Value toJSON() const;
protected: protected:
std::string myIndice; ///< Holds this objects indice, if any. std::string myIndice; ///< Holds this objects indice, if any.
@ -95,7 +97,7 @@ namespace AMF{
/// container type. /// container type.
class Object3{ class Object3{
public: public:
std::string Indice(); std::string Indice() const;
obj3type GetType(); obj3type GetType();
double DblValue(); double DblValue();
int IntValue(); int IntValue();
@ -114,6 +116,7 @@ namespace AMF{
Object3(std::string indice, obj3type setType = AMF3_OBJECT); Object3(std::string indice, obj3type setType = AMF3_OBJECT);
std::string Print(std::string indent = ""); std::string Print(std::string indent = "");
std::string Pack(); std::string Pack();
JSON::Value toJSON() const;
protected: protected:
std::string myIndice; ///< Holds this objects indice, if any. 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()); setBootMsOffset(src.getMember("unixzero").asInt() - Util::unixMS() + Util::bootMS());
}else{ }else{
MEDIUM_MSG("No member \'unixzero\' found in DTSC::Scan. Calculating locally."); 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++){ for (std::map<size_t, Track>::iterator it = tracks.begin(); it != tracks.end(); it++){
if (it->second.track.getInt(it->second.trackLastmsField) > lastMs){ if (it->second.track.getInt(it->second.trackNowmsField) > nowMs){
lastMs = it->second.track.getInt(it->second.trackLastmsField); 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.trackCodecField = t.track.getFieldData("codec");
t.trackFirstmsField = t.track.getFieldData("firstms"); t.trackFirstmsField = t.track.getFieldData("firstms");
t.trackLastmsField = t.track.getFieldData("lastms"); 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.trackBpsField = t.track.getFieldData("bps");
t.trackMaxbpsField = t.track.getFieldData("maxbps"); t.trackMaxbpsField = t.track.getFieldData("maxbps");
t.trackLangField = t.track.getFieldData("lang"); t.trackLangField = t.track.getFieldData("lang");
@ -1332,6 +1335,9 @@ namespace DTSC{
t.trackCodecField = t.track.getFieldData("codec"); t.trackCodecField = t.track.getFieldData("codec");
t.trackFirstmsField = t.track.getFieldData("firstms"); t.trackFirstmsField = t.track.getFieldData("firstms");
t.trackLastmsField = t.track.getFieldData("lastms"); 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.trackBpsField = t.track.getFieldData("bps");
t.trackMaxbpsField = t.track.getFieldData("maxbps"); t.trackMaxbpsField = t.track.getFieldData("maxbps");
t.trackLangField = t.track.getFieldData("lang"); t.trackLangField = t.track.getFieldData("lang");
@ -1542,6 +1548,11 @@ namespace DTSC{
t.track.setString(t.trackCodecField, origAccess.getPointer("codec")); t.track.setString(t.trackCodecField, origAccess.getPointer("codec"));
t.track.setInt(t.trackFirstmsField, origAccess.getInt("firstms")); t.track.setInt(t.trackFirstmsField, origAccess.getInt("firstms"));
t.track.setInt(t.trackLastmsField, origAccess.getInt("lastms")); 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.trackBpsField, origAccess.getInt("bps"));
t.track.setInt(t.trackMaxbpsField, origAccess.getInt("maxbps")); t.track.setInt(t.trackMaxbpsField, origAccess.getInt("maxbps"));
t.track.setString(t.trackLangField, origAccess.getPointer("lang")); t.track.setString(t.trackLangField, origAccess.getPointer("lang"));
@ -1807,6 +1818,9 @@ namespace DTSC{
t.trackCodecField = t.track.getFieldData("codec"); t.trackCodecField = t.track.getFieldData("codec");
t.trackFirstmsField = t.track.getFieldData("firstms"); t.trackFirstmsField = t.track.getFieldData("firstms");
t.trackLastmsField = t.track.getFieldData("lastms"); 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.trackBpsField = t.track.getFieldData("bps");
t.trackMaxbpsField = t.track.getFieldData("maxbps"); t.trackMaxbpsField = t.track.getFieldData("maxbps");
t.trackLangField = t.track.getFieldData("lang"); t.trackLangField = t.track.getFieldData("lang");
@ -1999,6 +2013,15 @@ namespace DTSC{
return t.track.getInt(t.trackLastmsField); 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{ uint64_t Meta::getDuration(size_t trackIdx) const{
const DTSC::Track &t = tracks.at(trackIdx); const DTSC::Track &t = tracks.at(trackIdx);
return t.track.getInt(t.trackLastmsField) - t.track.getInt(t.trackFirstmsField); return t.track.getInt(t.trackLastmsField) - t.track.getInt(t.trackFirstmsField);

View file

@ -238,6 +238,7 @@ namespace DTSC{
Util::RelAccXFieldData trackCodecField; Util::RelAccXFieldData trackCodecField;
Util::RelAccXFieldData trackFirstmsField; Util::RelAccXFieldData trackFirstmsField;
Util::RelAccXFieldData trackLastmsField; Util::RelAccXFieldData trackLastmsField;
Util::RelAccXFieldData trackNowmsField;
Util::RelAccXFieldData trackBpsField; Util::RelAccXFieldData trackBpsField;
Util::RelAccXFieldData trackMaxbpsField; Util::RelAccXFieldData trackMaxbpsField;
Util::RelAccXFieldData trackLangField; Util::RelAccXFieldData trackLangField;
@ -375,6 +376,9 @@ namespace DTSC{
void setLastms(size_t trackIdx, uint64_t lastms); void setLastms(size_t trackIdx, uint64_t lastms);
uint64_t getLastms(size_t trackIdx) const; 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; uint64_t getDuration(size_t trackIdx) const;
void setBps(size_t trackIdx, uint64_t bps); 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 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){ 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 meta_in = AMF::parse((unsigned char *)data + 11, len - 15);
AMF::Object *tmp = 0; AMF::Object *tmp = 0;
if (meta_in.getContentP(1) && meta_in.getContentP(0) && (meta_in.getContentP(0)->StrValue() == "onMetaData")){ 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; 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); std::string codec = meta.getCodec(reTrack);
if (data[0] == 0x08 && (codec == "" || codec != getAudioCodec() || (needsInitData() && isInitData()))){ if (data[0] == 0x08 && (codec == "" || codec != getAudioCodec() || (needsInitData() && isInitData()))){
char audiodata = data[11]; 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 /// It is necessary to follow up with a selectDefaultTracks() call to strip unsupported
/// codecs/combinations. /// 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> 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); 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, std::set<size_t> Util::getSupportedTracks(const DTSC::Meta &M, const JSON::Value &capa,
const std::string &type, const std::string &UA){ 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; uint64_t maxLastMs = 0;
std::set<size_t> toRemove; std::set<size_t> toRemove;
for (std::set<size_t>::iterator it = validTracks.begin(); it != validTracks.end(); it++){ 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*/ /*LTS-END*/
std::set<size_t> validTracks = M.getValidTracks(); std::set<size_t> validTracks = M.getValidTracks(true);
if (capa){validTracks = getSupportedTracks(M, capa);} if (capa){validTracks = getSupportedTracks(M, capa);}
// check which tracks don't actually exist // 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*/ /*LTS-START*/
if (noSelAudio && M.getType(*trit) == "audio"){continue;} if (noSelAudio && M.getType(*trit) == "audio"){continue;}
if (noSelVideo && M.getType(*trit) == "video"){continue;} if (noSelVideo && M.getType(*trit) == "video"){continue;}
if (noSelMeta && M.getType(*trit) == "meta"){continue;}
if (noSelSub && if (noSelSub &&
(M.getType(*trit) == "subtitle" || M.getCodec(*trit) == "subtitle")){ (M.getType(*trit) == "subtitle" || M.getCodec(*trit) == "subtitle")){
continue; continue;

View file

@ -63,6 +63,7 @@ namespace Util{
uint64_t time; uint64_t time;
uint64_t offset; uint64_t offset;
size_t partIndex; size_t partIndex;
bool ghostPacket;
}; };
/// Packet sorter used to determine which packet should be output next /// 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 == "ID3" || codec == "RAW"){sectionLen += M.getInit(*it).size();}
if (codec == "AAC"){sectionLen += 4;} // length of AAC descriptor if (codec == "AAC"){sectionLen += 4;} // length of AAC descriptor
if (codec == "opus"){sectionLen += 10;} // 6 bytes registration desc, 4 bytes opus desc 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); std::string lang = M.getLang(*it);
if (lang.size() == 3 && lang != "und"){ if (lang.size() == 3 && lang != "und"){
sectionLen += 6; // language descriptor sectionLen += 6; // language descriptor
@ -1425,6 +1426,9 @@ namespace TS{
es_info.append("\005\004Opus", 6);//registration descriptor es_info.append("\005\004Opus", 6);//registration descriptor
es_info.append("\177\002\200", 3);//Opus descriptor es_info.append("\177\002\200", 3);//Opus descriptor
es_info.append(1, (char)M.getChannels(*it)); 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"){ }else if (codec == "AC3"){
entry.setStreamType(0x81); entry.setStreamType(0x81);
}else if (codec == "ID3"){ }else if (codec == "ID3"){

View file

@ -150,8 +150,11 @@ namespace TS{
Stream::Stream(){ Stream::Stream(){
psCache = 0; psCache = 0;
psCacheTid = 0; psCacheTid = 0;
rParser = NONE;
} }
void Stream::setRawDataParser(rawDataType parser){rParser = parser;}
Stream::~Stream(){} Stream::~Stream(){}
void Stream::parse(char *newPack, uint64_t bytePos){ void Stream::parse(char *newPack, uint64_t bytePos){
@ -288,6 +291,10 @@ namespace TS{
std::string reg = desc.getRegistration(); std::string reg = desc.getRegistration();
if (reg == "Opus"){ if (reg == "Opus"){
pidToCodec[pid] = OPUS; pidToCodec[pid] = OPUS;
}else if (reg == "JSON"){
pidToCodec[pid] = JSON;
}else if (rParser == JSON){
pidToCodec[pid] = JSON;
}else{ }else{
pidToCodec.erase(pid); pidToCodec.erase(pid);
} }
@ -644,6 +651,13 @@ namespace TS{
mp2Hdr[tid] = std::string(pesPayload, realPayloadSize); 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){ if (thisCodec == OPUS){
size_t offset = 0; size_t offset = 0;
while (realPayloadSize > offset+1){ while (realPayloadSize > offset+1){
@ -1058,6 +1072,11 @@ namespace TS{
codec = "AC3"; codec = "AC3";
size = 16; size = 16;
}break; }break;
case JSON:{
addNewTrack = true;
type = "meta";
codec = "JSON";
}break;
case OPUS:{ case OPUS:{
addNewTrack = true; addNewTrack = true;
type = "audio"; type = "audio";

View file

@ -22,6 +22,11 @@ namespace TS{
OPUS = 0x060001 OPUS = 0x060001
}; };
enum rawDataType{
NONE = 0,
JSON
};
class ADTSRemainder{ class ADTSRemainder{
private: private:
char *data; char *data;
@ -75,9 +80,11 @@ namespace TS{
std::set<size_t> getActiveTracks(); std::set<size_t> getActiveTracks();
void setLastms(size_t tid, uint64_t timestamp); void setLastms(size_t tid, uint64_t timestamp);
void setRawDataParser(rawDataType parser);
private: private:
uint64_t lastPAT; uint64_t lastPAT;
rawDataType rParser;
ProgramAssociationTable associationTable; ProgramAssociationTable associationTable;
std::map<size_t, ADTSRemainder> remainders; std::map<size_t, ADTSRemainder> remainders;

View file

@ -840,6 +840,11 @@ namespace Util{
return true; 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. /// Returns the (max) size of the given field.
/// For string types, returns the exact size excluding terminating null byte. /// For string types, returns the exact size excluding terminating null byte.
/// For other types, returns the maximum size possible. /// For other types, returns the maximum size possible.

View file

@ -86,6 +86,7 @@ namespace Util{
size = s; size = s;
offset = o; offset = o;
} }
operator bool() const {return offset;}
}; };
#define RAX_NESTED 0x01 #define RAX_NESTED 0x01
@ -158,6 +159,7 @@ namespace Util{
bool isExit() const; bool isExit() const;
bool isReload() const; bool isReload() const;
bool isRecordAvailable(uint64_t recordNo) 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; uint32_t getSize(const std::string &name, uint64_t recordNo = 0) const;
char *getPointer(const std::string &name, uint64_t recordNo = 0) const; char *getPointer(const std::string &name, uint64_t recordNo = 0) const;

View file

@ -349,6 +349,8 @@ namespace Mist{
} }
for (std::set<size_t>::iterator idx = tracks.begin(); idx != tracks.end(); idx++){ for (std::set<size_t>::iterator idx = tracks.begin(); idx != tracks.end(); idx++){
size_t i = *idx; size_t i = *idx;
//Don't delete idle metadata tracks
if (M.getType(i) == "meta"){continue;}
uint64_t lastUp = M.getLastUpdated(i); uint64_t lastUp = M.getLastUpdated(i);
//Prevent issues when getLastUpdated > current time. This can happen if the second rolls over exactly during this loop. //Prevent issues when getLastUpdated > current time. This can happen if the second rolls over exactly during this loop.
if (lastUp >= time){continue;} if (lastUp >= time){continue;}

View file

@ -478,7 +478,7 @@ namespace Mist{
packSendSize = 24 + (BsetPart.timeOffset ? 17 : 0) + (BsetPart.bpos ? 15 : 0) + 19 + packSendSize = 24 + (BsetPart.timeOffset ? 17 : 0) + (BsetPart.bpos ? 15 : 0) + 19 +
stszBox.getEntrySize(stszIndex) + 11 - 2 + 19; stszBox.getEntrySize(stszIndex) + 11 - 2 + 19;
meta.update(BsetPart.time, BsetPart.timeOffset, tNumber, meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
stszBox.getEntrySize(stszIndex) - 2, BsetPart.bpos, true, packSendSize); stszBox.getEntrySize(stszIndex) - 2, BsetPart.bpos+2, true, packSendSize);
} }
}else{ }else{
meta.update(BsetPart.time, BsetPart.timeOffset, tNumber, meta.update(BsetPart.time, BsetPart.timeOffset, tNumber,
@ -558,16 +558,11 @@ namespace Mist{
} }
if (M.getCodec(curPart.trackID) == "subtitle"){ if (M.getCodec(curPart.trackID) == "subtitle"){
unsigned int txtLen = Bit::btohs(readBuffer + (curPart.bpos-readPos));
if (!txtLen && false){
curPart.index++;
return getNext(idx);
}
static JSON::Value thisPack; static JSON::Value thisPack;
thisPack.null(); thisPack.null();
thisPack["trackid"] = (uint64_t)curPart.trackID; thisPack["trackid"] = (uint64_t)curPart.trackID;
thisPack["bpos"] = curPart.bpos; //(long long)fileSource.tellg(); thisPack["bpos"] = curPart.bpos; //(long long)fileSource.tellg();
thisPack["data"] = std::string(readBuffer + (curPart.bpos-readPos) + 2, txtLen); thisPack["data"] = std::string(readBuffer + (curPart.bpos-readPos), curPart.size);
thisPack["time"] = curPart.time; thisPack["time"] = curPart.time;
if (curPart.duration){thisPack["duration"] = curPart.duration;} if (curPart.duration){thisPack["duration"] = curPart.duration;}
thisPack["keyframe"] = true; thisPack["keyframe"] = true;
@ -583,6 +578,7 @@ namespace Mist{
curPart.index++; curPart.index++;
if (curPart.index < headerData(M.getID(curPart.trackID)).size()){ if (curPart.index < headerData(M.getID(curPart.trackID)).size()){
headerData(M.getID(curPart.trackID)).getPart(curPart.index, curPart.bpos); headerData(M.getID(curPart.trackID)).getPart(curPart.index, curPart.bpos);
if (M.getCodec(curPart.trackID) == "subtitle"){curPart.bpos += 2;}
curPart.size = parts.getSize(curPart.index); curPart.size = parts.getSize(curPart.index);
curPart.offset = parts.getOffset(curPart.index); curPart.offset = parts.getOffset(curPart.index);
curPart.time = M.getPartTime(curPart.index, thisIdx); curPart.time = M.getPartTime(curPart.index, thisIdx);
@ -616,6 +612,7 @@ namespace Mist{
for (size_t i = 0; i < headerDataSize; i++){ for (size_t i = 0; i < headerDataSize; i++){
thisHeader.getPart(i, addPart.bpos); thisHeader.getPart(i, addPart.bpos);
if (M.getCodec(idx) == "subtitle"){addPart.bpos += 2;}
addPart.size = parts.getSize(i); addPart.size = parts.getSize(i);
addPart.offset = parts.getOffset(i); addPart.offset = parts.getOffset(i);
addPart.time = M.getPartTime(i, idx); addPart.time = M.getPartTime(i, idx);

View file

@ -243,6 +243,25 @@ namespace Mist{
capa["optional"]["segmentsize"]["type"] = "uint"; capa["optional"]["segmentsize"]["type"] = "uint";
capa["optional"]["segmentsize"]["default"] = 1900; capa["optional"]["segmentsize"]["default"] = 1900;
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
JSON::Value option;
option["long"] = "datatrack";
option["short"] = "D";
option["arg"] = "string";
option["default"] = "";
option["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", option);
capa["optional"]["fallback_stream"]["name"] = "Fallback stream"; capa["optional"]["fallback_stream"]["name"] = "Fallback stream";
capa["optional"]["fallback_stream"]["help"] = capa["optional"]["fallback_stream"]["help"] =
"Alternative stream to load for playback when there is no active broadcast"; "Alternative stream to load for playback when there is no active broadcast";
@ -253,7 +272,7 @@ namespace Mist{
capa["optional"]["raw"]["help"] = "Enable raw MPEG-TS passthrough mode"; capa["optional"]["raw"]["help"] = "Enable raw MPEG-TS passthrough mode";
capa["optional"]["raw"]["option"] = "--raw"; capa["optional"]["raw"]["option"] = "--raw";
JSON::Value option; option.null();
option["long"] = "raw"; option["long"] = "raw";
option["short"] = "R"; option["short"] = "R";
option["help"] = "Enable raw MPEG-TS passthrough mode"; option["help"] = "Enable raw MPEG-TS passthrough mode";
@ -277,6 +296,11 @@ namespace Mist{
config->getOption("input", true).append("ts-exec:srt-live-transmit " + srtUrl.getUrl() + " file://con"); config->getOption("input", true).append("ts-exec:srt-live-transmit " + srtUrl.getUrl() + " file://con");
INFO_MSG("Rewriting SRT source '%s' to '%s'", source.c_str(), config->getString("input").c_str()); INFO_MSG("Rewriting SRT source '%s' to '%s'", source.c_str(), config->getString("input").c_str());
} }
if (config->getString("datatrack") == "json"){
liveStream.setRawDataParser(TS::JSON);
tsStream.setRawDataParser(TS::JSON);
}
// We call preRun early and, if successful, close the opened reader. // We call preRun early and, if successful, close the opened reader.
// This is to ensure we have udpMode/rawMode/standAlone all set properly before the first call to needsLock. // This is to ensure we have udpMode/rawMode/standAlone all set properly before the first call to needsLock.
// The reader must be closed so that the angel process does not have a reader open. // The reader must be closed so that the angel process does not have a reader open.

View file

@ -146,6 +146,25 @@ namespace Mist{
option["help"] = "Enable raw MPEG-TS passthrough mode"; option["help"] = "Enable raw MPEG-TS passthrough mode";
config->addOption("raw", option); config->addOption("raw", option);
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
option.null();
option["long"] = "datatrack";
option["short"] = "D";
option["arg"] = "string";
option["default"] = "";
option["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", option);
lastTimeStamp = 0; lastTimeStamp = 0;
timeStampOffset = 0; timeStampOffset = 0;
receiver_ctx = 0; receiver_ctx = 0;
@ -156,7 +175,12 @@ namespace Mist{
rist_destroy(receiver_ctx); rist_destroy(receiver_ctx);
} }
bool inputTSRIST::checkArguments(){return true;} bool inputTSRIST::checkArguments(){
if (config->getString("datatrack") == "json"){
tsStream.setRawDataParser(TS::JSON);
}
return true;
}
/// Live Setup of SRT Input. Runs only if we are the "main" thread /// Live Setup of SRT Input. Runs only if we are the "main" thread
bool inputTSRIST::preRun(){ bool inputTSRIST::preRun(){

View file

@ -117,6 +117,25 @@ namespace Mist{
option["help"] = "Enable raw MPEG-TS passthrough mode"; option["help"] = "Enable raw MPEG-TS passthrough mode";
config->addOption("raw", option); config->addOption("raw", option);
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
option.null();
option["long"] = "datatrack";
option["short"] = "D";
option["arg"] = "string";
option["default"] = "";
option["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", option);
// Setup if we are called form with a thread for push-based input. // Setup if we are called form with a thread for push-based input.
if (s.connected()){ if (s.connected()){
srtConn = s; srtConn = s;
@ -140,7 +159,12 @@ namespace Mist{
inputTSSRT::~inputTSSRT(){} inputTSSRT::~inputTSSRT(){}
bool inputTSSRT::checkArguments(){return true;} bool inputTSSRT::checkArguments(){
if (config->getString("datatrack") == "json"){
tsStream.setRawDataParser(TS::JSON);
}
return true;
}
/// Live Setup of SRT Input. Runs only if we are the "main" thread /// Live Setup of SRT Input. Runs only if we are the "main" thread
bool inputTSSRT::preRun(){ bool inputTSSRT::preRun(){

View file

@ -21,6 +21,7 @@ outputs = [
{'name' : 'WAV', 'format' : 'wav', 'extra': ['http']}, {'name' : 'WAV', 'format' : 'wav', 'extra': ['http']},
{'name' : 'SDP', 'format' : 'sdp', 'extra': ['http']}, {'name' : 'SDP', 'format' : 'sdp', 'extra': ['http']},
{'name' : 'HTTP', 'format' : 'http_internal', 'extra': ['http','embed']}, {'name' : 'HTTP', 'format' : 'http_internal', 'extra': ['http','embed']},
{'name' : 'JSONLine', 'format' : 'jsonline'},
] ]
if usessl if usessl

View file

@ -94,7 +94,8 @@ namespace Mist{
dataWaitTimeout = 2500; dataWaitTimeout = 2500;
pushing = false; pushing = false;
recursingSync = false; recursingSync = false;
firstTime = 0; firstTime = Util::bootMS();
thisTime = 0;
firstPacketTime = 0xFFFFFFFFFFFFFFFFull; firstPacketTime = 0xFFFFFFFFFFFFFFFFull;
lastPacketTime = 0; lastPacketTime = 0;
tkn = ""; tkn = "";
@ -247,7 +248,7 @@ namespace Mist{
size_t mainTrack = getMainSelectedTrack(); size_t mainTrack = getMainSelectedTrack();
if (mainTrack != INVALID_TRACK_ID){ if (mainTrack != INVALID_TRACK_ID){
DTSC::Keys keys(M.keys(mainTrack)); DTSC::Keys keys(M.keys(mainTrack));
if (keys.getValidCount() >= minTracks || M.getLastms(mainTrack) - M.getFirstms(mainTrack) > minMs){ if (keys.getValidCount() >= minTracks || M.getNowms(mainTrack) - M.getFirstms(mainTrack) > minMs){
return true; return true;
} }
HIGH_MSG("NOT READY YET (%zu tracks, main track: %zu, with %zu keys)", HIGH_MSG("NOT READY YET (%zu tracks, main track: %zu, with %zu keys)",
@ -393,7 +394,7 @@ namespace Mist{
} }
bool autoSeek = buffer.size(); bool autoSeek = buffer.size();
uint64_t seekTarget = buffer.getSyncMode()?currentTime():0; uint64_t seekTarget = buffer.getSyncMode()?thisTime:0;
std::set<size_t> newSelects = std::set<size_t> newSelects =
Util::wouldSelect(M, targetParams, capa, UA, autoSeek ? seekTarget : 0); Util::wouldSelect(M, targetParams, capa, UA, autoSeek ? seekTarget : 0);
@ -401,7 +402,7 @@ namespace Mist{
std::set<size_t> toRemove; std::set<size_t> toRemove;
for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){ for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){
// autoSeeking and target not in bounds? Drop it too. // autoSeeking and target not in bounds? Drop it too.
if (M.getLastms(*it) < std::max(seekTarget, (uint64_t)6000lu) - 6000){ if (M.getNowms(*it) < std::max(seekTarget, (uint64_t)6000lu) - 6000){
toRemove.insert(*it); toRemove.insert(*it);
} }
} }
@ -493,7 +494,7 @@ namespace Mist{
//Abort if there are no keys //Abort if there are no keys
if (!keys.getValidCount()){return 0;} if (!keys.getValidCount()){return 0;}
//Get the key for the current time //Get the key for the current time
size_t keyNum = M.getKeyNumForTime(trk, lastPacketTime); size_t keyNum = M.getKeyNumForTime(trk, thisTime);
if (keyNum == INVALID_KEY_NUM){return 0;} if (keyNum == INVALID_KEY_NUM){return 0;}
if (keys.getEndValid() <= keyNum+1){return 0;} if (keys.getEndValid() <= keyNum+1){return 0;}
//Return the next key //Return the next key
@ -720,6 +721,13 @@ namespace Mist{
return buffer.begin()->time; return buffer.begin()->time;
} }
/// Return the intended target current time of the media buffer (as opposed to actual)
/// This takes into account the current playback speed as well as the maxSkipAhead setting.
uint64_t Output::targetTime(){
if (!realTime){return currentTime();}
return (((Util::bootMS() - firstTime) * 1000) / realTime + maxSkipAhead);
}
/// Return the start time of the selected tracks. /// Return the start time of the selected tracks.
/// Returns the start time of earliest track if nothing is selected. /// Returns the start time of earliest track if nothing is selected.
/// Returns zero if no tracks exist. /// Returns zero if no tracks exist.
@ -836,16 +844,16 @@ namespace Mist{
} }
HIGH_MSG("Seeking for pos %" PRIu64, pos); HIGH_MSG("Seeking for pos %" PRIu64, pos);
if (meta.getLive() && meta.getLastms(tid) < pos){ if (meta.getLive() && meta.getNowms(tid) < pos){
unsigned int maxTime = 0; unsigned int maxTime = 0;
while (meta.getLastms(tid) < pos && myConn && ++maxTime <= 20 && keepGoing()){ while (meta.getNowms(tid) < pos && myConn && ++maxTime <= 20 && keepGoing()){
Util::wait(500); Util::wait(500);
stats(); stats();
} }
} }
if (meta.getLastms(tid) < pos){ if (meta.getNowms(tid) < pos){
WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: past end of track (= %" PRIu64 "ms).", WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: past end of track (= %" PRIu64 "ms).",
pos, tid, meta.getLastms(tid)); pos, tid, meta.getNowms(tid));
userSelect.erase(tid); userSelect.erase(tid);
return false; return false;
} }
@ -884,46 +892,24 @@ namespace Mist{
tmpPack.reInit(curPage[tid].mapped + tmp.offset, 0, true); tmpPack.reInit(curPage[tid].mapped + tmp.offset, 0, true);
tmp.time = tmpPack.getTime(); tmp.time = tmpPack.getTime();
char *mpd = curPage[tid].mapped; char *mpd = curPage[tid].mapped;
uint64_t nowMs = M.getNowms(tid);
while (tmp.time < pos && tmpPack){ while (tmp.time < pos && tmpPack){
tmp.offset += tmpPack.getDataLen(); tmp.offset += tmpPack.getDataLen();
tmpPack.reInit(mpd + tmp.offset, 0, true); tmpPack.reInit(mpd + tmp.offset, 0, true);
tmp.time = tmpPack.getTime(); tmp.time = tmpPack.getTime();
} }
if (tmpPack){ if (tmpPack){
tmp.ghostPacket = false;
HIGH_MSG("Sought to time %" PRIu64 " in %s", tmp.time, curPage[tid].name.c_str()); HIGH_MSG("Sought to time %" PRIu64 " in %s", tmp.time, curPage[tid].name.c_str());
tmp.partIndex = M.getPartIndex(tmpPack.getTime(), tmp.tid); tmp.partIndex = M.getPartIndex(tmpPack.getTime(), tmp.tid);
buffer.insert(tmp); buffer.insert(tmp);
return true; return true;
} }
// don't print anything for empty packets - not sign of corruption, just unfinished stream. tmp.partIndex = M.getPartIndex(nowMs, tmp.tid);
if (curPage[tid].mapped[tmp.offset] != 0){ tmp.ghostPacket = true;
//There's a chance the packet header was written in between this check and the previous. tmp.time = nowMs;
//Let's check one more time before aborting buffer.insert(tmp);
tmpPack.reInit(mpd + tmp.offset, 0, true); return true;
tmp.time = tmpPack.getTime();
if (tmpPack){
HIGH_MSG("Sought to time %" PRIu64 " in %s", tmp.time, curPage[tid].name.c_str());
tmp.partIndex = M.getPartIndex(tmpPack.getTime(), tmp.tid);
buffer.insert(tmp);
return true;
}
FAIL_MSG("Noes! Couldn't find packet on track %zu because of some kind of corruption error "
"or somesuch.",
tid);
return false;
}
VERYHIGH_MSG("Track %zu no data (key %" PRIu32 " @ %" PRIu64 ") - waiting...", tid,
keyNum + (getNextKey ? 1 : 0), tmp.offset);
uint32_t i = 0;
while (meta.getVod() && curPage[tid].mapped[tmp.offset] == 0 && ++i <= 10){
Util::wait(100 * i);
stats();
}
if (curPage[tid].mapped[tmp.offset]){return seek(tid, pos, getNextKey);}
FAIL_MSG("Track %zu no data (key %" PRIu32 "@%" PRIu64 ", page %s, time %" PRIu64 " -> %" PRIu64 ", next=%" PRIu64 ") - timeout", tid, keyNum + (getNextKey ? 1 : 0), tmp.offset, curPage[tid].name.c_str(), pos, actualKeyTime, keys.getTime(keyNum+1));
userSelect.erase(tid);
firstTime = Util::bootMS() - (buffer.begin()->time * realTime / 1000);
return false;
} }
/// This function decides where in the stream initial playback starts. /// This function decides where in the stream initial playback starts.
@ -948,7 +934,7 @@ namespace Mist{
bool good = true; bool good = true;
// check if all tracks have data for this point in time // check if all tracks have data for this point in time
for (std::map<size_t, Comms::Users>::iterator ti = userSelect.begin(); ti != userSelect.end(); ++ti){ for (std::map<size_t, Comms::Users>::iterator ti = userSelect.begin(); ti != userSelect.end(); ++ti){
if (meta.getLastms(ti->first) < seekPos + needsLookAhead){ if (meta.getNowms(ti->first) < seekPos + needsLookAhead){
good = false; good = false;
break; break;
} }
@ -957,15 +943,15 @@ namespace Mist{
HIGH_MSG("Skipping track %zu, not in tracks", ti->first); HIGH_MSG("Skipping track %zu, not in tracks", ti->first);
continue; continue;
}// ignore missing tracks }// ignore missing tracks
if (M.getLastms(ti->first) < seekPos + needsLookAhead + M.getMinKeepAway(ti->first)){ if (M.getNowms(ti->first) < seekPos + needsLookAhead + M.getMinKeepAway(ti->first)){
good = false; good = false;
break; break;
} }
if (meta.getLastms(ti->first) == M.getFirstms(ti->first)){ if (meta.getNowms(ti->first) == M.getFirstms(ti->first)){
HIGH_MSG("Skipping track %zu, last equals first", ti->first); HIGH_MSG("Skipping track %zu, last equals first", ti->first);
continue; continue;
}// ignore point-tracks }// ignore point-tracks
if (meta.getLastms(ti->first) < seekPos){ if (meta.getNowms(ti->first) < seekPos){
good = false; good = false;
break; break;
} }
@ -1099,7 +1085,7 @@ namespace Mist{
if (M.getLive() && (targetParams.count("startunix") || targetParams.count("stopunix"))){ if (M.getLive() && (targetParams.count("startunix") || targetParams.count("stopunix"))){
uint64_t unixStreamBegin = Util::epoch() - endTime()/1000; uint64_t unixStreamBegin = Util::epoch() - endTime()/1000;
size_t mainTrack = getMainSelectedTrack(); size_t mainTrack = getMainSelectedTrack();
int64_t streamAvail = M.getLastms(mainTrack); int64_t streamAvail = M.getNowms(mainTrack);
if (targetParams.count("startunix")){ if (targetParams.count("startunix")){
int64_t startUnix = atoll(targetParams["startunix"].c_str()); int64_t startUnix = atoll(targetParams["startunix"].c_str());
if (startUnix < 0){ if (startUnix < 0){
@ -1116,8 +1102,7 @@ namespace Mist{
} }
} }
if (startUnix < unixStreamBegin){ if (startUnix < unixStreamBegin){
WARN_MSG("Start time is earlier than stream begin - starting earliest possible"); WARN_MSG("Start time (%" PRId64 ") is earlier than stream begin (%" PRId64 ") - starting earliest possible", startUnix, unixStreamBegin);
WARN_MSG("%" PRId64 " < %" PRId64, startUnix, unixStreamBegin);
targetParams["start"] = "-1"; targetParams["start"] = "-1";
}else{ }else{
targetParams["start"] = JSON::Value((startUnix - unixStreamBegin) * 1000).asString(); targetParams["start"] = JSON::Value((startUnix - unixStreamBegin) * 1000).asString();
@ -1145,20 +1130,20 @@ namespace Mist{
if (targetParams.count("start") && atoll(targetParams["start"].c_str()) != 0){ if (targetParams.count("start") && atoll(targetParams["start"].c_str()) != 0){
size_t mainTrack = getMainSelectedTrack(); size_t mainTrack = getMainSelectedTrack();
int64_t startRec = atoll(targetParams["start"].c_str()); int64_t startRec = atoll(targetParams["start"].c_str());
if (startRec > M.getLastms(mainTrack)){ if (startRec > M.getNowms(mainTrack)){
if (!M.getLive()){ if (!M.getLive()){
onFail("Playback start past end of non-live source", true); onFail("Playback start past end of non-live source", true);
return; return;
} }
int64_t streamAvail = M.getLastms(mainTrack); int64_t streamAvail = M.getNowms(mainTrack);
int64_t lastUpdated = Util::getMS(); int64_t lastUpdated = Util::getMS();
INFO_MSG("Waiting for stream to reach playback starting point. Current last ms is '%" PRIu64 "'", streamAvail); INFO_MSG("Waiting for stream to reach playback starting point. Current last ms is '%" PRIu64 "'", streamAvail);
while (Util::getMS() - lastUpdated < 5000 && startRec > streamAvail && keepGoing()){ while (Util::getMS() - lastUpdated < 5000 && startRec > streamAvail && keepGoing()){
Util::sleep(500); Util::sleep(500);
if (M.getLastms(mainTrack) > streamAvail){ if (M.getNowms(mainTrack) > streamAvail){
HIGH_MSG("Waiting for stream to reach playback starting point. Current last ms is '%" PRIu64 "'", streamAvail); HIGH_MSG("Waiting for stream to reach playback starting point. Current last ms is '%" PRIu64 "'", streamAvail);
stats(); stats();
streamAvail = M.getLastms(mainTrack); streamAvail = M.getNowms(mainTrack);
lastUpdated = Util::getMS(); lastUpdated = Util::getMS();
} }
} }
@ -1309,12 +1294,12 @@ namespace Mist{
bool Output::reachedPlannedStop(){ bool Output::reachedPlannedStop(){
// If we're recording to file and reached the target position, stop // If we're recording to file and reached the target position, stop
if (isRecordingToFile && targetParams.count("recstop") && if (isRecordingToFile && targetParams.count("recstop") &&
atoll(targetParams["recstop"].c_str()) <= lastPacketTime){ atoll(targetParams["recstop"].c_str()) <= thisTime){
INFO_MSG("End of planned recording reached"); INFO_MSG("End of planned recording reached");
return true; return true;
} }
// Regardless of playback method, if we've reached the wanted stop point, stop // Regardless of playback method, if we've reached the wanted stop point, stop
if (targetParams.count("stop") && atoll(targetParams["stop"].c_str()) <= lastPacketTime){ if (targetParams.count("stop") && atoll(targetParams["stop"].c_str()) <= thisTime){
INFO_MSG("End of planned playback reached"); INFO_MSG("End of planned playback reached");
return true; return true;
} }
@ -1331,7 +1316,7 @@ namespace Mist{
return false; return false;
} }
// is this a split point? // is this a split point?
if (targetParams.count("nxt-split") && atoll(targetParams["nxt-split"].c_str()) <= lastPacketTime){ if (targetParams.count("nxt-split") && atoll(targetParams["nxt-split"].c_str()) <= thisTime){
INFO_MSG("Split point reached"); INFO_MSG("Split point reached");
return true; return true;
} }
@ -1610,10 +1595,11 @@ namespace Mist{
// slow down processing, if real time speed is wanted // slow down processing, if real time speed is wanted
if (realTime && buffer.getSyncMode()){ if (realTime && buffer.getSyncMode()){
uint8_t i = 6; uint64_t amount = thisTime - targetTime();
while (--i && thisPacket.getTime() > (((Util::bootMS() - firstTime) * 1000) / realTime + maxSkipAhead) && size_t i = (amount / 1000) + 6;
while (--i && thisTime > targetTime() &&
keepGoing()){ keepGoing()){
uint64_t amount = thisPacket.getTime() - (((Util::bootMS() - firstTime) * 1000) / realTime + maxSkipAhead); amount = thisTime - targetTime();
if (amount > 1000){amount = 1000;} if (amount > 1000){amount = 1000;}
idleTime(amount); idleTime(amount);
//Make sure we stay responsive to requests and stats while waiting //Make sure we stay responsive to requests and stats while waiting
@ -1633,23 +1619,23 @@ namespace Mist{
// wait at most double the look ahead time, plus ten seconds // wait at most double the look ahead time, plus ten seconds
uint64_t timeoutTries = (needsLookAhead / sleepTime) * 2 + (10000 / sleepTime); uint64_t timeoutTries = (needsLookAhead / sleepTime) * 2 + (10000 / sleepTime);
uint64_t needsTime = thisTime + needsLookAhead; uint64_t needsTime = thisTime + needsLookAhead;
bool firstTime = true; bool firstLookahead = true;
while (--timeoutTries && keepGoing()){ while (--timeoutTries && keepGoing()){
bool lookReady = true; bool lookReady = true;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin();
it != userSelect.end(); it++){ it != userSelect.end(); it++){
if (meta.getLastms(it->first) <= needsTime){ if (meta.getNowms(it->first) <= needsTime){
if (timeoutTries == 1){ if (timeoutTries == 1){
WARN_MSG("Track %zu: %" PRIu64 " <= %" PRIu64, it->first, WARN_MSG("Track %zu: %" PRIu64 " <= %" PRIu64, it->first,
meta.getLastms(it->first), needsTime); meta.getNowms(it->first), needsTime);
} }
lookReady = false; lookReady = false;
break; break;
} }
} }
if (lookReady){break;} if (lookReady){break;}
if (firstTime){ if (firstLookahead){
firstTime = false; firstLookahead = false;
}else{ }else{
playbackSleep(sleepTime); playbackSleep(sleepTime);
} }
@ -1760,7 +1746,7 @@ namespace Mist{
onFinish(); onFinish();
break; break;
} }
uint64_t endRec = lastPacketTime + atoll(targetParams["split"].c_str()) * 1000; uint64_t endRec = thisTime + atoll(targetParams["split"].c_str()) * 1000;
targetParams["nxt-split"] = JSON::Value(endRec).asString(); targetParams["nxt-split"] = JSON::Value(endRec).asString();
sentHeader = false; sentHeader = false;
sendHeader(); sendHeader();
@ -1987,24 +1973,34 @@ namespace Mist{
return false; return false;
} }
// if we're going to read past the end of the data page, load the next page // if we're going to read past the end of the data page...
// this only happens for VoD
if (nxt.offset >= curPage[nxt.tid].len || if (nxt.offset >= curPage[nxt.tid].len ||
(!memcmp(curPage[nxt.tid].mapped + nxt.offset, "\000\000\000\000", 4))){ (!memcmp(curPage[nxt.tid].mapped + nxt.offset, "\000\000\000\000", 4))){
if (M.getVod() && nxt.time >= M.getLastms(nxt.tid)){ // For non-live, we may have just reached the end of the track. That's normal and fine, drop it.
if (!M.getLive() && nxt.time >= M.getLastms(nxt.tid)){
dropTrack(nxt.tid, "end of VoD track reached", false); dropTrack(nxt.tid, "end of VoD track reached", false);
return false; return false;
} }
// Check if there is a next page for the timestamp we're looking for.
if (M.getPageNumberForTime(nxt.tid, nxt.time) != currentPage[nxt.tid]){ if (M.getPageNumberForTime(nxt.tid, nxt.time) != currentPage[nxt.tid]){
loadPageForKey(nxt.tid, M.getPageNumberForTime(nxt.tid, nxt.time)); loadPageForKey(nxt.tid, M.getPageNumberForTime(nxt.tid, nxt.time));
nxt.offset = 0; nxt.offset = 0;
//Only read the next time if the page load succeeded and there is a packet to read from //Only read the next time if the page load succeeded and there is a packet to read from
if (curPage[nxt.tid].mapped && curPage[nxt.tid].mapped[0] == 'D'){ if (curPage[nxt.tid].mapped && curPage[nxt.tid].mapped[0] == 'D'){
nxt.time = getDTSCTime(curPage[nxt.tid].mapped, 0); nxt.time = getDTSCTime(curPage[nxt.tid].mapped, 0);
nxt.ghostPacket = false;
}else{
nxt.ghostPacket = true;
} }
buffer.replaceFirst(nxt); buffer.replaceFirst(nxt);
return false; return false;
} }
// We're still on the same page; ghost packets should update their time and retry later
if (nxt.ghostPacket){
nxt.time = M.getNowms(nxt.tid);
buffer.replaceFirst(nxt);
return false;
}
if (nxt.offset >= curPage[nxt.tid].len){ if (nxt.offset >= curPage[nxt.tid].len){
INFO_MSG("Reading past end of page %s: %" PRIu64 " > %" PRIu64 " for time %" PRIu64 " on track %zu", curPage[nxt.tid].name.c_str(), nxt.offset, curPage[nxt.tid].len, nxt.time, nxt.tid); INFO_MSG("Reading past end of page %s: %" PRIu64 " > %" PRIu64 " for time %" PRIu64 " on track %zu", curPage[nxt.tid].name.c_str(), nxt.offset, curPage[nxt.tid].len, nxt.time, nxt.tid);
dropTrack(nxt.tid, "reading past end of page"); dropTrack(nxt.tid, "reading past end of page");
@ -2016,6 +2012,7 @@ namespace Mist{
} }
// We know this packet will be valid, pre-load it so we know its length // We know this packet will be valid, pre-load it so we know its length
DTSC::Packet preLoad(curPage[nxt.tid].mapped + nxt.offset, 0, true); DTSC::Packet preLoad(curPage[nxt.tid].mapped + nxt.offset, 0, true);
nxt.time = preLoad.getTime();
nextTime = 0; nextTime = 0;
@ -2042,6 +2039,7 @@ namespace Mist{
if (!M.getLive() && nxt.time >= M.getLastms(nxt.tid)){ if (!M.getLive() && nxt.time >= M.getLastms(nxt.tid)){
thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true); thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true);
thisIdx = nxt.tid; thisIdx = nxt.tid;
thisTime = nxt.time;
dropTrack(nxt.tid, "end of non-live track reached", false); dropTrack(nxt.tid, "end of non-live track reached", false);
return true; return true;
} }
@ -2068,6 +2066,9 @@ namespace Mist{
break;//Valid packet! break;//Valid packet!
} }
// Force valid packet if nowMs is higher than current packet time
if (M.getNowms(nxt.tid) > nxt.time){break;}
//Okay, there's no next page yet, and no next packet on this page either. //Okay, there's no next page yet, and no next packet on this page either.
//That means we're waiting for data to show up, somewhere. //That means we're waiting for data to show up, somewhere.
@ -2118,7 +2119,7 @@ namespace Mist{
} }
emptyCount = 0; // valid packet - reset empty counter emptyCount = 0; // valid packet - reset empty counter
thisIdx = nxt.tid; thisIdx = nxt.tid;
thisTime = thisPacket.getTime(); thisTime = nxt.time;
if (!userSelect[nxt.tid]){ if (!userSelect[nxt.tid]){
dropTrack(nxt.tid, "track is not alive!"); dropTrack(nxt.tid, "track is not alive!");
@ -2134,7 +2135,14 @@ namespace Mist{
// we assume the next packet is the next on this same page // we assume the next packet is the next on this same page
nxt.offset += thisPacket.getDataLen(); nxt.offset += thisPacket.getDataLen();
nxt.time = nextTime; if (!nextTime){
// If time is not known yet, insert a ghostPacket with a known safe time
nxt.time = M.getNowms(nxt.tid);
nxt.ghostPacket = true;
}else{
nxt.time = nextTime;
nxt.ghostPacket = false;
}
++nxt.partIndex; ++nxt.partIndex;
// exchange the current packet in the buffer for the next one // exchange the current packet in the buffer for the next one
@ -2425,11 +2433,11 @@ namespace Mist{
uint64_t oneTime = 0; uint64_t oneTime = 0;
uint64_t twoTime = 0; uint64_t twoTime = 0;
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){ for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){
if (M.getLastms(*it) > oneTime){oneTime = M.getLastms(*it);} if (M.getNowms(*it) > oneTime){oneTime = M.getNowms(*it);}
} }
Util::wait(2000); Util::wait(2000);
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){ for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){
if (M.getLastms(*it) > twoTime){twoTime = M.getLastms(*it);} if (M.getNowms(*it) > twoTime){twoTime = M.getNowms(*it);}
} }
if (twoTime <= oneTime+500){ if (twoTime <= oneTime+500){
disconnect(); disconnect();

View file

@ -42,6 +42,7 @@ namespace Mist{
uint64_t currentTime(); uint64_t currentTime();
uint64_t startTime(); uint64_t startTime();
uint64_t endTime(); uint64_t endTime();
uint64_t targetTime();
void setBlocking(bool blocking); void setBlocking(bool blocking);
bool selectDefaultTracks(); bool selectDefaultTracks();
bool connectToFile(std::string file, bool append = false, Socket::Connection *conn = 0); bool connectToFile(std::string file, bool append = false, Socket::Connection *conn = 0);

View file

@ -10,7 +10,15 @@
namespace Mist{ namespace Mist{
HTTPOutput::HTTPOutput(Socket::Connection &conn) : Output(conn){ HTTPOutput::HTTPOutput(Socket::Connection &conn) : Output(conn){
//Websocket related
webSock = 0; webSock = 0;
wsCmds = false;
stayLive = true;
target_rate = 0.0;
forwardTo = 0;
prevVidTrack = INVALID_TRACK_ID;
//General
idleInterval = 0; idleInterval = 0;
idleLast = 0; idleLast = 0;
if (config->getString("ip").size()){myConn.setHost(config->getString("ip"));} if (config->getString("ip").size()){myConn.setHost(config->getString("ip"));}
@ -179,16 +187,84 @@ namespace Mist{
return ""; return "";
} }
bool HTTPOutput::onFinish(){
//If we're in the middle of sending a chunked reply, finish it cleanly and get read for the next request
if (!webSock && H.sendingChunks){
H.Chunkify(0, 0, myConn);
wantRequest = true;
return true;
}
//If we're a websocket and handling commands, finish it cleanly too
if (webSock && wsCmds){
JSON::Value r;
r["type"] = "on_stop";
r["data"]["current"] = currentTime();
r["data"]["begin"] = Output::startTime();
r["data"]["end"] = Output::endTime();
webSock->sendFrame(r.toString());
parseData = false;
return false;
}
//All other cases call the parent finish handler
return Output::onFinish();
}
void HTTPOutput::sendNext(){
//If we're not in websocket mode and handling commands, we do nothing here
if (!wsCmds || !webSock){return;}
//Finish fast-forwarding if forwardTo time was reached
if (forwardTo && thisTime >= forwardTo){
forwardTo = 0;
if (target_rate == 0.0){
realTime = 1000;//set playback speed to default
firstTime = Util::bootMS() - thisTime;
maxSkipAhead = 0;//enable automatic rate control
}else{
stayLive = false;
//Set new realTime speed
realTime = 1000 / target_rate;
firstTime = Util::bootMS() - (thisTime / target_rate);
maxSkipAhead = 1;//disable automatic rate control
}
JSON::Value r;
r["type"] = "set_speed";
r["data"]["play_rate_prev"] = "fast-forward";
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
webSock->sendFrame(r.toString());
}
// Handle nice move-over to new main video track
if (prevVidTrack != INVALID_TRACK_ID && thisIdx != prevVidTrack && M.getType(thisIdx) == "video"){
if (!thisPacket.getFlag("keyframe")){
// Ignore the packet if not a keyframe
return;
}
dropTrack(prevVidTrack, "Smoothly switching to new video track", false);
prevVidTrack = INVALID_TRACK_ID;
handleWebsocketIdle();
onIdle();
sendHeader();
}
}
void HTTPOutput::requestHandler(){ void HTTPOutput::requestHandler(){
// Handle onIdle function caller, if needed // Handle onIdle function caller, if needed
if (idleInterval && (Util::bootMS() > idleLast + idleInterval)){ if (idleInterval && (Util::bootMS() > idleLast + idleInterval)){
if (wsCmds){handleWebsocketIdle();}
onIdle(); onIdle();
idleLast = Util::bootMS(); idleLast = Util::bootMS();
} }
// Handle websockets // Handle websockets
if (webSock){ if (webSock){
if (webSock->readFrame()){ if (webSock->readFrame()){
onWebsocketFrame(); if (!wsCmds || !handleWebsocketCommands()){
onWebsocketFrame();
}
idleLast = Util::bootMS(); idleLast = Util::bootMS();
return; return;
} }
@ -270,6 +346,7 @@ namespace Mist{
if (H.GetVar("audio") != ""){targetParams["audio"] = H.GetVar("audio");} if (H.GetVar("audio") != ""){targetParams["audio"] = H.GetVar("audio");}
if (H.GetVar("video") != ""){targetParams["video"] = H.GetVar("video");} if (H.GetVar("video") != ""){targetParams["video"] = H.GetVar("video");}
if (H.GetVar("meta") != ""){targetParams["meta"] = H.GetVar("meta");}
if (H.GetVar("subtitle") != ""){targetParams["subtitle"] = H.GetVar("subtitle");} if (H.GetVar("subtitle") != ""){targetParams["subtitle"] = H.GetVar("subtitle");}
if (H.GetVar("start") != ""){targetParams["start"] = H.GetVar("start");} if (H.GetVar("start") != ""){targetParams["start"] = H.GetVar("start");}
if (H.GetVar("stop") != ""){targetParams["stop"] = H.GetVar("stop");} if (H.GetVar("stop") != ""){targetParams["stop"] = H.GetVar("stop");}
@ -314,6 +391,13 @@ namespace Mist{
webSock = 0; webSock = 0;
return; return;
} }
//Generic websocket handling sets idle interval to 1s and changes name by appending "/WS"
if (wsCmds){
idleInterval = 1000;
if (capa["name"].asStringRef().find("/WS") != std::string::npos){
capa["name"] = capa["name"].asStringRef() + "/WS";
}
}
onWebsocketConnect(); onWebsocketConnect();
H.Clean(); H.Clean();
return; return;
@ -331,6 +415,361 @@ namespace Mist{
if (!sawRequest && !myConn.spool() && !isBlocking && !parseData){Util::sleep(100);} if (!sawRequest && !myConn.spool() && !isBlocking && !parseData){Util::sleep(100);}
} }
/// Handles standardized WebSocket commands.
/// Returns true if a command was executed, false otherwise.
bool HTTPOutput::handleWebsocketCommands(){
//only handle text frames
if (webSock->frameType != 1){return false;}
//Parse JSON and check command type
JSON::Value command = JSON::fromString(webSock->data, webSock->data.size());
if (!command || !command.isMember("type")){return false;}
//Seek command, for changing playback position
if (command["type"] == "seek") {
handleWebsocketSeek(command);
return true;
}
//Pause command, toggles pause state
if (command["type"] == "pause") {
parseData = !parseData;
JSON::Value r;
r["type"] = "pause";
r["paused"] = !parseData;
if (!parseData){
//Store current target time into lastPacketTime when pausing
lastPacketTime = targetTime();
}else{
//On resume, restore the timing to be where it was when pausing
firstTime = Util::bootMS() - (lastPacketTime / target_rate);
}
webSock->sendFrame(r.toString());
return true;
}
//Hold command, forces pause state on
if (command["type"] == "hold") {
if (parseData){
//Store current target time into lastPacketTime when pausing
lastPacketTime = targetTime();
}
parseData = false;
webSock->sendFrame("{\"type\":\"pause\",\"paused\":true}");
return true;
}
//Tracks command, for (re)selecting tracks
if (command["type"] == "tracks") {
if (command.isMember("audio")){
if (!command["audio"].isNull() && command["audio"] != "auto"){
targetParams["audio"] = command["audio"].asString();
}else{
targetParams.erase("audio");
}
}
if (command.isMember("video")){
if (!command["video"].isNull() && command["video"] != "auto"){
targetParams["video"] = command["video"].asString();
}else{
targetParams.erase("video");
}
}
if (command.isMember("meta")){
if (!command["meta"].isNull() && command["meta"] != "auto"){
targetParams["meta"] = command["meta"].asString();
}else{
targetParams.erase("meta");
}
}
if (command.isMember("seek_time")){
possiblyReselectTracks(command["seek_time"].asInt());
}else{
possiblyReselectTracks(currentTime());
}
return true;
}
//Fast_forward command, fast-forwards to given timestamp and resume previous speed
if (command["type"] == "fast_forward"){
if (command.isMember("ff_to")){
forwardTo = command["ff_to"].asInt();
if (forwardTo > currentTime()){
realTime = 0;
}else{
if (target_rate == 0.0){
firstTime = Util::bootMS() - forwardTo;
}else{
firstTime = Util::bootMS() - (forwardTo / target_rate);
}
forwardTo = 0;
}
}else{
JSON::Value r;
r["type"] = "warning";
r["warning"] = "Ignored fast_forward command: ff_to property missing";
webSock->sendFrame(r.toString());
}
onIdle();
return true;
}
//Set_speed command, changes playback speed
if (command["type"] == "set_speed") {
JSON::Value r;
r["type"] = "set_speed";
if (!command.isMember("play_rate")){
r["error"] = "play_rate missing";
webSock->sendFrame(r.toString());
return false;
}
double set_rate = command["play_rate"].asDouble();
if (!parseData){
parseData = true;
selectDefaultTracks();
}
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
if (set_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = set_rate;
}
if (target_rate != set_rate){
uint64_t prevTargetTime = targetTime();
target_rate = set_rate;
if (target_rate == 0.0){
realTime = 1000;//set playback speed to default
firstTime = Util::bootMS() - prevTargetTime;
maxSkipAhead = 0;//enabled automatic rate control
}else{
stayLive = false;
//Set new realTime speed
realTime = 1000 / target_rate;
firstTime = Util::bootMS() - (prevTargetTime / target_rate);
maxSkipAhead = 1;//disable automatic rate control
}
}
if (M.getLive()){r["data"]["live_point"] = stayLive;}
webSock->sendFrame(r.toString());
handleWebsocketIdle();
onIdle();
return true;
}
//Stop command, ends playback and disconnects the socket explicitly
if (command["type"] == "stop") {
Util::logExitReason(ER_CLEAN_REMOTE_CLOSE, "User requested stop");
myConn.close();
return true;
}
//Play command, sets pause state off and optionally also seeks
if (command["type"] == "play") {
parseData = true;
if (command.isMember("seek_time")){
handleWebsocketSeek(command);
}else{
if (!currentTime()){
command["seek_time"] = 0;
handleWebsocketSeek(command);
}else{
parseData = true;
selectDefaultTracks();
firstTime = Util::bootMS() - (lastPacketTime / target_rate);
}
}
return true;
}
//Unhandled commands end up here
return false;
}
void HTTPOutput::handleWebsocketIdle(){
if (!webSock){return;}
if (!parseData){return;}
//Finish fast-forwarding if forwardTo time was reached
if (forwardTo && targetTime() >= forwardTo){
forwardTo = 0;
if (target_rate == 0.0){
realTime = 1000;//set playback speed to default
firstTime = Util::bootMS() - targetTime();
maxSkipAhead = 0;//enable automatic rate control
}else{
stayLive = false;
//Set new realTime speed
realTime = 1000 / target_rate;
firstTime = Util::bootMS() - (targetTime() / target_rate);
maxSkipAhead = 1;//disable automatic rate control
}
JSON::Value r;
r["type"] = "set_speed";
r["data"]["play_rate_prev"] = "fast-forward";
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
webSock->sendFrame(r.toString());
}
JSON::Value r;
r["type"] = "on_time";
r["data"]["current"] = targetTime();
r["data"]["next"] = currentTime();
r["data"]["begin"] = Output::startTime();
r["data"]["end"] = Output::endTime();
if (realTime == 0){
r["data"]["play_rate_curr"] = "fast-forward";
}else{
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
}
uint64_t jitter = 0;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
r["data"]["tracks"].append((uint64_t)it->first);
if (jitter < M.getMinKeepAway(it->first)){jitter = M.getMinKeepAway(it->first);}
}
r["data"]["jitter"] = jitter;
if (M.getLive() && dataWaitTimeout < jitter*1.5){dataWaitTimeout = jitter*1.5;}
if (capa.isMember("maxdelay") && capa["maxdelay"].asInt() < jitter*1.5){capa["maxdelay"] = jitter*1.5;}
webSock->sendFrame(r.toString());
}
bool HTTPOutput::possiblyReselectTracks(uint64_t seekTarget){
// Remember the previous video track, if any.
std::set<size_t> prevSelTracks;
prevVidTrack = INVALID_TRACK_ID;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
prevSelTracks.insert(it->first);
if (M.getType(it->first) == "video"){
prevVidTrack = it->first;
}
}
if (!selectDefaultTracks()) {
prevVidTrack = INVALID_TRACK_ID;
handleWebsocketIdle();
onIdle();
return false;
}
if (seekTarget != currentTime()){prevVidTrack = INVALID_TRACK_ID;}
bool hasVideo = false;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
if (M.getType(it->first) == "video"){hasVideo = true;}
}
// Add the previous video track back, if we had one.
if (prevVidTrack != INVALID_TRACK_ID && !userSelect.count(prevVidTrack) && hasVideo){
userSelect[prevVidTrack].reload(streamName, prevVidTrack);
seek(seekTarget);
std::set<size_t> newSelTracks;
newSelTracks.insert(prevVidTrack);
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
if (M.getType(it->first) != "video"){
newSelTracks.insert(it->first);
}
}
if (prevSelTracks != newSelTracks){
seek(seekTarget, true);
realTime = 0;
forwardTo = seekTarget;
sendHeader();
JSON::Value r;
r["type"] = "set_speed";
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
r["data"]["play_rate_curr"] = "fast-forward";
webSock->sendFrame(r.toString());
}
}else{
prevVidTrack = INVALID_TRACK_ID;
seek(seekTarget, true);
realTime = 0;
forwardTo = seekTarget;
sendHeader();
JSON::Value r;
r["type"] = "set_speed";
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
r["data"]["play_rate_curr"] = "fast-forward";
webSock->sendFrame(r.toString());
}
handleWebsocketIdle();
onIdle();
return true;
}
bool HTTPOutput::handleWebsocketSeek(const JSON::Value& command) {
JSON::Value r;
r["type"] = "seek";
if (!command.isMember("seek_time")){
r["error"] = "seek_time missing";
webSock->sendFrame(r.toString());
return false;
}
uint64_t seek_time = command["seek_time"].asInt();
if (!parseData){
parseData = true;
selectDefaultTracks();
}
stayLive = (target_rate == 0.0) && (Output::endTime() < seek_time + 5000);
if (command["seek_time"].asStringRef() == "live"){stayLive = true;}
if (stayLive){seek_time = Output::endTime();}
if (!seek(seek_time, true)) {
r["error"] = "seek failed, continuing as-is";
webSock->sendFrame(r.toString());
return false;
}
if (M.getLive()){r["data"]["live_point"] = stayLive;}
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
if (command.isMember("ff_to") || (seek_time >= 250 && currentTime() < seek_time - 250)){
forwardTo = seek_time;
if (command.isMember("ff_to") && command["ff_to"].asInt() > forwardTo){
forwardTo = command["ff_to"].asInt();
}
if (forwardTo < currentTime()){
if (target_rate == 0.0){
firstTime = Util::bootMS() - forwardTo;
}else{
firstTime = Util::bootMS() - (forwardTo / target_rate);
}
forwardTo = 0;
}else{
realTime = 0;
r["data"]["play_rate_curr"] = "fast-forward";
}
}
handleWebsocketIdle();
onIdle();
webSock->sendFrame(r.toString());
return true;
}
/// Default HTTP handler. /// Default HTTP handler.
/// Only takes care of OPTIONS and HEAD, saving the original request, and calling respondHTTP /// Only takes care of OPTIONS and HEAD, saving the original request, and calling respondHTTP
void HTTPOutput::onHTTP(){ void HTTPOutput::onHTTP(){

View file

@ -15,18 +15,32 @@ namespace Mist{
virtual void onHTTP(); virtual void onHTTP();
virtual void respondHTTP(const HTTP::Parser & req, bool headersOnly); virtual void respondHTTP(const HTTP::Parser & req, bool headersOnly);
virtual void onIdle(){}; virtual void onIdle(){};
virtual void onWebsocketFrame(){};
virtual void onWebsocketConnect(){};
virtual void preWebsocketConnect(){};
virtual void requestHandler(); virtual void requestHandler();
virtual void preHTTP(); virtual void preHTTP();
virtual bool onFinish();
virtual void sendNext();
static bool listenMode(){return false;} static bool listenMode(){return false;}
virtual bool doesWebsockets(){return false;}
void reConnector(std::string &connector); void reConnector(std::string &connector);
std::string getHandler(); std::string getHandler();
bool parseRange(std::string header, uint64_t &byteStart, uint64_t &byteEnd); bool parseRange(std::string header, uint64_t &byteStart, uint64_t &byteEnd);
//WebSocket related
virtual bool doesWebsockets(){return false;}
virtual void onWebsocketFrame(){};
virtual void onWebsocketConnect(){};
virtual void preWebsocketConnect(){};
bool handleWebsocketCommands();
void handleWebsocketIdle();
bool handleWebsocketSeek(const JSON::Value & command);
bool possiblyReselectTracks(uint64_t seekTarget);
protected: protected:
//WebSocket related
bool wsCmds; ///< If true, implements all our standard websocket-based seek/play/etc commands
double target_rate; ///< Target playback speed rate (1.0 = normal, 0 = auto)
uint64_t forwardTo; ///< Playback position we're fast-forwarding towards
size_t prevVidTrack; ///< Previously selected main video track
bool stayLive; ///< Whether or not we're trying to stay on the live-most point, for live streams
bool responded; bool responded;
HTTP::Parser H; HTTP::Parser H;
HTTP::Websocket *webSock; HTTP::Websocket *webSock;

View file

@ -9,6 +9,8 @@
#include <mist/websocket.h> #include <mist/websocket.h>
#include <sys/stat.h> #include <sys/stat.h>
bool includeZeroMatches = false;
namespace Mist{ namespace Mist{
/// Helper function to find the protocol entry for a given port number /// Helper function to find the protocol entry for a given port number
std::string getProtocolForPort(uint16_t portNo){ std::string getProtocolForPort(uint16_t portNo){
@ -647,7 +649,7 @@ namespace Mist{
// loop over the added sources, add them to json_resp["sources"] // loop over the added sources, add them to json_resp["sources"]
for (std::set<JSON::Value, sourceCompare>::iterator it = sources.begin(); it != sources.end(); it++){ for (std::set<JSON::Value, sourceCompare>::iterator it = sources.begin(); it != sources.end(); it++){
if ((*it)["simul_tracks"].asInt() > 0){ if (includeZeroMatches || (*it)["simul_tracks"].asInt() > 0){
if (Comms::tknMode & 0x04){ if (Comms::tknMode & 0x04){
JSON::Value tmp; JSON::Value tmp;
tmp = (*it); tmp = (*it);
@ -664,6 +666,7 @@ namespace Mist{
void OutHTTP::respondHTTP(const HTTP::Parser & req, bool headersOnly){ void OutHTTP::respondHTTP(const HTTP::Parser & req, bool headersOnly){
origStreamName = streamName; origStreamName = streamName;
includeZeroMatches = req.GetVar("inclzero").size();
if (req.GetHeader("X-Mst-Path").size()){mistPath = req.GetHeader("X-Mst-Path");} if (req.GetHeader("X-Mst-Path").size()){mistPath = req.GetHeader("X-Mst-Path");}

View file

@ -74,6 +74,7 @@ namespace Mist{
capa["codecs"][0u][1u].append("+AC3"); capa["codecs"][0u][1u].append("+AC3");
capa["codecs"][0u][1u].append("+MP2"); capa["codecs"][0u][1u].append("+MP2");
capa["codecs"][0u][1u].append("+opus"); capa["codecs"][0u][1u].append("+opus");
capa["codecs"][0u][2u].append("+JSON");
capa["codecs"][1u][0u].append("rawts"); capa["codecs"][1u][0u].append("rawts");
capa["methods"][0u]["handler"] = "http"; capa["methods"][0u]["handler"] = "http";
capa["methods"][0u]["type"] = "html5/video/mpeg"; capa["methods"][0u]["type"] = "html5/video/mpeg";

View file

@ -5,6 +5,7 @@
namespace Mist{ namespace Mist{
OutJSON::OutJSON(Socket::Connection &conn) : HTTPOutput(conn){ OutJSON::OutJSON(Socket::Connection &conn) : HTTPOutput(conn){
wsCmds = true;
realTime = 0; realTime = 0;
bootMsOffset = 0; bootMsOffset = 0;
keepReselecting = false; keepReselecting = false;
@ -19,7 +20,8 @@ namespace Mist{
capa["friendly"] = "JSON over HTTP"; capa["friendly"] = "JSON over HTTP";
capa["desc"] = "Pseudostreaming in JSON format over HTTP"; capa["desc"] = "Pseudostreaming in JSON format over HTTP";
capa["url_match"] = "/$.json"; capa["url_match"] = "/$.json";
capa["codecs"][0u][0u].append("@+meta"); capa["codecs"][0u][0u].append("@meta");
capa["codecs"][0u][0u].append("subtitle");
capa["methods"][0u]["handler"] = "http"; capa["methods"][0u]["handler"] = "http";
capa["methods"][0u]["type"] = "html5/text/javascript"; capa["methods"][0u]["type"] = "html5/text/javascript";
capa["methods"][0u]["hrn"] = "JSON progressive"; capa["methods"][0u]["hrn"] = "JSON progressive";
@ -33,6 +35,9 @@ namespace Mist{
} }
void OutJSON::sendNext(){ void OutJSON::sendNext(){
//Call parent handler for generic websocket handling
HTTPOutput::sendNext();
if (keepReselecting){ if (keepReselecting){
// If we can select more tracks, do it and continue. // If we can select more tracks, do it and continue.
if (selectDefaultTracks()){ if (selectDefaultTracks()){
@ -44,11 +49,31 @@ namespace Mist{
char *dPtr; char *dPtr;
size_t dLen; size_t dLen;
thisPacket.getString("data", dPtr, dLen); thisPacket.getString("data", dPtr, dLen);
if (dLen == 0 || (dLen == 1 && dPtr[0] == ' ')){return;}
jPack["data"] = JSON::fromString(dPtr, dLen); jPack["data"] = JSON::fromString(dPtr, dLen);
jPack["time"] = thisPacket.getTime(); jPack["time"] = thisTime;
jPack["track"] = (uint64_t)thisIdx; jPack["track"] = (uint64_t)thisIdx;
}else if (M.getCodec(thisIdx) == "subtitle"){
char *dPtr;
size_t dLen;
thisPacket.getString("data", dPtr, dLen);
//Ignore blank subtitles
if (dLen == 0 || (dLen == 1 && dPtr[0] == ' ')){return;}
//Get duration, or calculate if missing
uint64_t duration = thisPacket.getInt("duration");
if (!duration){duration = dLen * 75 + 800;}
//Build JSON data to transmit
jPack["duration"] = duration;
jPack["time"] = thisTime;
jPack["track"] = (uint64_t)thisIdx;
jPack["data"] = std::string(dPtr, dLen);
}else{ }else{
jPack = thisPacket.toJSON(); jPack = thisPacket.toJSON();
jPack.removeMember("bpos");
jPack["generic_converter_used"] = true;
} }
if (dupcheck){ if (dupcheck){
if (jPack.compareExcept(lastVal, nodup)){ if (jPack.compareExcept(lastVal, nodup)){
@ -75,13 +100,14 @@ namespace Mist{
} }
void OutJSON::sendHeader(){ void OutJSON::sendHeader(){
sentHeader = true;
if (webSock){return;}
std::string method = H.method; std::string method = H.method;
H.Clean(); H.Clean();
H.SetHeader("Content-Type", "text/javascript"); H.SetHeader("Content-Type", "text/javascript");
H.protocol = "HTTP/1.0"; H.protocol = "HTTP/1.0";
H.setCORSHeaders(); H.setCORSHeaders();
H.SendResponse("200", "OK", myConn); H.SendResponse("200", "OK", myConn);
sentHeader = true;
} }
void OutJSON::onFail(const std::string &msg, bool critical){ void OutJSON::onFail(const std::string &msg, bool critical){
@ -203,9 +229,11 @@ namespace Mist{
/// Repeats last JSON packet every 5 seconds to keep stream alive. /// Repeats last JSON packet every 5 seconds to keep stream alive.
void OutJSON::onIdle(){ void OutJSON::onIdle(){
lastOutTime += (Util::bootMS() - lastSendTime); if (isPushing()){
lastSendTime = Util::bootMS(); lastOutTime += (Util::bootMS() - lastSendTime);
bufferLivePacket(lastOutTime, 0, pushTrack, lastOutData.data(), lastOutData.size(), 0, true); lastSendTime = Util::bootMS();
bufferLivePacket(lastOutTime, 0, pushTrack, lastOutData.data(), lastOutData.size(), 0, true);
}
} }
void OutJSON::onHTTP(){ void OutJSON::onHTTP(){

View file

@ -0,0 +1,112 @@
#include "output_jsonline.h"
#include <mist/defines.h>
#include <mist/http_parser.h>
#include <mist/url.h>
#include <mist/triggers.h>
#include <mist/stream.h>
namespace Mist{
OutJSONLine::OutJSONLine(Socket::Connection &conn) : Output(conn){
trkIdx = INVALID_TRACK_ID;
streamName = config->getString("streamname");
wantRequest = true;
parseData = false;
if (Triggers::shouldTrigger("PUSH_REWRITE")){
std::string payload = "jsonline://" + myConn.getBoundAddress() + ":" + config->getOption("port").asString() + "\n" + getConnectedHost() + "\n" + streamName;
std::string newStream = streamName;
Triggers::doTrigger("PUSH_REWRITE", payload, "", false, newStream);
if (!newStream.size()){
FAIL_MSG("Push from %s to URL %s rejected - PUSH_REWRITE trigger blanked the URL",
getConnectedHost().c_str(), reqUrl.c_str());
config->is_active = false;
return;
}else{
streamName = newStream;
Util::sanitizeName(streamName);
Util::setStreamName(streamName);
}
}
if (!allowPush("")){
FAIL_MSG("Pushing not allowed");
config->is_active = false;
return;
}
initialize();
trkIdx = meta.addTrack();
meta.setType(trkIdx, "meta");
meta.setCodec(trkIdx, config->getString("codec"));
meta.setID(trkIdx, 1);
offset = M.getBootMsOffset();
myConn.setBlocking(false);
INFO_MSG("%s track index is %zu", config->getString("codec").c_str(), trkIdx);
}
OutJSONLine::~OutJSONLine(){
if (trkIdx != INVALID_TRACK_ID && M){
meta.abandonTrack(trkIdx);
}
}
void OutJSONLine::init(Util::Config *cfg){
Output::init(cfg);
capa["name"] = "JSONLine";
capa["friendly"] = "JSON lines over raw TCP";
capa["desc"] = "Real time JSON line-by-line input over a raw TCP socket or standard input";
capa["deps"] = "";
capa["required"]["streamname"]["name"] = "Stream";
capa["required"]["streamname"]["help"] = "What streamname to serve. For multiple streams, add "
"this protocol multiple times using different ports.";
capa["required"]["streamname"]["type"] = "str";
capa["required"]["streamname"]["option"] = "--stream";
capa["required"]["streamname"]["short"] = "s";
cfg->addOption("codec",
JSON::fromString("{\"arg\":\"string\",\"default\":\"JSON\",\"short\":\"c\",\"long\":"
"\"codec\",\"help\":\"Codec to use for data ingest, JSON by default\"}"));
capa["optional"]["codec"]["name"] = "Codec";
capa["optional"]["codec"]["help"] = "What codec to ingest as";
capa["optional"]["codec"]["default"] = "JSON";
capa["optional"]["codec"]["type"] = "str";
capa["optional"]["codec"]["option"] = "--codec";
capa["optional"]["codec"]["short"] = "c";
capa["codecs"][0u][0u].append("JSON");
cfg->addConnectorOptions(3456, capa);
config = cfg;
}
void OutJSONLine::sendNext(){
}
bool OutJSONLine::listenMode(){return true;}
void OutJSONLine::requestHandler(){
if (myConn.spool()){
while (myConn.Received().size()){
dPtr.append(myConn.Received().get());
myConn.Received().get().clear();
if (dPtr.size() && dPtr[dPtr.size() - 1] == '\n'){
thisTime = Util::bootMS() - offset;
thisIdx = trkIdx;
thisPacket.genericFill(thisTime, 0, 1, dPtr, dPtr.size(), 0, true);
bufferLivePacket(thisPacket);
dPtr.truncate(0);
}
}
}else{
meta.setNowms(trkIdx, Util::bootMS() - offset);
Util::sleep(10);
}
}
std::string OutJSONLine::getStatsName(){
if (!parseData){
return "INPUT:" + capa["name"].asStringRef();
}else{
return Output::getStatsName();
}
}
bool OutJSONLine::isReadyForPlay(){return true;}
}// namespace Mist

View file

@ -0,0 +1,26 @@
#include "output.h"
namespace Mist{
class OutJSONLine : public Output{
public:
OutJSONLine(Socket::Connection &conn);
~OutJSONLine();
static void init(Util::Config *cfg);
void sendNext();
static bool listenMode();
bool isReadyForPlay();
void requestHandler();
private:
std::string getStatsName();
Util::ResizeablePointer dPtr;
size_t trkIdx;
uint64_t offset;
protected:
inline virtual bool keepGoing(){
return config->is_active && (!listenMode() || myConn);
}
};
}// namespace Mist
typedef Mist::OutJSONLine mistOut;

View file

@ -106,14 +106,11 @@ namespace Mist{
} }
OutMP4::OutMP4(Socket::Connection &conn) : HTTPOutput(conn){ OutMP4::OutMP4(Socket::Connection &conn) : HTTPOutput(conn){
prevVidTrack = INVALID_TRACK_ID; wsCmds = true;
nextHeaderTime = 0xffffffffffffffffull; nextHeaderTime = 0xffffffffffffffffull;
startTime = 0; startTime = 0;
endTime = 0xffffffffffffffffull; endTime = 0xffffffffffffffffull;
realBaseOffset = 1; realBaseOffset = 1;
stayLive = true;
target_rate = 0.0;
forwardTo = 0;
} }
OutMP4::~OutMP4(){} OutMP4::~OutMP4(){}
@ -1197,12 +1194,8 @@ namespace Mist{
} }
void OutMP4::sendNext(){ void OutMP4::sendNext(){
//Call parent handler for generic websocket handling
if (!thisPacket.getData()) { HTTPOutput::sendNext();
FAIL_MSG("`thisPacket.getData()` is invalid.");
return;
}
// Obtain a pointer to the data of this packet // Obtain a pointer to the data of this packet
char *dataPointer = 0; char *dataPointer = 0;
size_t len = 0; size_t len = 0;
@ -1210,68 +1203,6 @@ namespace Mist{
// WebSockets send each packet directly. The packet is constructed in `appendSinglePacketMoof()`. // WebSockets send each packet directly. The packet is constructed in `appendSinglePacketMoof()`.
if (webSock) { if (webSock) {
if (forwardTo && currentTime() >= forwardTo){
forwardTo = 0;
if (target_rate == 0.0){
realTime = 1000;//set playback speed to default
firstTime = Util::bootMS() - currentTime();
maxSkipAhead = 0;//enabled automatic rate control
}else{
stayLive = false;
//Set new realTime speed
realTime = 1000 / target_rate;
firstTime = Util::bootMS() - (currentTime() / target_rate);
maxSkipAhead = 1;//disable automatic rate control
}
JSON::Value r;
r["type"] = "set_speed";
r["data"]["play_rate_prev"] = "fast-forward";
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
webSock->sendFrame(r.toString());
}
// Handle nice move-over to new track ID
if (prevVidTrack != INVALID_TRACK_ID && thisIdx != prevVidTrack && M.getType(thisIdx) == "video"){
if (!thisPacket.getFlag("keyframe")){
// Ignore the packet if not a keyframe
return;
}
dropTrack(prevVidTrack, "Smoothly switching to new video track", false);
prevVidTrack = INVALID_TRACK_ID;
onIdle();
sendHeader();
/*
MP4::AVCC avccbox;
avccbox.setPayload(M.getInit(thisIdx));
std::string bs = avccbox.asAnnexB();
static Util::ResizeablePointer initBuf;
initBuf.assign(0,0);
initBuf.allocate(bs.size());
char * ib = initBuf;
initBuf.append(0, nalu::fromAnnexB(bs.data(), bs.size(), ib));
webBuf.truncate(0);
appendSinglePacketMoof(webBuf, bs.size());
char mdatHeader[8] ={0x00, 0x00, 0x00, 0x00, 'm', 'd', 'a', 't'};
Bit::htobl(mdatHeader, 8 + len); //8 bytes for the header + length of data.
webBuf.append(mdatHeader, 8);
webBuf.append(dataPointer, len);
webBuf.append(initBuf, initBuf.size());
webSock->sendFrame(webBuf, webBuf.size(), 2);
return;
*/
}
webBuf.truncate(0); webBuf.truncate(0);
appendSinglePacketMoof(webBuf); appendSinglePacketMoof(webBuf);
@ -1415,9 +1346,6 @@ namespace Mist{
} }
webSock->sendFrame(headerData, headerData.size(), 2); webSock->sendFrame(headerData, headerData.size(), 2);
std::ofstream bleh("/tmp/bleh.mp4");
bleh.write(headerData, headerData.size());
bleh.close();
sentHeader = true; sentHeader = true;
return; return;
} }
@ -1448,24 +1376,15 @@ namespace Mist{
} }
void OutMP4::onWebsocketConnect() { void OutMP4::onWebsocketConnect() {
capa["name"] = "MP4/WS";
capa["maxdelay"] = 5000; capa["maxdelay"] = 5000;
fragSeqNum = 0; fragSeqNum = 0;
idleInterval = 1000;
maxSkipAhead = 0; maxSkipAhead = 0;
if (M.getLive()){dataWaitTimeout = 450;} if (M.getLive()){dataWaitTimeout = 450;}
} }
void OutMP4::onWebsocketFrame() { void OutMP4::onWebsocketFrame() {
JSON::Value command = JSON::fromString(webSock->data, webSock->data.size()); JSON::Value command = JSON::fromString(webSock->data, webSock->data.size());
if (!command.isMember("type")) { if (!command.isMember("type")) {return;}
JSON::Value r;
r["type"] = "error";
r["data"] = "type field missing from command";
webSock->sendFrame(r.toString());
return;
}
if (command["type"] == "request_codec_data") { if (command["type"] == "request_codec_data") {
//If no supported codecs are passed, assume autodetected capabilities //If no supported codecs are passed, assume autodetected capabilities
@ -1490,119 +1409,10 @@ namespace Mist{
selectDefaultTracks(); selectDefaultTracks();
initialSeek(); initialSeek();
sendHeader(); sendHeader();
}else if (command["type"] == "seek") {
handleWebsocketSeek(command);
}else if (command["type"] == "pause") {
parseData = !parseData;
JSON::Value r;
r["type"] = "pause";
r["paused"] = !parseData;
//Make sure we reset our timing code, too
if (parseData){
firstTime = Util::bootMS() - (currentTime() / target_rate);
}
webSock->sendFrame(r.toString());
}else if (command["type"] == "hold") {
parseData = false;
webSock->sendFrame("{\"type\":\"pause\",\"paused\":true}");
}else if (command["type"] == "tracks") {
if (command.isMember("audio")){
if (!command["audio"].isNull() && command["audio"] != "auto"){
targetParams["audio"] = command["audio"].asString();
}else{
targetParams.erase("audio");
}
}
if (command.isMember("video")){
if (!command["video"].isNull() && command["video"] != "auto"){
targetParams["video"] = command["video"].asString();
}else{
targetParams.erase("video");
}
}
if (command.isMember("seek_time")){
possiblyReselectTracks(command["seek_time"].asInt());
}else{
possiblyReselectTracks(currentTime());
}
return; return;
}else if (command["type"] == "set_speed") {
handleWebsocketSetSpeed(command);
}else if (command["type"] == "stop") {
Util::logExitReason(ER_CLEAN_REMOTE_CLOSE, "User requested stop");
myConn.close();
}else if (command["type"] == "play") {
parseData = true;
if (command.isMember("seek_time")){handleWebsocketSeek(command);}
} }
} }
bool OutMP4::possiblyReselectTracks(uint64_t seekTarget){
// Remember the previous video track, if any.
std::set<size_t> prevSelTracks;
prevVidTrack = INVALID_TRACK_ID;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
prevSelTracks.insert(it->first);
if (M.getType(it->first) == "video"){
prevVidTrack = it->first;
}
}
if (!selectDefaultTracks()) {
prevVidTrack = INVALID_TRACK_ID;
onIdle();
return false;
}
if (seekTarget != currentTime()){prevVidTrack = INVALID_TRACK_ID;}
bool hasVideo = false;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
if (M.getType(it->first) == "video"){hasVideo = true;}
}
// Add the previous video track back, if we had one.
if (prevVidTrack != INVALID_TRACK_ID && !userSelect.count(prevVidTrack) && hasVideo){
userSelect[prevVidTrack].reload(streamName, prevVidTrack);
seek(seekTarget);
std::set<size_t> newSelTracks;
newSelTracks.insert(prevVidTrack);
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
if (M.getType(it->first) != "video"){
newSelTracks.insert(it->first);
}
}
if (prevSelTracks != newSelTracks){
seek(seekTarget, true);
realTime = 0;
forwardTo = seekTarget;
sendHeader();
JSON::Value r;
r["type"] = "set_speed";
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
r["data"]["play_rate_curr"] = "fast-forward";
webSock->sendFrame(r.toString());
}
}else{
prevVidTrack = INVALID_TRACK_ID;
seek(seekTarget, true);
realTime = 0;
forwardTo = seekTarget;
sendHeader();
JSON::Value r;
r["type"] = "set_speed";
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
r["data"]["play_rate_curr"] = "fast-forward";
webSock->sendFrame(r.toString());
}
onIdle();
return true;
}
void OutMP4::sendWebsocketCodecData(const std::string& type) { void OutMP4::sendWebsocketCodecData(const std::string& type) {
JSON::Value r; JSON::Value r;
r["type"] = type; r["type"] = type;
@ -1627,136 +1437,6 @@ namespace Mist{
webSock->sendFrame(r.toString()); webSock->sendFrame(r.toString());
} }
bool OutMP4::handleWebsocketSeek(JSON::Value& command) {
JSON::Value r;
r["type"] = "seek";
if (!command.isMember("seek_time")){
r["error"] = "seek_time missing";
webSock->sendFrame(r.toString());
return false;
}
uint64_t seek_time = command["seek_time"].asInt();
if (!parseData){
parseData = true;
selectDefaultTracks();
}
stayLive = (target_rate == 0.0) && (Output::endTime() < seek_time + 5000);
if (command["seek_time"].asStringRef() == "live"){stayLive = true;}
if (stayLive){seek_time = Output::endTime();}
if (!seek(seek_time, true)) {
r["error"] = "seek failed, continuing as-is";
webSock->sendFrame(r.toString());
return false;
}
if (M.getLive()){r["data"]["live_point"] = stayLive;}
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
if (seek_time >= 250 && currentTime() < seek_time - 250){
forwardTo = seek_time;
realTime = 0;
r["data"]["play_rate_curr"] = "fast-forward";
}
onIdle();
webSock->sendFrame(r.toString());
return true;
}
bool OutMP4::handleWebsocketSetSpeed(JSON::Value& command) {
JSON::Value r;
r["type"] = "set_speed";
if (!command.isMember("play_rate")){
r["error"] = "play_rate missing";
webSock->sendFrame(r.toString());
return false;
}
double set_rate = command["play_rate"].asDouble();
if (!parseData){
parseData = true;
selectDefaultTracks();
}
if (target_rate == 0.0){
r["data"]["play_rate_prev"] = "auto";
}else{
r["data"]["play_rate_prev"] = target_rate;
}
if (set_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = set_rate;
}
if (target_rate != set_rate){
target_rate = set_rate;
if (target_rate == 0.0){
realTime = 1000;//set playback speed to default
firstTime = Util::bootMS() - currentTime();
maxSkipAhead = 0;//enabled automatic rate control
}else{
stayLive = false;
//Set new realTime speed
realTime = 1000 / target_rate;
firstTime = Util::bootMS() - (currentTime() / target_rate);
maxSkipAhead = 1;//disable automatic rate control
}
}
if (M.getLive()){r["data"]["live_point"] = stayLive;}
webSock->sendFrame(r.toString());
onIdle();
return true;
}
void OutMP4::onIdle() {
if (!webSock){return;}
if (!parseData){return;}
JSON::Value r;
r["type"] = "on_time";
r["data"]["current"] = currentTime();
r["data"]["begin"] = Output::startTime();
r["data"]["end"] = Output::endTime();
if (realTime == 0){
r["data"]["play_rate_curr"] = "fast-forward";
}else{
if (target_rate == 0.0){
r["data"]["play_rate_curr"] = "auto";
}else{
r["data"]["play_rate_curr"] = target_rate;
}
}
uint64_t jitter = 0;
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
r["data"]["tracks"].append((uint64_t)it->first);
if (jitter < M.getMinKeepAway(it->first)){jitter = M.getMinKeepAway(it->first);}
}
r["data"]["jitter"] = jitter;
if (M.getLive() && dataWaitTimeout < jitter*1.5){dataWaitTimeout = jitter*1.5;}
if (capa["maxdelay"].asInt() < jitter*1.5){capa["maxdelay"] = jitter*1.5;}
webSock->sendFrame(r.toString());
}
bool OutMP4::onFinish() {
if (!webSock){
H.Chunkify(0, 0, myConn);
wantRequest = true;
return true;
}
JSON::Value r;
r["type"] = "on_stop";
r["data"]["current"] = currentTime();
r["data"]["begin"] = Output::startTime();
r["data"]["end"] = Output::endTime();
webSock->sendFrame(r.toString());
parseData = false;
return false;
}
void OutMP4::dropTrack(size_t trackId, const std::string &reason, bool probablyBad){ void OutMP4::dropTrack(size_t trackId, const std::string &reason, bool probablyBad){
if (webSock && (reason == "EOP: data wait timeout" || reason == "disappeared from metadata") && possiblyReselectTracks(currentTime())){ if (webSock && (reason == "EOP: data wait timeout" || reason == "disappeared from metadata") && possiblyReselectTracks(currentTime())){
return; return;

View file

@ -106,16 +106,10 @@ namespace Mist{
bool doesWebsockets() { return true; } bool doesWebsockets() { return true; }
void onWebsocketConnect(); void onWebsocketConnect();
void onWebsocketFrame(); void onWebsocketFrame();
void onIdle();
virtual bool onFinish();
virtual void dropTrack(size_t trackId, const std::string &reason, bool probablyBad = true); virtual void dropTrack(size_t trackId, const std::string &reason, bool probablyBad = true);
protected: protected:
bool possiblyReselectTracks(uint64_t seekTarget);
void sendWebsocketCodecData(const std::string& type); void sendWebsocketCodecData(const std::string& type);
bool handleWebsocketSeek(JSON::Value& command); bool handleWebsocketSeek(JSON::Value& command);
bool handleWebsocketSetSpeed(JSON::Value& command);
bool stayLive;
double target_rate; ///< Target playback speed rate (1.0 = normal, 0 = auto)
uint64_t fileSize; uint64_t fileSize;
uint64_t byteStart; uint64_t byteStart;
@ -123,11 +117,9 @@ namespace Mist{
int64_t leftOver; int64_t leftOver;
uint64_t currPos; uint64_t currPos;
uint64_t seekPoint; uint64_t seekPoint;
uint64_t forwardTo;
uint64_t nextHeaderTime; uint64_t nextHeaderTime;
uint64_t headerSize; uint64_t headerSize;
size_t prevVidTrack;
// variables for standard MP4 // variables for standard MP4
std::set<keyPart> sortSet; // needed for unfragmented MP4, remembers the order of keyparts std::set<keyPart> sortSet; // needed for unfragmented MP4, remembers the order of keyparts

View file

@ -1707,7 +1707,7 @@ namespace Mist{
size_t reTrack = next.cs_id * 3 + (F.data[0] == 0x09 ? 1 : (F.data[0] == 0x08 ? 2 : 3)); size_t reTrack = next.cs_id * 3 + (F.data[0] == 0x09 ? 1 : (F.data[0] == 0x08 ? 2 : 3));
if (!reTrackToID.count(reTrack)){reTrackToID[reTrack] = INVALID_TRACK_ID;} if (!reTrackToID.count(reTrack)){reTrackToID[reTrack] = INVALID_TRACK_ID;}
F.toMeta(meta, *amf_storage, reTrackToID[reTrack], targetParams); F.toMeta(meta, *amf_storage, reTrackToID[reTrack], targetParams);
if (F.getDataLen() && !(F.needsInitData() && F.isInitData())){ if ((F.getDataLen() || (amf_storage && amf_storage->hasContent())) && !(F.needsInitData() && F.isInitData())){
uint64_t tagTime = next.timestamp; uint64_t tagTime = next.timestamp;
uint64_t timeOffset = 0; uint64_t timeOffset = 0;
if (targetParams.count("timeoffset")){ if (targetParams.count("timeoffset")){
@ -1778,8 +1778,22 @@ namespace Mist{
} }
} }
ltt = tagTime; ltt = tagTime;
// bufferLivePacket(thisPacket); if (ltt){
bufferLivePacket(tagTime, F.offset(), idx, F.getData(), F.getDataLen(), 0, F.isKeyframe); for (std::map<size_t, uint64_t>::iterator it = lastTagTime.begin(); it != lastTagTime.end(); ++it){
if (it->second == reTrack){continue;}
size_t iIdx = reTrackToID[it->second];
if (it->first < ltt){
meta.setNowms(iIdx, ltt-1);
it->second = ltt-1;
}
}
}
if (F.data[0] == 0x12 && amf_storage){
std::string mData = amf_storage->toJSON().toString();
bufferLivePacket(tagTime, F.offset(), idx, mData.c_str(), mData.size(), 0, true);
}else{
bufferLivePacket(tagTime, F.offset(), idx, F.getData(), F.getDataLen(), 0, F.isKeyframe);
}
if (!meta){config->is_active = false;} if (!meta){config->is_active = false;}
} }
break; break;

View file

@ -30,6 +30,12 @@ namespace Mist{
} }
void OutSRT::sendNext(){ void OutSRT::sendNext(){
// Reached the end we wanted? Stop here.
if (filter_to > 0 && thisTime > filter_to && filter_to > filter_from){
config->is_active = false;
return;
}
char *dataPointer = 0; char *dataPointer = 0;
size_t len = 0; size_t len = 0;
thisPacket.getString("data", dataPointer, len); thisPacket.getString("data", dataPointer, len);
@ -37,35 +43,22 @@ namespace Mist{
if (len == 0 || (len == 1 && dataPointer[0] == ' ')){return;} if (len == 0 || (len == 1 && dataPointer[0] == ' ')){return;}
std::stringstream tmp; std::stringstream tmp;
if (!webVTT){tmp << lastNum++ << std::endl;} if (!webVTT){tmp << lastNum++ << std::endl;}
uint64_t time = thisPacket.getTime();
// filter subtitle in specific timespan
if (filter_from > 0 && time < filter_from){
index++; // when using seek, the index is lost.
seek(filter_from);
return;
}
if (filter_to > 0 && time > filter_to && filter_to > filter_from){
config->is_active = false;
return;
}
char tmpBuf[50]; char tmpBuf[50];
size_t tmpLen = size_t tmpLen =
sprintf(tmpBuf, "%.2" PRIu64 ":%.2" PRIu64 ":%.2" PRIu64 ".%.3" PRIu64, (time / 3600000), sprintf(tmpBuf, "%.2" PRIu64 ":%.2" PRIu64 ":%.2" PRIu64 ".%.3" PRIu64, (thisTime / 3600000),
((time % 3600000) / 60000), (((time % 3600000) % 60000) / 1000), time % 1000); ((thisTime % 3600000) / 60000), (((thisTime % 3600000) % 60000) / 1000), thisTime % 1000);
tmp.write(tmpBuf, tmpLen); tmp.write(tmpBuf, tmpLen);
tmp << " --> "; tmp << " --> ";
time += thisPacket.getInt("duration"); uint64_t time = thisTime + thisPacket.getInt("duration");
if (time == thisPacket.getTime()){time += len * 75 + 800;} if (time == thisTime){time += len * 75 + 800;}
tmpLen = sprintf(tmpBuf, "%.2" PRIu64 ":%.2" PRIu64 ":%.2" PRIu64 ".%.3" PRIu64, (time / 3600000), tmpLen = sprintf(tmpBuf, "%.2" PRIu64 ":%.2" PRIu64 ":%.2" PRIu64 ".%.3" PRIu64, (time / 3600000),
((time % 3600000) / 60000), (((time % 3600000) % 60000) / 1000), time % 1000); ((time % 3600000) / 60000), (((time % 3600000) % 60000) / 1000), time % 1000);
tmp.write(tmpBuf, tmpLen); tmp.write(tmpBuf, tmpLen);
tmp << std::endl; tmp << std::endl;
myConn.SendNow(tmp.str()); myConn.SendNow(tmp.str());
// prevent double newlines // prevent extra newlines
if (dataPointer[len - 1] == '\n'){--dataPointer;} while (len && dataPointer[len - 1] == '\n'){--len;}
myConn.SendNow(dataPointer, len); myConn.SendNow(dataPointer, len);
myConn.SendNow("\n\n"); myConn.SendNow("\n\n");
} }
@ -82,7 +75,7 @@ namespace Mist{
void OutSRT::onHTTP(){ void OutSRT::onHTTP(){
std::string method = H.method; std::string method = H.method;
webVTT = (H.url.find(".vtt") != std::string::npos) || (H.url.find(".webvtt") != std::string::npos); webVTT = (H.url.find(".vtt") != std::string::npos) || (H.url.find(".webvtt") != std::string::npos);
if (H.GetVar("track") != ""){ if (H.GetVar("track").size()){
size_t tid = atoll(H.GetVar("track").c_str()); size_t tid = atoll(H.GetVar("track").c_str());
if (M.getValidTracks().count(tid)){ if (M.getValidTracks().count(tid)){
userSelect.clear(); userSelect.clear();
@ -94,7 +87,10 @@ namespace Mist{
filter_to = 0; filter_to = 0;
index = 0; index = 0;
if (H.GetVar("from") != ""){filter_from = JSON::Value(H.GetVar("from")).asInt();} if (H.GetVar("from") != ""){
filter_from = JSON::Value(H.GetVar("from")).asInt();
seek(filter_from);
}
if (H.GetVar("to") != ""){filter_to = JSON::Value(H.GetVar("to")).asInt();} if (H.GetVar("to") != ""){filter_to = JSON::Value(H.GetVar("to")).asInt();}
if (filter_to){realTime = 0;} if (filter_to){realTime = 0;}

View file

@ -180,6 +180,7 @@ namespace Mist{
capa["codecs"][0u][1u].append("+AC3"); capa["codecs"][0u][1u].append("+AC3");
capa["codecs"][0u][1u].append("+MP2"); capa["codecs"][0u][1u].append("+MP2");
capa["codecs"][0u][1u].append("+opus"); capa["codecs"][0u][1u].append("+opus");
capa["codecs"][0u][2u].append("+JSON");
capa["codecs"][1u][0u].append("rawts"); capa["codecs"][1u][0u].append("rawts");
cfg->addConnectorOptions(8888, capa); cfg->addConnectorOptions(8888, capa);
config = cfg; config = cfg;
@ -194,6 +195,25 @@ namespace Mist{
opt["arg_num"] = 1; opt["arg_num"] = 1;
opt["help"] = "Target tsudp:// or tsrtp:// or tstcp:// URL to push out towards."; opt["help"] = "Target tsudp:// or tsrtp:// or tstcp:// URL to push out towards.";
cfg->addOption("target", opt); cfg->addOption("target", opt);
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
opt.null();
opt["long"] = "datatrack";
opt["short"] = "D";
opt["arg"] = "string";
opt["default"] = "";
opt["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", opt);
} }
void OutTS::initialSeek(){ void OutTS::initialSeek(){
@ -290,6 +310,9 @@ namespace Mist{
onFinish(); onFinish();
return; return;
} }
if (config->getString("datatrack") == "json"){
tsIn.setRawDataParser(TS::JSON);
}
} }
// we now know we probably have a packet ready at the next 188 bytes // we now know we probably have a packet ready at the next 188 bytes
// remove from buffer and insert into TS input // remove from buffer and insert into TS input

View file

@ -226,8 +226,14 @@ namespace Mist{
fillPacket(dataPointer, dataLen, firstPack, video, keyframe, pkgPid, contPkg); fillPacket(dataPointer, dataLen, firstPack, video, keyframe, pkgPid, contPkg);
}else if (type == "meta"){ }else if (type == "meta"){
long unsigned int tempLen = dataLen; long unsigned int tempLen = dataLen;
if (codec == "JSON"){tempLen += 2;}
bs = TS::Packet::getPESMetaLeadIn(tempLen, packTime, M.getBps(thisIdx)); bs = TS::Packet::getPESMetaLeadIn(tempLen, packTime, M.getBps(thisIdx));
fillPacket(bs.data(), bs.size(), firstPack, video, keyframe, pkgPid, contPkg); fillPacket(bs.data(), bs.size(), firstPack, video, keyframe, pkgPid, contPkg);
if (codec == "JSON"){
char dLen[2];
Bit::htobs(dLen, dataLen);
fillPacket(dLen, 2, firstPack, video, keyframe, pkgPid, contPkg);
}
fillPacket(dataPointer, dataLen, firstPack, video, keyframe, pkgPid, contPkg); fillPacket(dataPointer, dataLen, firstPack, video, keyframe, pkgPid, contPkg);
} }
if (packData.getBytesFree() < 184){ if (packData.getBytesFree() < 184){

View file

@ -181,6 +181,9 @@ namespace Mist{
onFinish(); onFinish();
return; return;
} }
if (config->getString("datatrack") == "json"){
tsIn.setRawDataParser(TS::JSON);
}
parseData = false; parseData = false;
wantRequest = true; wantRequest = true;
@ -225,6 +228,7 @@ namespace Mist{
capa["codecs"][0u][1u].append("+AC3"); capa["codecs"][0u][1u].append("+AC3");
capa["codecs"][0u][1u].append("+MP2"); capa["codecs"][0u][1u].append("+MP2");
capa["codecs"][0u][1u].append("+opus"); capa["codecs"][0u][1u].append("+opus");
capa["codecs"][0u][2u].append("+JSON");
capa["codecs"][1u][0u].append("rawts"); capa["codecs"][1u][0u].append("rawts");
capa["optional"]["profile"]["name"] = "RIST profile"; capa["optional"]["profile"]["name"] = "RIST profile";
@ -283,6 +287,25 @@ namespace Mist{
opt["arg_num"] = 1; opt["arg_num"] = 1;
opt["help"] = "Target rist:// URL to push out towards."; opt["help"] = "Target rist:// URL to push out towards.";
cfg->addOption("target", opt); cfg->addOption("target", opt);
opt.null();
opt["long"] = "datatrack";
opt["short"] = "D";
opt["arg"] = "string";
opt["default"] = "";
opt["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", opt);
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
} }
// Buffers TS packets and sends after 7 are buffered. // Buffers TS packets and sends after 7 are buffered.

View file

@ -119,6 +119,9 @@ namespace Mist{
onFinish(); onFinish();
return; return;
} }
if (config->getString("datatrack") == "json"){
tsIn.setRawDataParser(TS::JSON);
}
parseData = false; parseData = false;
wantRequest = true; wantRequest = true;
} }
@ -209,6 +212,7 @@ namespace Mist{
capa["codecs"][0u][1u].append("AC3"); capa["codecs"][0u][1u].append("AC3");
capa["codecs"][0u][1u].append("MP2"); capa["codecs"][0u][1u].append("MP2");
capa["codecs"][0u][1u].append("opus"); capa["codecs"][0u][1u].append("opus");
capa["codecs"][0u][2u].append("JSON");
capa["codecs"][1u][0u].append("rawts"); capa["codecs"][1u][0u].append("rawts");
cfg->addConnectorOptions(8889, capa); cfg->addConnectorOptions(8889, capa);
config = cfg; config = cfg;
@ -279,6 +283,25 @@ namespace Mist{
opt["arg_num"] = 1; opt["arg_num"] = 1;
opt["help"] = "Target srt:// URL to push out towards."; opt["help"] = "Target srt:// URL to push out towards.";
cfg->addOption("target", opt); cfg->addOption("target", opt);
capa["optional"]["datatrack"]["name"] = "MPEG Data track parser";
capa["optional"]["datatrack"]["help"] = "Which parser to use for data tracks";
capa["optional"]["datatrack"]["type"] = "select";
capa["optional"]["datatrack"]["option"] = "--datatrack";
capa["optional"]["datatrack"]["short"] = "D";
capa["optional"]["datatrack"]["default"] = "";
capa["optional"]["datatrack"]["select"][0u][0u] = "";
capa["optional"]["datatrack"]["select"][0u][1u] = "None / disabled";
capa["optional"]["datatrack"]["select"][1u][0u] = "json";
capa["optional"]["datatrack"]["select"][1u][1u] = "2b size-prepended JSON";
opt.null();
opt["long"] = "datatrack";
opt["short"] = "D";
opt["arg"] = "string";
opt["default"] = "";
opt["help"] = "Which parser to use for data tracks";
config->addOption("datatrack", opt);
} }
// Buffers TS packets and sends after 7 are buffered. // Buffers TS packets and sends after 7 are buffered.