HLS CMAF updated to use hls_support library for (LL)HLS manifest generation.
- also removed duplicate CMAF library methods
This commit is contained in:
parent
c54690d346
commit
e9d5920a80
4 changed files with 360 additions and 419 deletions
130
lib/cmaf.cpp
130
lib/cmaf.cpp
|
@ -13,25 +13,6 @@ namespace CMAF{
|
||||||
return payloadSize;
|
return payloadSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t trackHeaderSize(const DTSC::Meta &M, size_t track){
|
|
||||||
// EDTS Box needed? + 36
|
|
||||||
size_t res = 36 + 8 + 108 + 8 + 92 + 8 + 32 + 33 + 44 + 8 + 20 + 16 + 16 + 16 + 40;
|
|
||||||
|
|
||||||
res += M.getType(track).size();
|
|
||||||
|
|
||||||
// Type-specific boxes
|
|
||||||
std::string tType = M.getType(track);
|
|
||||||
if (tType == "video"){res += 20 + 16 + 86 + 16 + 8 + M.getInit(track).size() + 20;}//20 for btrt box
|
|
||||||
if (tType == "audio"){
|
|
||||||
res += 16 + 16 + 36 + 35 + (M.getInit(track).size() ? 2 + M.getInit(track).size() : 0) + 20;//20 for btrt box
|
|
||||||
}
|
|
||||||
if (tType == "meta"){res += 12 + 16 + 64;}
|
|
||||||
|
|
||||||
if (M.getVod()){res += 16;}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t simplifiedTrackId(const DTSC::Meta & M, size_t idx) {
|
size_t simplifiedTrackId(const DTSC::Meta & M, size_t idx) {
|
||||||
std::string type = M.getType(idx);
|
std::string type = M.getType(idx);
|
||||||
if (type == "video") {return 1;}
|
if (type == "video") {return 1;}
|
||||||
|
@ -175,7 +156,7 @@ namespace CMAF{
|
||||||
((i + 1 < fragments.getEndValid()) ? fragments.getFirstKey(i + 1) : keys.getEndValid());
|
((i + 1 < fragments.getEndValid()) ? fragments.getFirstKey(i + 1) : keys.getEndValid());
|
||||||
|
|
||||||
MP4::sidxReference refItem;
|
MP4::sidxReference refItem;
|
||||||
refItem.referencedSize = payloadSize(M, track, keys.getTime(firstKey), keys.getTime(endKey)) + fragmentHeaderSize(M, track, i) + 8;
|
refItem.referencedSize = payloadSize(M, track, keys.getTime(firstKey), keys.getTime(endKey)) + keyHeaderSize(M, track, i) + 8;
|
||||||
refItem.subSegmentDuration =
|
refItem.subSegmentDuration =
|
||||||
(endKey == keys.getEndValid() ? M.getLastms(track) : keys.getTime(endKey)) - keys.getTime(firstKey);
|
(endKey == keys.getEndValid() ? M.getLastms(track) : keys.getTime(endKey)) - keys.getTime(firstKey);
|
||||||
refItem.sapStart = true;
|
refItem.sapStart = true;
|
||||||
|
@ -199,7 +180,7 @@ namespace CMAF{
|
||||||
bool operator<(const sortPart &rhs) const{return time < rhs.time;}
|
bool operator<(const sortPart &rhs) const{return time < rhs.time;}
|
||||||
};
|
};
|
||||||
|
|
||||||
size_t fragmentHeaderSize(const DTSC::Meta &M, size_t track, size_t fragment){
|
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, size_t fragment){
|
||||||
uint64_t tmpRes = 8 + 16 + 32 + 20;
|
uint64_t tmpRes = 8 + 16 + 32 + 20;
|
||||||
|
|
||||||
DTSC::Fragments fragments(M.fragments(track));
|
DTSC::Fragments fragments(M.fragments(track));
|
||||||
|
@ -217,113 +198,6 @@ namespace CMAF{
|
||||||
return tmpRes;
|
return tmpRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string fragmentHeader(const DTSC::Meta &M, size_t track, size_t fragment, bool simplifyTrackIds, bool UTCTime){
|
|
||||||
|
|
||||||
DTSC::Fragments fragments(M.fragments(track));
|
|
||||||
DTSC::Keys keys(M.keys(track));
|
|
||||||
DTSC::Parts parts(M.parts(track));
|
|
||||||
|
|
||||||
size_t firstKey = fragments.getFirstKey(fragment);
|
|
||||||
size_t endKey = keys.getEndValid();
|
|
||||||
if (fragment + 1 < fragments.getEndValid()){endKey = fragments.getFirstKey(fragment + 1);}
|
|
||||||
|
|
||||||
std::stringstream header;
|
|
||||||
|
|
||||||
if (M.getLive()){
|
|
||||||
MP4::SIDX sidxBox;
|
|
||||||
sidxBox.setTimescale(1000);
|
|
||||||
sidxBox.setEarliestPresentationTime(keys.getTime(firstKey));
|
|
||||||
|
|
||||||
MP4::sidxReference refItem;
|
|
||||||
refItem.referencedSize = 230000;
|
|
||||||
refItem.subSegmentDuration = keys.getTime(endKey) - keys.getTime(firstKey);
|
|
||||||
refItem.sapStart = true;
|
|
||||||
refItem.sapType = 16;
|
|
||||||
refItem.sapDeltaTime = 0;
|
|
||||||
|
|
||||||
refItem.referenceType = 0;
|
|
||||||
sidxBox.setReference(refItem, 0);
|
|
||||||
sidxBox.setReferenceID(1);
|
|
||||||
|
|
||||||
header.write(sidxBox.asBox(), sidxBox.boxedSize());
|
|
||||||
}
|
|
||||||
|
|
||||||
MP4::MOOF moofBox;
|
|
||||||
MP4::MFHD mfhdBox(fragment + 1);
|
|
||||||
moofBox.setContent(mfhdBox, 0);
|
|
||||||
|
|
||||||
size_t firstPart = keys.getFirstPart(firstKey);
|
|
||||||
size_t endPart = parts.getEndValid();
|
|
||||||
if (fragment + 1 < fragments.getEndValid()){
|
|
||||||
endPart = keys.getFirstPart(fragments.getFirstKey(fragment + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<sortPart> trunOrder;
|
|
||||||
|
|
||||||
uint64_t relativeOffset = fragmentHeaderSize(M, track, fragment) + 8;
|
|
||||||
|
|
||||||
sortPart temp;
|
|
||||||
temp.time = keys.getTime(firstKey);
|
|
||||||
temp.partIndex = keys.getFirstPart(firstKey);
|
|
||||||
temp.bytePos = relativeOffset;
|
|
||||||
|
|
||||||
for (size_t p = firstPart; p < endPart; p++){
|
|
||||||
trunOrder.insert(temp);
|
|
||||||
temp.time += parts.getDuration(p);
|
|
||||||
temp.partIndex++;
|
|
||||||
temp.bytePos += parts.getSize(p);
|
|
||||||
}
|
|
||||||
|
|
||||||
MP4::TRAF trafBox;
|
|
||||||
MP4::TFHD tfhdBox;
|
|
||||||
|
|
||||||
tfhdBox.setFlags(MP4::tfhdSampleFlag | MP4::tfhdBaseIsMoof | MP4::tfhdSampleDesc);
|
|
||||||
tfhdBox.setTrackID(track + 1);
|
|
||||||
if (simplifyTrackIds){
|
|
||||||
tfhdBox.setTrackID(simplifiedTrackId(M, track));
|
|
||||||
}
|
|
||||||
tfhdBox.setDefaultSampleDuration(444);
|
|
||||||
tfhdBox.setDefaultSampleSize(444);
|
|
||||||
tfhdBox.setDefaultSampleFlags((M.getType(track) == "video") ? (MP4::noIPicture | MP4::noKeySample)
|
|
||||||
: (MP4::isIPicture | MP4::isKeySample));
|
|
||||||
tfhdBox.setSampleDescriptionIndex(1);
|
|
||||||
trafBox.setContent(tfhdBox, 0);
|
|
||||||
|
|
||||||
MP4::TFDT tfdtBox;
|
|
||||||
if (M.getVod()){
|
|
||||||
tfdtBox.setBaseMediaDecodeTime(M.getTimeForFragmentIndex(track, fragment) - M.getFirstms(track));
|
|
||||||
}else{
|
|
||||||
tfdtBox.setBaseMediaDecodeTime((UTCTime ? M.getTimeForFragmentIndex(track, fragment) + M.getBootMsOffset() + unixBootDiff : M.getTimeForFragmentIndex(track, fragment)));
|
|
||||||
}
|
|
||||||
trafBox.setContent(tfdtBox, 1);
|
|
||||||
|
|
||||||
MP4::TRUN trunBox;
|
|
||||||
trunBox.setFlags(MP4::trundataOffset | MP4::trunfirstSampleFlags | MP4::trunsampleSize |
|
|
||||||
MP4::trunsampleDuration | MP4::trunsampleOffsets);
|
|
||||||
|
|
||||||
// The value set here, will be updated afterwards to the correct value
|
|
||||||
trunBox.setDataOffset(trunOrder.begin()->bytePos);
|
|
||||||
|
|
||||||
trunBox.setFirstSampleFlags(MP4::isIPicture | MP4::isKeySample);
|
|
||||||
|
|
||||||
size_t trunOffset = 0;
|
|
||||||
|
|
||||||
for (std::set<sortPart>::iterator it = trunOrder.begin(); it != trunOrder.end(); it++){
|
|
||||||
MP4::trunSampleInformation sampleInfo;
|
|
||||||
sampleInfo.sampleSize = parts.getSize(it->partIndex);
|
|
||||||
sampleInfo.sampleDuration = parts.getDuration(it->partIndex);
|
|
||||||
sampleInfo.sampleOffset = parts.getOffset(it->partIndex);
|
|
||||||
trunBox.setSampleInformation(sampleInfo, trunOffset++);
|
|
||||||
}
|
|
||||||
trafBox.setContent(trunBox, 2);
|
|
||||||
|
|
||||||
moofBox.setContent(trafBox, 1);
|
|
||||||
|
|
||||||
header.write(moofBox.asBox(), moofBox.boxedSize());
|
|
||||||
|
|
||||||
return header.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the full size of a 'moof' box for a DTSC::Key based fragment.
|
/// Calculates the full size of a 'moof' box for a DTSC::Key based fragment.
|
||||||
/// Used when building the 'moof' box to calculate the relative data offsets.
|
/// Used when building the 'moof' box to calculate the relative data offsets.
|
||||||
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime){
|
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime){
|
||||||
|
|
|
@ -5,10 +5,8 @@
|
||||||
|
|
||||||
namespace CMAF{
|
namespace CMAF{
|
||||||
size_t payloadSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime);
|
size_t payloadSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime);
|
||||||
size_t trackHeaderSize(const DTSC::Meta &M, size_t track);
|
|
||||||
std::string trackHeader(const DTSC::Meta &M, size_t track, bool simplifyTrackIds = false);
|
std::string trackHeader(const DTSC::Meta &M, size_t track, bool simplifyTrackIds = false);
|
||||||
size_t fragmentHeaderSize(const DTSC::Meta &M, size_t track, size_t fragment);
|
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, size_t fragment);
|
||||||
std::string fragmentHeader(const DTSC::Meta &M, size_t track, size_t fragment, bool simplifyTrackIds = false, bool UTCTime = false);
|
|
||||||
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime);
|
size_t keyHeaderSize(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime);
|
||||||
std::string keyHeader(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime, uint64_t segmentNum, bool simplifyTrackIds = false, bool UTCTime = false);
|
std::string keyHeader(const DTSC::Meta &M, size_t track, uint64_t startTime, uint64_t endTime, uint64_t segmentNum, bool simplifyTrackIds = false, bool UTCTime = false);
|
||||||
}// namespace CMAF
|
}// namespace CMAF
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
#include "output_cmaf.h"
|
#include "output_cmaf.h"
|
||||||
#include <iomanip>
|
|
||||||
#include <mist/bitfields.h>
|
#include <mist/bitfields.h>
|
||||||
#include <mist/checksum.h>
|
#include <mist/checksum.h>
|
||||||
#include <mist/cmaf.h>
|
#include <mist/cmaf.h>
|
||||||
#include <mist/defines.h>
|
// #include <mist/defines.h>
|
||||||
#include <mist/encode.h>
|
// #include <mist/encode.h>
|
||||||
#include <mist/langcodes.h> /*LTS*/
|
#include <mist/hls_support.h>
|
||||||
#include <mist/mp4.h>
|
// #include <mist/mp4.h>
|
||||||
#include <mist/mp4_dash.h>
|
// #include <mist/mp4_dash.h>
|
||||||
#include <mist/mp4_encryption.h>
|
// #include <mist/mp4_encryption.h>
|
||||||
#include <mist/mp4_generic.h>
|
// #include <mist/mp4_generic.h>
|
||||||
#include <mist/stream.h>
|
// #include <mist/timing.h>
|
||||||
#include <mist/timing.h>
|
|
||||||
|
int64_t bootMsOffset; // boot time in ms
|
||||||
|
uint64_t systemBoot; // time since boot in ms
|
||||||
|
const std::string hlsMediaFormat = ".m4s";
|
||||||
|
|
||||||
uint64_t bootMsOffset;
|
|
||||||
uint64_t cmafBoot = Util::bootSecs();
|
uint64_t cmafBoot = Util::bootSecs();
|
||||||
uint64_t dataUp = 0;
|
uint64_t dataUp = 0;
|
||||||
uint64_t dataDown = 0;
|
uint64_t dataDown = 0;
|
||||||
|
@ -28,13 +29,12 @@ namespace Mist{
|
||||||
http.SendRequest(D.getSocket());
|
http.SendRequest(D.getSocket());
|
||||||
|
|
||||||
if (debugParam.length()){
|
if (debugParam.length()){
|
||||||
if (debugParam[debugParam.length()-1] != '/'){
|
if (debugParam[debugParam.length() - 1] != '/'){debugParam += '/';}
|
||||||
debugParam += '/';
|
|
||||||
}
|
|
||||||
debug = true;
|
debug = true;
|
||||||
std::string filename = url.getUrl();
|
std::string filename = url.getUrl();
|
||||||
filename.erase(0, filename.rfind("/") + 1);
|
filename.erase(0, filename.rfind("/") + 1);
|
||||||
snprintf(debugName, 500, "%s%s-%" PRIu64, debugParam.c_str(), filename.c_str(), Util::bootMS());
|
snprintf(debugName, 500, "%s%s-%" PRIu64, debugParam.c_str(), filename.c_str(),
|
||||||
|
Util::bootMS());
|
||||||
INFO_MSG("CMAF DEBUG FILE: %s", debugName);
|
INFO_MSG("CMAF DEBUG FILE: %s", debugName);
|
||||||
debugFile = fopen(debugName, "wb");
|
debugFile = fopen(debugName, "wb");
|
||||||
}
|
}
|
||||||
|
@ -58,19 +58,29 @@ namespace Mist{
|
||||||
uint64_t preUp = D.getSocket().dataUp();
|
uint64_t preUp = D.getSocket().dataUp();
|
||||||
uint64_t preDown = D.getSocket().dataDown();
|
uint64_t preDown = D.getSocket().dataDown();
|
||||||
D.getHTTP().Chunkify(data, len, D.getSocket());
|
D.getHTTP().Chunkify(data, len, D.getSocket());
|
||||||
if (debug && debugFile) {
|
if (debug && debugFile){fwrite(data, 1, len, debugFile);}
|
||||||
fwrite(data, 1, len, debugFile);
|
|
||||||
|
|
||||||
}
|
|
||||||
dataUp += D.getSocket().dataUp() - preUp;
|
dataUp += D.getSocket().dataUp() - preUp;
|
||||||
dataDown += D.getSocket().dataDown() - preDown;
|
dataDown += D.getSocket().dataDown() - preDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CMAFPushTrack::send(const std::string & data){
|
void CMAFPushTrack::send(const std::string &data){send(data.data(), data.size());}
|
||||||
send(data.data(), data.size());
|
|
||||||
|
bool OutCMAF::isReadyForPlay(){
|
||||||
|
if (!isInitialized){initialize();}
|
||||||
|
meta.reloadReplacedPagesIfNeeded();
|
||||||
|
if (!M.getValidTracks().size()){return false;}
|
||||||
|
uint32_t mainTrack = M.mainTrack();
|
||||||
|
if (mainTrack == INVALID_TRACK_ID){return false;}
|
||||||
|
DTSC::Fragments fragments(M.fragments(mainTrack));
|
||||||
|
return fragments.getValidCount() > 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
OutCMAF::OutCMAF(Socket::Connection &conn) : HTTPOutput(conn){
|
OutCMAF::OutCMAF(Socket::Connection &conn) : HTTPOutput(conn){
|
||||||
|
// load from global config
|
||||||
|
systemBoot = Util::getGlobalConfig("systemBoot").asInt();
|
||||||
|
// fall back to local calculation if loading from global config fails
|
||||||
|
if (!systemBoot){systemBoot = (Util::unixMS() - Util::bootMS());}
|
||||||
|
|
||||||
uaDelay = 0;
|
uaDelay = 0;
|
||||||
realTime = 0;
|
realTime = 0;
|
||||||
if (config->getString("target").size()){
|
if (config->getString("target").size()){
|
||||||
|
@ -81,11 +91,13 @@ namespace Mist{
|
||||||
target.replace(0, 4, "http"); // Translate to http for cmaf:// or https for cmafs://
|
target.replace(0, 4, "http"); // Translate to http for cmaf:// or https for cmafs://
|
||||||
pushUrl = HTTP::URL(target);
|
pushUrl = HTTP::URL(target);
|
||||||
|
|
||||||
INFO_MSG("About to push stream %s out. Host: %s, port: %d, location: %s", streamName.c_str(),
|
INFO_MSG("About to push stream %s out. Host: %s, port: %" PRIu32 ", location: %s",
|
||||||
pushUrl.host.c_str(), pushUrl.getPort(), pushUrl.path.c_str());
|
streamName.c_str(), pushUrl.host.c_str(), pushUrl.getPort(), pushUrl.path.c_str());
|
||||||
initialize();
|
initialize();
|
||||||
initialSeek();
|
initialSeek();
|
||||||
startPushOut();
|
startPushOut();
|
||||||
|
}else{
|
||||||
|
realTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +107,8 @@ namespace Mist{
|
||||||
Output::connStats(now, statComm);
|
Output::connStats(now, statComm);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//For push output, this data is not coming from the usual place as we have multiple connections to worry about.
|
// For push output, this data is not coming from the usual place as we have multiple
|
||||||
|
// connections to worry about.
|
||||||
statComm.setUp(dataUp);
|
statComm.setUp(dataUp);
|
||||||
statComm.setDown(dataDown);
|
statComm.setDown(dataDown);
|
||||||
statComm.setTime(now - cmafBoot);
|
statComm.setTime(now - cmafBoot);
|
||||||
|
@ -103,7 +116,8 @@ namespace Mist{
|
||||||
|
|
||||||
// Properly end all tracks on shutdown.
|
// Properly end all tracks on shutdown.
|
||||||
OutCMAF::~OutCMAF(){
|
OutCMAF::~OutCMAF(){
|
||||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end();
|
||||||
|
it++){
|
||||||
onTrackEnd(it->first);
|
onTrackEnd(it->first);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,6 +159,18 @@ namespace Mist{
|
||||||
// MP3 does not work in browsers
|
// MP3 does not work in browsers
|
||||||
capa["exceptions"]["codec:MP3"] = JSON::fromString("[[\"blacklist\",[\"Mozilla/\"]]]");
|
capa["exceptions"]["codec:MP3"] = JSON::fromString("[[\"blacklist\",[\"Mozilla/\"]]]");
|
||||||
|
|
||||||
|
cfg->addOption(
|
||||||
|
"listlimit",
|
||||||
|
JSON::fromString(
|
||||||
|
"{\"arg\":\"integer\",\"default\":0,\"short\":\"y\",\"long\":\"list-limit\","
|
||||||
|
"\"help\":\"Maximum number of segments in live playlists (0 = infinite).\"}"));
|
||||||
|
capa["optional"]["listlimit"]["name"] = "Live playlist limit";
|
||||||
|
capa["optional"]["listlimit"]["help"] =
|
||||||
|
"Maximum number of parts in live playlists. (0 = infinite)";
|
||||||
|
capa["optional"]["listlimit"]["default"] = 0;
|
||||||
|
capa["optional"]["listlimit"]["type"] = "uint";
|
||||||
|
capa["optional"]["listlimit"]["option"] = "--list-limit";
|
||||||
|
|
||||||
cfg->addOption("nonchunked",
|
cfg->addOption("nonchunked",
|
||||||
JSON::fromString("{\"short\":\"C\",\"long\":\"nonchunked\",\"help\":\"Do not "
|
JSON::fromString("{\"short\":\"C\",\"long\":\"nonchunked\",\"help\":\"Do not "
|
||||||
"send chunked, but buffer whole segments.\"}"));
|
"send chunked, but buffer whole segments.\"}"));
|
||||||
|
@ -154,6 +180,28 @@ namespace Mist{
|
||||||
"significantly, but increases compatibility somewhat.";
|
"significantly, but increases compatibility somewhat.";
|
||||||
capa["optional"]["nonchunked"]["option"] = "--nonchunked";
|
capa["optional"]["nonchunked"]["option"] = "--nonchunked";
|
||||||
|
|
||||||
|
cfg->addOption("mergesessions",
|
||||||
|
JSON::fromString("{\"short\":\"M\",\"long\":\"mergesessions\",\"help\":\"Merge "
|
||||||
|
"together sessions from one user into a single session.\"}"));
|
||||||
|
capa["optional"]["mergesessions"]["name"] = "Merge sessions";
|
||||||
|
capa["optional"]["mergesessions"]["help"] =
|
||||||
|
"If enabled, merges together all views from a single user into a single combined session. "
|
||||||
|
"If disabled, each view (main playlist request) is a separate session.";
|
||||||
|
capa["optional"]["mergesessions"]["option"] = "--mergesessions";
|
||||||
|
|
||||||
|
cfg->addOption("chunkpath",
|
||||||
|
JSON::fromString("{\"arg\":\"string\",\"default\":\"\",\"short\":\"e\",\"long\":"
|
||||||
|
"\"chunkpath\",\"help\":\"Alternate URL path to "
|
||||||
|
"prepend to chunk paths, for serving through e.g. a CDN\"}"));
|
||||||
|
capa["optional"]["chunkpath"]["name"] = "Prepend path for chunks";
|
||||||
|
capa["optional"]["chunkpath"]["help"] =
|
||||||
|
"Chunks will be served from this path. This also disables sessions IDs for chunks.";
|
||||||
|
capa["optional"]["chunkpath"]["default"] = "";
|
||||||
|
capa["optional"]["chunkpath"]["type"] = "str";
|
||||||
|
capa["optional"]["chunkpath"]["option"] = "--chunkpath";
|
||||||
|
capa["optional"]["chunkpath"]["short"] = "e";
|
||||||
|
capa["optional"]["chunkpath"]["default"] = "";
|
||||||
|
|
||||||
capa["push_urls"].append("cmaf://*");
|
capa["push_urls"].append("cmaf://*");
|
||||||
capa["push_urls"].append("cmafs://*");
|
capa["push_urls"].append("cmafs://*");
|
||||||
|
|
||||||
|
@ -165,39 +213,148 @@ namespace Mist{
|
||||||
cfg->addOption("target", opt);
|
cfg->addOption("target", opt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/******************************/
|
||||||
|
/* HLS Manifest Generation */
|
||||||
|
/******************************/
|
||||||
|
|
||||||
|
/// \brief Builds master playlist for (LL)HLS.
|
||||||
|
///\return The master playlist file for (LL)HLS.
|
||||||
|
void OutCMAF::sendHlsMasterManifest(){
|
||||||
|
selectDefaultTracks();
|
||||||
|
|
||||||
|
std::string sessId = "";
|
||||||
|
if (hasSessionIDs()){
|
||||||
|
std::string ua = UA + JSON::Value(getpid()).asString();
|
||||||
|
crc = checksum::crc32(0, ua.data(), ua.size());
|
||||||
|
sessId = JSON::Value(crc).asString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for forced "no low latency" parameter
|
||||||
|
bool noLLHLS = H.GetVar("llhls").size() ? H.GetVar("llhls") == "0" : false;
|
||||||
|
|
||||||
|
// Populate the struct that will help generate the master playlist
|
||||||
|
const HLS::MasterData masterData ={
|
||||||
|
hasSessionIDs(),
|
||||||
|
noLLHLS,
|
||||||
|
hlsMediaFormat == ".ts",
|
||||||
|
getMainSelectedTrack(),
|
||||||
|
H.GetHeader("User-Agent"),
|
||||||
|
sessId,
|
||||||
|
systemBoot,
|
||||||
|
bootMsOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::stringstream result;
|
||||||
|
HLS::addMasterManifest(result, M, userSelect, masterData);
|
||||||
|
|
||||||
|
H.SetBody(result.str());
|
||||||
|
H.SendResponse("200", "OK", myConn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief Builds media playlist to (LL)HLS
|
||||||
|
///\return The media playlist file to (LL)HLS
|
||||||
|
void OutCMAF::sendHlsMediaManifest(const size_t requestTid){
|
||||||
|
const HLS::HlsSpecData hlsSpec ={H.GetVar("_HLS_skip"), H.GetVar("_HLS_msn"),
|
||||||
|
H.GetVar("_HLS_part")};
|
||||||
|
|
||||||
|
size_t timingTid = HLS::getTimingTrackId(M, H.GetVar("mTrack"), getMainSelectedTrack());
|
||||||
|
|
||||||
|
// Chunkpath & Session ID logic
|
||||||
|
std::string urlPrefix = "";
|
||||||
|
std::string sessId = "";
|
||||||
|
if (config->getString("chunkpath").size()){
|
||||||
|
urlPrefix = HTTP::URL(config->getString("chunkpath")).link("./" + H.url).link("./").getUrl();
|
||||||
|
}else{
|
||||||
|
sessId = H.GetVar("sessId");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for forced "no low latency" parameter
|
||||||
|
bool noLLHLS = H.GetVar("llhls").size() ? H.GetVar("llhls") == "0" : false;
|
||||||
|
// override if valid header forces "no low latency"
|
||||||
|
noLLHLS = H.GetHeader("X-Mist-LLHLS").size() ? H.GetHeader("X-Mist-LLHLS") == "0" : noLLHLS;
|
||||||
|
|
||||||
|
const HLS::TrackData trackData ={
|
||||||
|
M.getLive(),
|
||||||
|
M.getType(requestTid) == "video",
|
||||||
|
noLLHLS,
|
||||||
|
hlsMediaFormat,
|
||||||
|
M.getEncryption(requestTid),
|
||||||
|
sessId,
|
||||||
|
timingTid,
|
||||||
|
requestTid,
|
||||||
|
M.biggestFragment(timingTid) / 1000,
|
||||||
|
atol(H.GetVar("iMsn").c_str()),
|
||||||
|
config->getInteger("listlimit"),
|
||||||
|
urlPrefix,
|
||||||
|
systemBoot,
|
||||||
|
bootMsOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fragment & Key handlers
|
||||||
|
DTSC::Fragments fragments(M.fragments(trackData.timingTrackId));
|
||||||
|
DTSC::Keys keys(M.keys(trackData.timingTrackId));
|
||||||
|
|
||||||
|
uint32_t bprErrCode = HLS::blockPlaylistReload(M, userSelect, trackData, hlsSpec, fragments, keys);
|
||||||
|
if (bprErrCode == 400){
|
||||||
|
H.SendResponse("400", "Bad Request: Invalid LLHLS parameter", myConn);
|
||||||
|
return;
|
||||||
|
}else if (bprErrCode == 503){
|
||||||
|
H.SendResponse("503", "Service Unavailable", myConn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HLS::FragmentData fragData;
|
||||||
|
HLS::populateFragmentData(M, userSelect, fragData, trackData, fragments, keys);
|
||||||
|
|
||||||
|
std::stringstream result;
|
||||||
|
HLS::addStartingMetaTags(result, fragData, trackData, hlsSpec);
|
||||||
|
HLS::addMediaFragments(result, M, fragData, trackData, fragments, keys);
|
||||||
|
HLS::addEndingTags(result, M, userSelect, fragData, trackData);
|
||||||
|
|
||||||
|
H.SetBody(result.str());
|
||||||
|
H.SendResponse("200", "OK", myConn);
|
||||||
|
}// namespace Mist
|
||||||
|
|
||||||
|
void OutCMAF::sendHlsManifest(const std::string url){
|
||||||
|
H.setCORSHeaders();
|
||||||
|
H.SetHeader("Content-Type", "application/vnd.apple.mpegurl;version=7"); // for .m3u8
|
||||||
|
H.SetHeader("Cache-Control", "no-store");
|
||||||
|
if (H.method == "OPTIONS" || H.method == "HEAD"){
|
||||||
|
H.SetBody("");
|
||||||
|
H.SendResponse("200", "OK", myConn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.find("/") == std::string::npos){
|
||||||
|
sendHlsMasterManifest();
|
||||||
|
}else{
|
||||||
|
sendHlsMediaManifest(atoll(url.c_str()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void OutCMAF::onHTTP(){
|
void OutCMAF::onHTTP(){
|
||||||
initialize();
|
initialize();
|
||||||
bootMsOffset = 0;
|
bootMsOffset = 0;
|
||||||
if (M.getLive()){bootMsOffset = M.getBootMsOffset();}
|
if (M.getLive()){bootMsOffset = M.getBootMsOffset();}
|
||||||
|
|
||||||
if (H.url.size() < streamName.length() + 7){
|
if (H.url.find('/', 6) == std::string::npos){
|
||||||
H.Clean();
|
|
||||||
H.SendResponse("404", "Stream not found", myConn);
|
H.SendResponse("404", "Stream not found", myConn);
|
||||||
H.Clean();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string method = H.method;
|
// Strip /cmaf/<streamname>/ from url
|
||||||
std::string url = H.url.substr(streamName.length() + 7); // Strip /cmaf/<streamname>/ from url
|
std::string url = H.url.substr(H.url.find('/', 6) + 1);
|
||||||
|
HTTP::URL req(reqUrl);
|
||||||
|
|
||||||
// Send a dash manifest for any URL with .mpd in the path
|
// Send a dash manifest for any URL with .mpd in the path
|
||||||
if (url.find(".mpd") != std::string::npos){
|
if (req.getExt() == "mpd"){
|
||||||
sendDashManifest();
|
sendDashManifest();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send a hls manifest for any URL with index.m3u8 in the path
|
// Send a hls manifest for any URL with index.m3u8 in the path
|
||||||
if (url.find("index.m3u8") != std::string::npos){
|
if (req.getExt() == "m3u8"){
|
||||||
size_t loc = url.find("index.m3u8");
|
sendHlsManifest(url);
|
||||||
if (loc == 0){
|
|
||||||
sendHlsManifest();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t idx = atoll(url.c_str());
|
|
||||||
if (url.find("?") == std::string::npos){
|
|
||||||
sendHlsManifest(idx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,13 +364,24 @@ namespace Mist{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
H.Clean();
|
const uint64_t msn = atoll(H.GetVar("msn").c_str());
|
||||||
H.SetHeader("Content-Type", "video/mp4");
|
const uint64_t dur = atoll(H.GetVar("dur").c_str());
|
||||||
H.SetHeader("Cache-Control", "no-cache");
|
const uint64_t mTrack = atoll(H.GetVar("mTrack").c_str());
|
||||||
|
|
||||||
|
H.SetHeader("Content-Type", "video/mp4"); // For .m4s
|
||||||
|
if (hasSessionIDs() && !config->getOption("chunkpath")){
|
||||||
|
H.SetHeader("Cache-Control", "no-store");
|
||||||
|
}else{
|
||||||
|
H.SetHeader("Cache-Control",
|
||||||
|
"public, max-age=" +
|
||||||
|
JSON::Value(M.getDuration(getMainSelectedTrack()) / 1000).asString() +
|
||||||
|
", immutable");
|
||||||
|
H.SetHeader("Pragma", "");
|
||||||
|
H.SetHeader("Expires", "");
|
||||||
|
}
|
||||||
H.setCORSHeaders();
|
H.setCORSHeaders();
|
||||||
if (method == "OPTIONS" || method == "HEAD"){
|
if (H.method == "OPTIONS" || H.method == "HEAD"){
|
||||||
H.SendResponse("200", "OK", myConn);
|
H.SendResponse("200", "OK", myConn);
|
||||||
H.Clean();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,16 +390,20 @@ namespace Mist{
|
||||||
idx = atoll(url.c_str() + url.find("Q(") + 2) % 100;
|
idx = atoll(url.c_str() + url.find("Q(") + 2) % 100;
|
||||||
}
|
}
|
||||||
if (!M.getValidTracks().count(idx)){
|
if (!M.getValidTracks().count(idx)){
|
||||||
H.Clean();
|
|
||||||
H.SendResponse("404", "Track not found", myConn);
|
H.SendResponse("404", "Track not found", myConn);
|
||||||
H.Clean();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url.find(".m4s") == std::string::npos){
|
if (url.find(hlsMediaFormat) == std::string::npos){
|
||||||
H.Clean();
|
|
||||||
H.SendResponse("404", "File not found", myConn);
|
H.SendResponse("404", "File not found", myConn);
|
||||||
H.Clean();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.find("init" + hlsMediaFormat) != std::string::npos){
|
||||||
|
std::string headerData = CMAF::trackHeader(M, idx);
|
||||||
|
H.StartResponse(H, myConn, config->getBool("nonchunked"));
|
||||||
|
H.Chunkify(headerData.c_str(), headerData.size(), myConn);
|
||||||
|
H.Chunkify("", 0, myConn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,28 +411,42 @@ namespace Mist{
|
||||||
userSelect.clear();
|
userSelect.clear();
|
||||||
userSelect[idx].reload(streamName, idx);
|
userSelect[idx].reload(streamName, idx);
|
||||||
|
|
||||||
H.StartResponse(H, myConn, config->getBool("nonchunked"));
|
uint64_t fragmentIndex;
|
||||||
|
uint64_t startTime;
|
||||||
|
uint32_t part;
|
||||||
|
|
||||||
if (url.find("init.m4s") != std::string::npos){
|
// set targetTime
|
||||||
std::string headerData = CMAF::trackHeader(M, idx);
|
if (sscanf(url.c_str(), "%*d/chunk_%" PRIu64 ".%" PRIu32 ".*", &startTime, &part) == 2){
|
||||||
H.Chunkify(headerData.c_str(), headerData.size(), myConn);
|
// Logic: calculate targetTime for partial segments
|
||||||
H.Chunkify("", 0, myConn);
|
targetTime = HLS::getPartTargetTime(M, idx, mTrack, startTime, msn, part);
|
||||||
H.Clean();
|
if (!targetTime){
|
||||||
|
H.SendResponse("404", "Partial fragment does not exist", myConn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
startTime += part * HLS::partDurationMaxMs;
|
||||||
uint64_t startTime = atoll(url.c_str() + url.find("/chunk_") + 7);
|
fragmentIndex = M.getFragmentIndexForTime(mTrack, startTime);
|
||||||
|
DEBUG_MSG(5, "partial segment requested: %s st %" PRIu64 " et %" PRIu64, url.c_str(),
|
||||||
|
startTime, targetTime);
|
||||||
|
}else if (sscanf(url.c_str(), "%*d/chunk_%" PRIu64 ".*", &startTime) == 1){
|
||||||
|
// Logic: calculate targetTime for full segments
|
||||||
if (M.getVod()){startTime += M.getFirstms(idx);}
|
if (M.getVod()){startTime += M.getFirstms(idx);}
|
||||||
uint64_t fragmentIndex = M.getFragmentIndexForTime(idx, startTime);
|
fragmentIndex = M.getFragmentIndexForTime(mTrack, startTime);
|
||||||
targetTime = M.getTimeForFragmentIndex(idx, fragmentIndex + 1);
|
targetTime = dur ? startTime + dur : M.getTimeForFragmentIndex(mTrack, fragmentIndex + 1);
|
||||||
|
DEBUG_MSG(5, "full segment requested: %s st %" PRIu64 " et %" PRIu64 " asd", url.c_str(),
|
||||||
|
startTime, targetTime);
|
||||||
|
}else{
|
||||||
|
H.SendResponse("400", "Bad Request: Could not parse the url", myConn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string headerData =
|
||||||
|
CMAF::keyHeader(M, idx, startTime, targetTime, fragmentIndex, false, false);
|
||||||
|
|
||||||
std::string headerData = CMAF::fragmentHeader(M, idx, fragmentIndex, false, false);
|
uint64_t mdatSize = 8 + CMAF::payloadSize(M, idx, startTime, targetTime);
|
||||||
H.Chunkify(headerData.c_str(), headerData.size(), myConn);
|
|
||||||
|
|
||||||
uint64_t mdatSize = 8 + CMAF::payloadSize(M, idx, targetTime, M.getTimeForFragmentIndex(idx, fragmentIndex+1));
|
|
||||||
char mdatHeader[] ={0x00, 0x00, 0x00, 0x00, 'm', 'd', 'a', 't'};
|
char mdatHeader[] ={0x00, 0x00, 0x00, 0x00, 'm', 'd', 'a', 't'};
|
||||||
Bit::htobl(mdatHeader, mdatSize);
|
Bit::htobl(mdatHeader, mdatSize);
|
||||||
|
|
||||||
|
H.StartResponse(H, myConn, config->getBool("nonchunked"));
|
||||||
|
H.Chunkify(headerData.c_str(), headerData.size(), myConn);
|
||||||
H.Chunkify(mdatHeader, 8, myConn);
|
H.Chunkify(mdatHeader, 8, myConn);
|
||||||
|
|
||||||
seek(startTime);
|
seek(startTime);
|
||||||
|
@ -269,8 +455,6 @@ namespace Mist{
|
||||||
parseData = true;
|
parseData = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void OutCMAF::sendNext(){
|
void OutCMAF::sendNext(){
|
||||||
if (isRecording()){
|
if (isRecording()){
|
||||||
pushNext();
|
pushNext();
|
||||||
|
@ -281,7 +465,6 @@ namespace Mist{
|
||||||
wantRequest = true;
|
wantRequest = true;
|
||||||
parseData = false;
|
parseData = false;
|
||||||
H.Chunkify("", 0, myConn);
|
H.Chunkify("", 0, myConn);
|
||||||
H.Clean();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
char *data;
|
char *data;
|
||||||
|
@ -309,10 +492,10 @@ namespace Mist{
|
||||||
void callBack(uint64_t, uint64_t, std::stringstream &, bool)){
|
void callBack(uint64_t, uint64_t, std::stringstream &, bool)){
|
||||||
DTSC::Fragments fragments(M.fragments(idx));
|
DTSC::Fragments fragments(M.fragments(idx));
|
||||||
uint32_t firstFragment = fragments.getFirstValid();
|
uint32_t firstFragment = fragments.getFirstValid();
|
||||||
uint32_t endFragment = fragments.getEndValid();
|
uint32_t lastFragment = fragments.getEndValid();
|
||||||
bool first = true;
|
bool first = true;
|
||||||
// skip the first two fragments if live
|
// skip the first two fragments if live
|
||||||
if (M.getLive() && (endFragment - firstFragment) > 6){firstFragment += 2;}
|
if (M.getLive() && (lastFragment - firstFragment) > 6){firstFragment += 2;}
|
||||||
|
|
||||||
if (M.getType(idx) == "audio"){
|
if (M.getType(idx) == "audio"){
|
||||||
uint32_t mainTrack = M.mainTrack();
|
uint32_t mainTrack = M.mainTrack();
|
||||||
|
@ -323,7 +506,7 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
DTSC::Keys keys(M.keys(idx));
|
DTSC::Keys keys(M.keys(idx));
|
||||||
for (; firstFragment < endFragment; ++firstFragment){
|
for (; firstFragment < lastFragment; ++firstFragment){
|
||||||
uint32_t duration = fragments.getDuration(firstFragment);
|
uint32_t duration = fragments.getDuration(firstFragment);
|
||||||
uint64_t starttime = keys.getTime(fragments.getFirstKey(firstFragment));
|
uint64_t starttime = keys.getTime(fragments.getFirstKey(firstFragment));
|
||||||
if (!duration){
|
if (!duration){
|
||||||
|
@ -379,7 +562,7 @@ namespace Mist{
|
||||||
std::string method = H.method;
|
std::string method = H.method;
|
||||||
H.Clean();
|
H.Clean();
|
||||||
H.SetHeader("Content-Type", "application/dash+xml");
|
H.SetHeader("Content-Type", "application/dash+xml");
|
||||||
H.SetHeader("Cache-Control", "no-cache");
|
H.SetHeader("Cache-Control", "no-store");
|
||||||
H.setCORSHeaders();
|
H.setCORSHeaders();
|
||||||
if (method == "OPTIONS" || method == "HEAD"){
|
if (method == "OPTIONS" || method == "HEAD"){
|
||||||
H.SendResponse("200", "OK", myConn);
|
H.SendResponse("200", "OK", myConn);
|
||||||
|
@ -414,7 +597,8 @@ namespace Mist{
|
||||||
<< M.getFpks(idx) / 1000 << "\" ";
|
<< M.getFpks(idx) / 1000 << "\" ";
|
||||||
}
|
}
|
||||||
r << "segmentAlignment=\"true\" id=\"" << idx
|
r << "segmentAlignment=\"true\" id=\"" << idx
|
||||||
<< "\" startWithSAP=\"1\" subsegmentAlignment=\"true\" subsegmentStartsWithSAP=\"1\">" << std::endl;
|
<< "\" startWithSAP=\"1\" subsegmentAlignment=\"true\" subsegmentStartsWithSAP=\"1\">"
|
||||||
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutCMAF::dashRepresentation(size_t id, size_t idx, std::stringstream &r){
|
void OutCMAF::dashRepresentation(size_t id, size_t idx, std::stringstream &r){
|
||||||
|
@ -440,7 +624,8 @@ namespace Mist{
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutCMAF::dashAdaptation(size_t id, std::set<size_t> tracks, bool aligned, std::stringstream &r){
|
void OutCMAF::dashAdaptation(size_t id, std::set<size_t> tracks, bool aligned,
|
||||||
|
std::stringstream &r){
|
||||||
if (!tracks.size()){return;}
|
if (!tracks.size()){return;}
|
||||||
if (aligned){
|
if (aligned){
|
||||||
size_t firstTrack = *tracks.begin();
|
size_t firstTrack = *tracks.begin();
|
||||||
|
@ -473,7 +658,8 @@ namespace Mist{
|
||||||
std::set<size_t> vTracks;
|
std::set<size_t> vTracks;
|
||||||
std::set<size_t> aTracks;
|
std::set<size_t> aTracks;
|
||||||
std::set<size_t> sTracks;
|
std::set<size_t> sTracks;
|
||||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end();
|
||||||
|
it++){
|
||||||
if (M.getType(it->first) == "video"){vTracks.insert(it->first);}
|
if (M.getType(it->first) == "video"){vTracks.insert(it->first);}
|
||||||
if (M.getType(it->first) == "audio"){aTracks.insert(it->first);}
|
if (M.getType(it->first) == "audio"){aTracks.insert(it->first);}
|
||||||
if (M.getType(it->first) == "subtitle"){sTracks.insert(it->first);}
|
if (M.getType(it->first) == "subtitle"){sTracks.insert(it->first);}
|
||||||
|
@ -490,7 +676,8 @@ namespace Mist{
|
||||||
size_t mainTrack = getMainSelectedTrack();
|
size_t mainTrack = getMainSelectedTrack();
|
||||||
size_t mainDuration = M.getDuration(mainTrack);
|
size_t mainDuration = M.getDuration(mainTrack);
|
||||||
if (M.getVod()){
|
if (M.getVod()){
|
||||||
r << "type=\"static\" mediaPresentationDuration=\"" << dashTime(mainDuration) << "\" minBufferTime=\"PT1.5S\" ";
|
r << "type=\"static\" mediaPresentationDuration=\"" << dashTime(mainDuration)
|
||||||
|
<< "\" minBufferTime=\"PT1.5S\" ";
|
||||||
}else{
|
}else{
|
||||||
r << "type=\"dynamic\" minimumUpdatePeriod=\"PT2.0S\" availabilityStartTime=\""
|
r << "type=\"dynamic\" minimumUpdatePeriod=\"PT2.0S\" availabilityStartTime=\""
|
||||||
<< Util::getUTCString(Util::epoch() - M.getLastms(mainTrack) / 1000)
|
<< Util::getUTCString(Util::epoch() - M.getLastms(mainTrack) / 1000)
|
||||||
|
@ -501,7 +688,8 @@ namespace Mist{
|
||||||
r << "profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" "
|
r << "profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" "
|
||||||
"xmlns=\"urn:mpeg:dash:schema:mpd:2011\" >"
|
"xmlns=\"urn:mpeg:dash:schema:mpd:2011\" >"
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
r << "<ProgramInformation><Title>" << streamName << "</Title></ProgramInformation>" << std::endl;
|
r << "<ProgramInformation><Title>" << streamName << "</Title></ProgramInformation>"
|
||||||
|
<< std::endl;
|
||||||
r << "<Period " << (M.getLive() ? "start=\"0\"" : "") << ">" << std::endl;
|
r << "<Period " << (M.getLive() ? "start=\"0\"" : "") << ">" << std::endl;
|
||||||
|
|
||||||
dashAdaptation(1, vTracks, videoAligned, r);
|
dashAdaptation(1, vTracks, videoAligned, r);
|
||||||
|
@ -511,8 +699,9 @@ namespace Mist{
|
||||||
for (std::set<size_t>::iterator it = sTracks.begin(); it != sTracks.end(); it++){
|
for (std::set<size_t>::iterator it = sTracks.begin(); it != sTracks.end(); it++){
|
||||||
std::string lang = (M.getLang(*it) == "" ? "unknown" : M.getLang(*it));
|
std::string lang = (M.getLang(*it) == "" ? "unknown" : M.getLang(*it));
|
||||||
r << "<AdaptationSet id=\"" << *it << "\" group=\"3\" mimeType=\"text/vtt\" lang=\"" << lang
|
r << "<AdaptationSet id=\"" << *it << "\" group=\"3\" mimeType=\"text/vtt\" lang=\"" << lang
|
||||||
<< "\"><Representation id=\"" << *it << "\" bandwidth=\"256\"><BaseURL>../../" << streamName
|
<< "\"><Representation id=\"" << *it << "\" bandwidth=\"256\"><BaseURL>../../"
|
||||||
<< ".vtt?track=" << *it << "</BaseURL></Representation></AdaptationSet>" << std::endl;
|
<< streamName << ".vtt?track=" << *it << "</BaseURL></Representation></AdaptationSet>"
|
||||||
|
<< std::endl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -521,147 +710,6 @@ namespace Mist{
|
||||||
return r.str();
|
return r.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************/
|
|
||||||
/* HLS v7 Manifest Generation */
|
|
||||||
/******************************/
|
|
||||||
|
|
||||||
void OutCMAF::sendHlsManifest(size_t idx, const std::string &sessId){
|
|
||||||
std::string method = H.method;
|
|
||||||
H.Clean();
|
|
||||||
// H.SetHeader("Content-Type", "application/vnd.apple.mpegurl");
|
|
||||||
H.SetHeader("Content-Type", "audio/mpegurl");
|
|
||||||
H.SetHeader("Cache-Control", "no-cache");
|
|
||||||
H.setCORSHeaders();
|
|
||||||
if (method == "OPTIONS" || method == "HEAD"){
|
|
||||||
H.SendResponse("200", "OK", myConn);
|
|
||||||
H.Clean();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (idx == INVALID_TRACK_ID){
|
|
||||||
H.SetBody(hlsManifest());
|
|
||||||
}else{
|
|
||||||
H.SetBody(hlsManifest(idx, sessId));
|
|
||||||
}
|
|
||||||
H.SendResponse("200", "OK", myConn);
|
|
||||||
H.Clean();
|
|
||||||
}
|
|
||||||
|
|
||||||
void hlsSegment(uint64_t start, uint64_t duration, std::stringstream &s, bool first){
|
|
||||||
if (bootMsOffset){
|
|
||||||
uint64_t unixMs = start + bootMsOffset + (Util::unixMS() - Util::bootMS());
|
|
||||||
time_t uSecs = unixMs/1000;
|
|
||||||
struct tm * tVal = gmtime(&uSecs);
|
|
||||||
s << "#EXT-X-PROGRAM-DATE-TIME: " << (tVal->tm_year+1900) << "-" << std::setw(2) << std::setfill('0') << (tVal->tm_mon+1) << "-" << std::setw(2) << std::setfill('0') << tVal->tm_mday << "T" << std::setw(2) << std::setfill('0') << tVal->tm_hour << ":" << std::setw(2) << std::setfill('0') << tVal->tm_min << ":" << std::setw(2) << std::setfill('0') << tVal->tm_sec << "." << std::setw(3) << std::setfill('0') << (unixMs%1000) << "Z" << std::endl;
|
|
||||||
}
|
|
||||||
s << "#EXTINF:" << (((double)duration) / 1000) << ",\r\nchunk_" << start << ".m4s" << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
///\brief Builds an index file for HTTP Live streaming.
|
|
||||||
///\return The index file for HTTP Live Streaming.
|
|
||||||
std::string OutCMAF::hlsManifest(){
|
|
||||||
std::stringstream result;
|
|
||||||
result << "#EXTM3U\r\n#EXT-X-VERSION:7\r\n#EXT-X-INDEPENDENT-SEGMENTS\r\n";
|
|
||||||
|
|
||||||
selectDefaultTracks();
|
|
||||||
std::set<size_t> vTracks;
|
|
||||||
std::set<size_t> aTracks;
|
|
||||||
std::set<size_t> sTracks;
|
|
||||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
||||||
if (M.getType(it->first) == "video"){vTracks.insert(it->first);}
|
|
||||||
if (M.getType(it->first) == "audio"){aTracks.insert(it->first);}
|
|
||||||
if (M.getType(it->first) == "subtitle"){sTracks.insert(it->first);}
|
|
||||||
}
|
|
||||||
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); it++){
|
|
||||||
std::string codec = M.getCodec(*it);
|
|
||||||
if (codec == "H264" || codec == "HEVC" || codec == "MPEG2"){
|
|
||||||
int bWidth = M.getBps(*it);
|
|
||||||
if (bWidth < 5){bWidth = 5;}
|
|
||||||
if (aTracks.size()){bWidth += M.getBps(*aTracks.begin());}
|
|
||||||
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (bWidth * 8)
|
|
||||||
<< ",RESOLUTION=" << M.getWidth(*it) << "x" << M.getHeight(*it);
|
|
||||||
if (M.getFpks(*it)){result << ",FRAME-RATE=" << (float)M.getFpks(*it) / 1000;}
|
|
||||||
if (aTracks.size()){result << ",AUDIO=\"aud1\"";}
|
|
||||||
if (sTracks.size()){result << ",SUBTITLES=\"sub1\"";}
|
|
||||||
if (codec == "H264" || codec == "HEVC"){
|
|
||||||
result << ",CODECS=\"";
|
|
||||||
result << Util::codecString(M.getCodec(*it), M.getInit(*it));
|
|
||||||
result << "\"";
|
|
||||||
}
|
|
||||||
result << "\r\n" << *it;
|
|
||||||
if (hasSessionIDs()){
|
|
||||||
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
|
|
||||||
}else{
|
|
||||||
result << "/index.m3u8\r\n";
|
|
||||||
}
|
|
||||||
}else if (codec == "subtitle"){
|
|
||||||
|
|
||||||
if (M.getLang(*it).empty()){meta.setLang(*it, "und");}
|
|
||||||
|
|
||||||
result << "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",LANGUAGE=\"" << M.getLang(*it)
|
|
||||||
<< "\",NAME=\"" << Encodings::ISO639::decode(M.getLang(*it))
|
|
||||||
<< "\",AUTOSELECT=NO,DEFAULT=NO,FORCED=NO,URI=\"" << *it << "/index.m3u8\""
|
|
||||||
<< "\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (std::set<size_t>::iterator it = aTracks.begin(); it != aTracks.end(); it++){
|
|
||||||
if (M.getLang(*it).empty()){meta.setLang(*it, "und");}
|
|
||||||
|
|
||||||
result << "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aud1\",LANGUAGE=\"" << M.getLang(*it)
|
|
||||||
<< "\",NAME=\"" << Encodings::ISO639::decode(M.getLang(*it))
|
|
||||||
<< "\",AUTOSELECT=YES,DEFAULT=YES,URI=\"" << *it << "/index.m3u8\""
|
|
||||||
<< "\r\n";
|
|
||||||
}
|
|
||||||
for (std::set<size_t>::iterator it = sTracks.begin(); it != sTracks.end(); it++){
|
|
||||||
if (M.getLang(*it).empty()){meta.setLang(*it, "und");}
|
|
||||||
|
|
||||||
result << "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",LANGUAGE=\"" << M.getLang(*it)
|
|
||||||
<< "\",NAME=\"" << Encodings::ISO639::decode(M.getLang(*it))
|
|
||||||
<< "\",AUTOSELECT=NO,DEFAULT=NO,FORCED=NO,URI=\"" << *it << "/index.m3u8\""
|
|
||||||
<< "\r\n";
|
|
||||||
}
|
|
||||||
if (aTracks.size() && !vTracks.size()){
|
|
||||||
std::string codec = M.getCodec(*aTracks.begin());
|
|
||||||
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << M.getBps(*aTracks.begin()) * 8;
|
|
||||||
result << ",CODECS=\""
|
|
||||||
<< Util::codecString(M.getCodec(*aTracks.begin()), M.getInit(*aTracks.begin())) << "\"\r\n";
|
|
||||||
result << *aTracks.begin() << "/index.m3u8\r\n";
|
|
||||||
}
|
|
||||||
HIGH_MSG("Sending this index: %s", result.str().c_str());
|
|
||||||
return result.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string OutCMAF::hlsManifest(size_t idx, const std::string &sessId){
|
|
||||||
std::stringstream result;
|
|
||||||
// parse single track
|
|
||||||
uint32_t targetDuration = (M.biggestFragment(idx) / 1000) + 1;
|
|
||||||
|
|
||||||
DTSC::Fragments fragments(M.fragments(idx));
|
|
||||||
uint32_t firstFragment = fragments.getFirstValid();
|
|
||||||
uint32_t endFragment = fragments.getEndValid();
|
|
||||||
// skip the first two fragments if live
|
|
||||||
if (M.getLive() && (endFragment - firstFragment) > 6){firstFragment += 2;}
|
|
||||||
if (M.getType(idx) == "audio"){
|
|
||||||
uint32_t mainTrack = M.mainTrack();
|
|
||||||
if (mainTrack == INVALID_TRACK_ID){return "";}
|
|
||||||
DTSC::Fragments f(M.fragments(mainTrack));
|
|
||||||
uint64_t firstVidTime = M.getTimeForFragmentIndex(mainTrack, f.getFirstValid());
|
|
||||||
firstFragment = M.getFragmentIndexForTime(idx, firstVidTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
result << "#EXTM3U\r\n"
|
|
||||||
"#EXT-X-VERSION:7\r\n"
|
|
||||||
"#EXT-X-TARGETDURATION:"
|
|
||||||
<< targetDuration << "\r\n";
|
|
||||||
if (M.getLive()){result << "#EXT-X-MEDIA-SEQUENCE:" << firstFragment << "\r\n";}
|
|
||||||
result << "#EXT-X-MAP:URI=\"init.m4s"
|
|
||||||
<< "\"\r\n";
|
|
||||||
|
|
||||||
generateSegmentlist(idx, result, hlsSegment);
|
|
||||||
|
|
||||||
if (M.getVod()){result << "#EXT-X-ENDLIST\r\n";}
|
|
||||||
return result.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
/****************************************/
|
/****************************************/
|
||||||
/* Smooth Streaming Manifest Generation */
|
/* Smooth Streaming Manifest Generation */
|
||||||
/****************************************/
|
/****************************************/
|
||||||
|
@ -676,8 +724,9 @@ namespace Mist{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts bytes per second and track ID into a single bits per second value, where the last two
|
/// Converts bytes per second and track ID into a single bits per second value, where the last
|
||||||
/// digits are the track ID. Breaks for track IDs > 99. But really, this is MS-SS, so who cares..?
|
/// two digits are the track ID. Breaks for track IDs > 99. But really, this is MS-SS, so who
|
||||||
|
/// cares..?
|
||||||
uint64_t bpsAndIdToBitrate(uint32_t bps, uint64_t tid){
|
uint64_t bpsAndIdToBitrate(uint32_t bps, uint64_t tid){
|
||||||
return ((uint64_t)((bps * 8) / 100)) * 100 + tid;
|
return ((uint64_t)((bps * 8) / 100)) * 100 + tid;
|
||||||
}
|
}
|
||||||
|
@ -692,7 +741,7 @@ namespace Mist{
|
||||||
std::string method = H.method;
|
std::string method = H.method;
|
||||||
H.Clean();
|
H.Clean();
|
||||||
H.SetHeader("Content-Type", "application/dash+xml");
|
H.SetHeader("Content-Type", "application/dash+xml");
|
||||||
H.SetHeader("Cache-Control", "no-cache");
|
H.SetHeader("Cache-Control", "no-store");
|
||||||
H.setCORSHeaders();
|
H.setCORSHeaders();
|
||||||
if (method == "OPTIONS" || method == "HEAD"){
|
if (method == "OPTIONS" || method == "HEAD"){
|
||||||
H.SendResponse("200", "OK", myConn);
|
H.SendResponse("200", "OK", myConn);
|
||||||
|
@ -704,7 +753,8 @@ namespace Mist{
|
||||||
H.Clean();
|
H.Clean();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutCMAF::smoothAdaptation(const std::string &type, std::set<size_t> tracks, std::stringstream &r){
|
void OutCMAF::smoothAdaptation(const std::string &type, std::set<size_t> tracks,
|
||||||
|
std::stringstream &r){
|
||||||
if (!tracks.size()){return;}
|
if (!tracks.size()){return;}
|
||||||
DTSC::Keys keys(M.keys(*tracks.begin()));
|
DTSC::Keys keys(M.keys(*tracks.begin()));
|
||||||
r << "<StreamIndex Type=\"" << type << "\" QualityLevels=\"" << tracks.size() << "\" Name=\""
|
r << "<StreamIndex Type=\"" << type << "\" QualityLevels=\"" << tracks.size() << "\" Name=\""
|
||||||
|
@ -763,7 +813,8 @@ namespace Mist{
|
||||||
selectDefaultTracks();
|
selectDefaultTracks();
|
||||||
std::set<size_t> vTracks;
|
std::set<size_t> vTracks;
|
||||||
std::set<size_t> aTracks;
|
std::set<size_t> aTracks;
|
||||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end();
|
||||||
|
it++){
|
||||||
if (M.getType(it->first) == "video"){vTracks.insert(it->first);}
|
if (M.getType(it->first) == "video"){vTracks.insert(it->first);}
|
||||||
if (M.getType(it->first) == "audio"){aTracks.insert(it->first);}
|
if (M.getType(it->first) == "audio"){aTracks.insert(it->first);}
|
||||||
}
|
}
|
||||||
|
@ -774,7 +825,8 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
if (M.getVod()){
|
if (M.getVod()){
|
||||||
r << "Duration=\"" << M.getLastms(vTracks.size() ? *vTracks.begin() : *aTracks.begin()) << "\">\n";
|
r << "Duration=\"" << M.getLastms(vTracks.size() ? *vTracks.begin() : *aTracks.begin())
|
||||||
|
<< "\">\n";
|
||||||
}else{
|
}else{
|
||||||
r << "Duration=\"0\" IsLive=\"TRUE\" LookAheadFragmentCount=\"2\" DVRWindowLength=\""
|
r << "Duration=\"0\" IsLive=\"TRUE\" LookAheadFragmentCount=\"2\" DVRWindowLength=\""
|
||||||
<< M.getBufferWindow() << "\" CanSeek=\"TRUE\" CanPause=\"TRUE\">\n";
|
<< M.getBufferWindow() << "\" CanSeek=\"TRUE\" CanPause=\"TRUE\">\n";
|
||||||
|
@ -791,7 +843,8 @@ namespace Mist{
|
||||||
/* CMAF Push Output functionality */
|
/* CMAF Push Output functionality */
|
||||||
/**********************************/
|
/**********************************/
|
||||||
|
|
||||||
//When we disconnect a track, or when we're done pushing out, send an empty 'mfra' box to indicate track end.
|
// When we disconnect a track, or when we're done pushing out, send an empty 'mfra' box to
|
||||||
|
// indicate track end.
|
||||||
void OutCMAF::onTrackEnd(size_t idx){
|
void OutCMAF::onTrackEnd(size_t idx){
|
||||||
if (!isRecording()){return;}
|
if (!isRecording()){return;}
|
||||||
if (!pushTracks.count(idx) || !pushTracks.at(idx).D.getSocket()){return;}
|
if (!pushTracks.count(idx) || !pushTracks.at(idx).D.getSocket()){return;}
|
||||||
|
@ -818,23 +871,28 @@ namespace Mist{
|
||||||
track.send(header);
|
track.send(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Function that waits at most `maxWait` ms (in steps of 100ms) for the next keyframe to become
|
||||||
/// Function that waits at most `maxWait` ms (in steps of 100ms) for the next keyframe to become available.
|
/// available. Uses thisIdx and thisPacket to determine track and current timestamp
|
||||||
/// Uses thisIdx and thisPacket to determine track and current timestamp respectively.
|
/// respectively.
|
||||||
bool OutCMAF::waitForNextKey(uint64_t maxWait){
|
bool OutCMAF::waitForNextKey(uint64_t maxWait){
|
||||||
uint64_t mTrk = getMainSelectedTrack();
|
uint64_t mTrk = getMainSelectedTrack();
|
||||||
size_t currentKey = M.getKeyIndexForTime(mTrk, thisTime);
|
size_t currentKey = M.getKeyIndexForTime(mTrk, thisTime);
|
||||||
uint64_t startTime = Util::bootMS();
|
uint64_t startTime = Util::bootMS();
|
||||||
DTSC::Keys keys(M.keys(mTrk));
|
DTSC::Keys keys(M.keys(mTrk));
|
||||||
while (startTime + maxWait > Util::bootMS() && keepGoing()){
|
while (startTime + maxWait > Util::bootMS() && keepGoing()){
|
||||||
if (keys.getEndValid() > currentKey + 1 && M.getLastms(thisIdx) >= M.getTimeForKeyIndex(mTrk, currentKey+1)){
|
if (keys.getEndValid() > currentKey + 1 &&
|
||||||
|
M.getLastms(thisIdx) >= M.getTimeForKeyIndex(mTrk, currentKey + 1)){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Util::sleep(20);
|
Util::sleep(20);
|
||||||
meta.reloadReplacedPagesIfNeeded();
|
meta.reloadReplacedPagesIfNeeded();
|
||||||
}
|
}
|
||||||
INFO_MSG("Timed out waiting for next key (track %" PRIu64 ", %zu+1, last is %zu, time is %" PRIu64 ")", mTrk, currentKey, keys.getEndValid()-1, M.getTimeForKeyIndex(getMainSelectedTrack(), currentKey+1));
|
INFO_MSG("Timed out waiting for next key (track %" PRIu64
|
||||||
return (keys.getEndValid() > currentKey + 1 && M.getLastms(thisIdx) >= M.getTimeForKeyIndex(mTrk, currentKey+1));
|
", %zu+1, last is %zu, time is %" PRIu64 ")",
|
||||||
|
mTrk, currentKey, keys.getEndValid() - 1,
|
||||||
|
M.getTimeForKeyIndex(getMainSelectedTrack(), currentKey + 1));
|
||||||
|
return (keys.getEndValid() > currentKey + 1 &&
|
||||||
|
M.getLastms(thisIdx) >= M.getTimeForKeyIndex(mTrk, currentKey + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up an empty connection to the target to make sure we can push data towards it.
|
// Set up an empty connection to the target to make sure we can push data towards it.
|
||||||
|
@ -846,12 +904,15 @@ namespace Mist{
|
||||||
parseData = true;
|
parseData = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
//CMAF Push output uses keyframe boundaries instead of fragment boundaries, to allow for lower latency
|
// CMAF Push output uses keyframe boundaries instead of fragment boundaries, to allow for lower
|
||||||
|
// latency
|
||||||
void OutCMAF::pushNext(){
|
void OutCMAF::pushNext(){
|
||||||
size_t mTrk = getMainSelectedTrack();
|
size_t mTrk = getMainSelectedTrack();
|
||||||
// Set up a new connection if this is a new track, or if we have been disconnected.
|
// Set up a new connection if this is a new track, or if we have been disconnected.
|
||||||
if (!pushTracks.count(thisIdx) || !pushTracks.at(thisIdx).D.getSocket()){
|
if (!pushTracks.count(thisIdx) || !pushTracks.at(thisIdx).D.getSocket()){
|
||||||
if (pushTracks.count(thisIdx)){INFO_MSG("Reconnecting existing track: socket was disconnected");}
|
if (pushTracks.count(thisIdx)){
|
||||||
|
INFO_MSG("Reconnecting existing track: socket was disconnected");
|
||||||
|
}
|
||||||
CMAFPushTrack &track = pushTracks[thisIdx];
|
CMAFPushTrack &track = pushTracks[thisIdx];
|
||||||
size_t keyIndex = M.getKeyIndexForTime(mTrk, thisPacket.getTime());
|
size_t keyIndex = M.getKeyIndexForTime(mTrk, thisPacket.getTime());
|
||||||
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex);
|
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex);
|
||||||
|
@ -859,11 +920,12 @@ namespace Mist{
|
||||||
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
INFO_MSG("Starting track %zu at %" PRIu64 "ms into the stream, current packet at %" PRIu64 "ms", thisIdx, track.headerFrom, thisPacket.getTime());
|
INFO_MSG("Starting track %zu at %" PRIu64 "ms into the stream, current packet at %" PRIu64
|
||||||
|
"ms",
|
||||||
|
thisIdx, track.headerFrom, thisPacket.getTime());
|
||||||
|
|
||||||
setupTrackObject(thisIdx);
|
setupTrackObject(thisIdx);
|
||||||
track.headerUntil = 0;
|
track.headerUntil = 0;
|
||||||
|
|
||||||
}
|
}
|
||||||
CMAFPushTrack &track = pushTracks[thisIdx];
|
CMAFPushTrack &track = pushTracks[thisIdx];
|
||||||
if (thisPacket.getTime() < track.headerFrom){return;}
|
if (thisPacket.getTime() < track.headerFrom){return;}
|
||||||
|
@ -873,7 +935,9 @@ namespace Mist{
|
||||||
if (keyTime > thisTime){
|
if (keyTime > thisTime){
|
||||||
realTime = 1000;
|
realTime = 1000;
|
||||||
if (!liveSeek()){
|
if (!liveSeek()){
|
||||||
WARN_MSG("Corruption probably occurred, initiating reconnect. Key %zu is time %" PRIu64 ", but packet is time %" PRIu64, keyIndex, keyTime, thisTime);
|
WARN_MSG("Corruption probably occurred, initiating reconnect. Key %zu is time %" PRIu64
|
||||||
|
", but packet is time %" PRIu64,
|
||||||
|
keyIndex, keyTime, thisTime);
|
||||||
onTrackEnd(thisIdx);
|
onTrackEnd(thisIdx);
|
||||||
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
track.headerFrom = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
||||||
track.headerUntil = 0;
|
track.headerUntil = 0;
|
||||||
|
@ -889,7 +953,8 @@ namespace Mist{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
track.headerUntil = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
track.headerUntil = M.getTimeForKeyIndex(mTrk, keyIndex + 1);
|
||||||
std::string keyHeader = CMAF::keyHeader(M, thisIdx, track.headerFrom, track.headerUntil, keyIndex+1, true, true);
|
std::string keyHeader = CMAF::keyHeader(M, thisIdx, track.headerFrom, track.headerUntil,
|
||||||
|
keyIndex + 1, true, true);
|
||||||
uint64_t mdatSize = 8 + CMAF::payloadSize(M, thisIdx, track.headerFrom, track.headerUntil);
|
uint64_t mdatSize = 8 + CMAF::payloadSize(M, thisIdx, track.headerFrom, track.headerUntil);
|
||||||
char mdatHeader[] ={0x00, 0x00, 0x00, 0x00, 'm', 'd', 'a', 't'};
|
char mdatHeader[] ={0x00, 0x00, 0x00, 0x00, 'm', 'd', 'a', 't'};
|
||||||
Bit::htobl(mdatHeader, mdatSize);
|
Bit::htobl(mdatHeader, mdatSize);
|
||||||
|
@ -904,5 +969,4 @@ namespace Mist{
|
||||||
track.send(data, dataLen);
|
track.send(data, dataLen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}// namespace Mist
|
}// namespace Mist
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
#include "output_http.h"
|
#include "output_http.h"
|
||||||
#include <mist/http_parser.h>
|
|
||||||
#include <mist/downloader.h>
|
#include <mist/downloader.h>
|
||||||
#include <mist/mp4_generic.h>
|
#include <mist/http_parser.h>
|
||||||
|
// #include <mist/mp4_generic.h>
|
||||||
|
|
||||||
namespace Mist{
|
namespace Mist{
|
||||||
/// Keeps track of the state of an outgoing CMAF Push track.
|
/// Keeps track of the state of an outgoing CMAF Push track.
|
||||||
class CMAFPushTrack{
|
class CMAFPushTrack{
|
||||||
public:
|
public:
|
||||||
CMAFPushTrack() {debug = false; debugFile = 0;}
|
CMAFPushTrack(){
|
||||||
|
debug = false;
|
||||||
|
debugFile = 0;
|
||||||
|
}
|
||||||
~CMAFPushTrack(){disconnect();}
|
~CMAFPushTrack(){disconnect();}
|
||||||
void connect(std::string debugParam = "");
|
void connect(std::string debugParam = "");
|
||||||
void disconnect();
|
void disconnect();
|
||||||
|
@ -33,10 +36,12 @@ namespace Mist{
|
||||||
void onHTTP();
|
void onHTTP();
|
||||||
void sendNext();
|
void sendNext();
|
||||||
void sendHeader(){};
|
void sendHeader(){};
|
||||||
|
bool isReadyForPlay();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void connStats(uint64_t now, Comms::Statistics &statComm);
|
virtual void connStats(uint64_t now, Comms::Statistics &statComm);
|
||||||
void onTrackEnd(size_t idx);
|
void onTrackEnd(size_t idx);
|
||||||
|
bool hasSessionIDs(){return !config->getBool("mergesessions");}
|
||||||
|
|
||||||
void sendDashManifest();
|
void sendDashManifest();
|
||||||
void dashAdaptationSet(size_t id, size_t idx, std::stringstream &r);
|
void dashAdaptationSet(size_t id, size_t idx, std::stringstream &r);
|
||||||
|
@ -46,9 +51,9 @@ namespace Mist{
|
||||||
std::string dashTime(uint64_t time);
|
std::string dashTime(uint64_t time);
|
||||||
std::string dashManifest(bool checkAlignment = true);
|
std::string dashManifest(bool checkAlignment = true);
|
||||||
|
|
||||||
void sendHlsManifest(size_t idx = INVALID_TRACK_ID, const std::string &sessId = "");
|
void sendHlsManifest(const std::string url);
|
||||||
std::string hlsManifest();
|
void sendHlsMasterManifest();
|
||||||
std::string hlsManifest(size_t idx, const std::string &sessId);
|
void sendHlsMediaManifest(const size_t requestTid);
|
||||||
|
|
||||||
void sendSmoothManifest();
|
void sendSmoothManifest();
|
||||||
std::string smoothManifest(bool checkAlignment = true);
|
std::string smoothManifest(bool checkAlignment = true);
|
||||||
|
|
Loading…
Add table
Reference in a new issue