MP4/WS protocol support.
Approx. 10% of code originally written by Roxlu, but keeping it split up during cleanup before merge proved practically impossible, so it's all merged into a single commit.
This commit is contained in:
parent
6276d03522
commit
9417fa8dc2
3 changed files with 564 additions and 16 deletions
|
@ -1243,10 +1243,11 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
stats();
|
stats();
|
||||||
}
|
}
|
||||||
|
if (!thisPacket){continue;}
|
||||||
}
|
}
|
||||||
|
|
||||||
// delay the stream until metadata has caught up, if needed
|
// delay the stream until metadata has caught up, if needed
|
||||||
if (needsLookAhead){
|
if (needsLookAhead && M.getLive()){
|
||||||
// we sleep in 20ms increments, or less if the lookahead time itself is less
|
// we sleep in 20ms increments, or less if the lookahead time itself is less
|
||||||
uint32_t sleepTime = std::min(20ul, needsLookAhead);
|
uint32_t sleepTime = std::min(20ul, needsLookAhead);
|
||||||
// wait at most double the look ahead time, plus ten seconds
|
// wait at most double the look ahead time, plus ten seconds
|
||||||
|
|
|
@ -7,8 +7,13 @@
|
||||||
#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> /* for `Util::codecString()` when streaming mp4 over websockets and playback using media source extensions. */
|
||||||
|
#include <mist/nal.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
std::set<std::string> supportedAudio;
|
||||||
|
std::set<std::string> supportedVideo;
|
||||||
|
|
||||||
namespace Mist{
|
namespace Mist{
|
||||||
std::string toUTF16(const std::string &original){
|
std::string toUTF16(const std::string &original){
|
||||||
|
@ -101,10 +106,14 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
OutMP4::OutMP4(Socket::Connection &conn) : HTTPOutput(conn){
|
OutMP4::OutMP4(Socket::Connection &conn) : HTTPOutput(conn){
|
||||||
|
prevVidTrack = INVALID_TRACK_ID;
|
||||||
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(){}
|
||||||
|
|
||||||
|
@ -113,7 +122,6 @@ namespace Mist{
|
||||||
capa["name"] = "MP4";
|
capa["name"] = "MP4";
|
||||||
capa["friendly"] = "MP4 over HTTP";
|
capa["friendly"] = "MP4 over HTTP";
|
||||||
capa["desc"] = "Pseudostreaming in MP4 format over HTTP";
|
capa["desc"] = "Pseudostreaming in MP4 format over HTTP";
|
||||||
capa["url_rel"] = "/$.mp4";
|
|
||||||
capa["url_match"][0u] = "/$.mp4";
|
capa["url_match"][0u] = "/$.mp4";
|
||||||
capa["url_match"][1u] = "/$.3gp";
|
capa["url_match"][1u] = "/$.3gp";
|
||||||
capa["url_match"][2u] = "/$.fmp4";
|
capa["url_match"][2u] = "/$.fmp4";
|
||||||
|
@ -122,9 +130,16 @@ namespace Mist{
|
||||||
capa["codecs"][0u][1u].append("AAC");
|
capa["codecs"][0u][1u].append("AAC");
|
||||||
capa["codecs"][0u][1u].append("MP3");
|
capa["codecs"][0u][1u].append("MP3");
|
||||||
capa["codecs"][0u][1u].append("AC3");
|
capa["codecs"][0u][1u].append("AC3");
|
||||||
|
jsonForEach(capa["codecs"][0u][0u], i){supportedVideo.insert(i->asStringRef());}
|
||||||
|
jsonForEach(capa["codecs"][0u][1u], i){supportedAudio.insert(i->asStringRef());}
|
||||||
capa["methods"][0u]["handler"] = "http";
|
capa["methods"][0u]["handler"] = "http";
|
||||||
capa["methods"][0u]["type"] = "html5/video/mp4";
|
capa["methods"][0u]["type"] = "html5/video/mp4";
|
||||||
capa["methods"][0u]["priority"] = 10u;
|
capa["methods"][0u]["priority"] = 9;
|
||||||
|
capa["methods"][0u]["url_rel"] = "/$.mp4";
|
||||||
|
capa["methods"][1u]["handler"] = "ws";
|
||||||
|
capa["methods"][1u]["type"] = "ws/video/mp4";
|
||||||
|
capa["methods"][1u]["priority"] = 10;
|
||||||
|
capa["methods"][1u]["url_rel"] = "/$.mp4";
|
||||||
// MP4 live is broken on Apple
|
// MP4 live is broken on Apple
|
||||||
capa["exceptions"]["live"] =
|
capa["exceptions"]["live"] =
|
||||||
JSON::fromString("[[\"blacklist\",[\"iPad\",\"iPhone\",\"iPod\",\"Safari\"]], "
|
JSON::fromString("[[\"blacklist\",[\"iPad\",\"iPhone\",\"iPod\",\"Safari\"]], "
|
||||||
|
@ -336,7 +351,8 @@ namespace Mist{
|
||||||
bool OutMP4::mp4Header(Util::ResizeablePointer & headOut, uint64_t &size, int fragmented){
|
bool OutMP4::mp4Header(Util::ResizeablePointer & headOut, uint64_t &size, int fragmented){
|
||||||
uint32_t mainTrack = M.mainTrack();
|
uint32_t mainTrack = M.mainTrack();
|
||||||
if (mainTrack == INVALID_TRACK_ID){return false;}
|
if (mainTrack == INVALID_TRACK_ID){return false;}
|
||||||
if (M.getLive()){needsLookAhead = 100;}
|
if (M.getLive()){needsLookAhead = 5000;}
|
||||||
|
if (webSock){needsLookAhead = 0;}
|
||||||
// Clear size if it was set before the function was called, just in case
|
// Clear size if it was set before the function was called, just in case
|
||||||
size = 0;
|
size = 0;
|
||||||
// Determines whether the outputfile is larger than 4GB, in which case we need to use 64-bit
|
// Determines whether the outputfile is larger than 4GB, in which case we need to use 64-bit
|
||||||
|
@ -368,6 +384,7 @@ namespace Mist{
|
||||||
uint64_t lastms = 0;
|
uint64_t lastms = 0;
|
||||||
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
||||||
it != userSelect.end(); it++){
|
it != userSelect.end(); it++){
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;}
|
||||||
lastms = std::max(lastms, M.getLastms(it->first));
|
lastms = std::max(lastms, M.getLastms(it->first));
|
||||||
firstms = std::min(firstms, M.getFirstms(it->first));
|
firstms = std::min(firstms, M.getFirstms(it->first));
|
||||||
}
|
}
|
||||||
|
@ -378,6 +395,7 @@ namespace Mist{
|
||||||
moovBox.setContent(mvhdBox, moovOffset++);
|
moovBox.setContent(mvhdBox, moovOffset++);
|
||||||
|
|
||||||
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;}
|
||||||
DTSC::Parts parts(M.parts(it->first));
|
DTSC::Parts parts(M.parts(it->first));
|
||||||
size_t partCount = parts.getValidCount();
|
size_t partCount = parts.getValidCount();
|
||||||
uint64_t tDuration = M.getLastms(it->first) - M.getFirstms(it->first);
|
uint64_t tDuration = M.getLastms(it->first) - M.getFirstms(it->first);
|
||||||
|
@ -646,6 +664,7 @@ namespace Mist{
|
||||||
mvexBox.setContent(mehdBox, curBox++);
|
mvexBox.setContent(mehdBox, curBox++);
|
||||||
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
||||||
it != userSelect.end(); it++){
|
it != userSelect.end(); it++){
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;}
|
||||||
MP4::TREX trexBox(it->first + 1);
|
MP4::TREX trexBox(it->first + 1);
|
||||||
trexBox.setDefaultSampleDuration(1000);
|
trexBox.setDefaultSampleDuration(1000);
|
||||||
mvexBox.setContent(trexBox, curBox++);
|
mvexBox.setContent(trexBox, curBox++);
|
||||||
|
@ -653,6 +672,7 @@ namespace Mist{
|
||||||
moovBox.setContent(mvexBox, moovOffset++);
|
moovBox.setContent(mvexBox, moovOffset++);
|
||||||
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
for (std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
||||||
it != userSelect.end(); it++){
|
it != userSelect.end(); it++){
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;}
|
||||||
if (M.getEncryption(it->first) != ""){
|
if (M.getEncryption(it->first) != ""){
|
||||||
MP4::PSSH psshBox;
|
MP4::PSSH psshBox;
|
||||||
psshBox.setSystemIDHex(Encodings::Hex::decode("9a04f07998404286ab92e65be0885f95"));
|
psshBox.setSystemIDHex(Encodings::Hex::decode("9a04f07998404286ab92e65be0885f95"));
|
||||||
|
@ -691,6 +711,7 @@ namespace Mist{
|
||||||
SortSet sortSet; // filling sortset for interleaving parts
|
SortSet sortSet; // filling sortset for interleaving parts
|
||||||
for (std::map<size_t, Comms::Users>::const_iterator subIt = userSelect.begin();
|
for (std::map<size_t, Comms::Users>::const_iterator subIt = userSelect.begin();
|
||||||
subIt != userSelect.end(); subIt++){
|
subIt != userSelect.end(); subIt++){
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && subIt->first == prevVidTrack){continue;}
|
||||||
keyPart temp;
|
keyPart temp;
|
||||||
temp.trackID = subIt->first;
|
temp.trackID = subIt->first;
|
||||||
temp.time = M.getFirstms(subIt->first);
|
temp.time = M.getFirstms(subIt->first);
|
||||||
|
@ -784,6 +805,103 @@ namespace Mist{
|
||||||
// That's technically legal, of course.
|
// That's technically legal, of course.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
size_t OutMP4::fragmentHeaderSize(std::deque<size_t>& sortedTracks, std::set<keyPart>& trunOrder, uint64_t startFragmentTime, uint64_t endFragmentTime) {
|
||||||
|
/*
|
||||||
|
8 = moof (once)
|
||||||
|
16 = mfhd (once)
|
||||||
|
|
||||||
|
per track:
|
||||||
|
32 = tfhd + first 8 bytes of traf
|
||||||
|
20 = tfdt
|
||||||
|
24 = 24 * ...
|
||||||
|
*/
|
||||||
|
size_t ret = (8 + 16) + ((32 + 20 + 24) * sortedTracks.size()) + 12 * trunOrder.size();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function was created to add support for streaming mp4
|
||||||
|
// over websockets. each time `sendNext()` is called we will
|
||||||
|
// wrap the data into a `moof` packet and send it to the
|
||||||
|
// webocket client.
|
||||||
|
void OutMP4::appendSinglePacketMoof(Util::ResizeablePointer& moofOut, size_t extraBytes){
|
||||||
|
|
||||||
|
/*
|
||||||
|
roxlu: I've added this check as this resulted in a
|
||||||
|
segfault while working on the websocket api. This
|
||||||
|
shouldn't be necessary as `sendNext()` should not be
|
||||||
|
called when `thisPacket` is invalid. Though, having this
|
||||||
|
here won't hurt and prevents us from running into
|
||||||
|
segfaults.
|
||||||
|
*/
|
||||||
|
if (!thisPacket.getData()) {
|
||||||
|
FAIL_MSG("Current packet has no data, lookahead: %lu.", needsLookAhead);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//INFO_MSG("-- thisPacket.getDataStrignLen(): %u", thisPacket.getDataStringLen());
|
||||||
|
//INFO_MSG("-- appendSinglePacketMoof");
|
||||||
|
|
||||||
|
MP4::MOOF moofBox;
|
||||||
|
MP4::MFHD mfhdBox(fragSeqNum++);
|
||||||
|
moofBox.setContent(mfhdBox, 0);
|
||||||
|
|
||||||
|
MP4::TRAF trafBox;
|
||||||
|
MP4::TFHD tfhdBox;
|
||||||
|
size_t track = thisIdx;
|
||||||
|
|
||||||
|
tfhdBox.setFlags(MP4::tfhdSampleFlag | MP4::tfhdBaseIsMoof | MP4::tfhdSampleDesc);
|
||||||
|
tfhdBox.setTrackID(track + 1);
|
||||||
|
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;
|
||||||
|
tfdtBox.setBaseMediaDecodeTime(thisPacket.getTime());
|
||||||
|
trafBox.setContent(tfdtBox, 1);
|
||||||
|
|
||||||
|
MP4::TRUN trunBox;
|
||||||
|
trunBox.setFirstSampleFlags(MP4::isIPicture | MP4::isKeySample);
|
||||||
|
trunBox.setFlags(MP4::trundataOffset | MP4::trunfirstSampleFlags | MP4::trunsampleSize |
|
||||||
|
MP4::trunsampleDuration | MP4::trunsampleOffsets);
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
8 = moof (once)
|
||||||
|
16 = mfhd (once)
|
||||||
|
|
||||||
|
per track:
|
||||||
|
32 = tfhd + first 8 bytes of traf
|
||||||
|
20 = tfdt
|
||||||
|
24 = 24 * ...
|
||||||
|
*/
|
||||||
|
trunBox.setDataOffset(8 + (8 + 16) + ((32 + 20 + 24)) + 12);
|
||||||
|
|
||||||
|
MP4::trunSampleInformation sampleInfo;
|
||||||
|
|
||||||
|
size_t part_idx = M.getPartIndex(thisPacket.getTime(), thisIdx);
|
||||||
|
DTSC::Parts parts(M.parts(thisIdx));
|
||||||
|
sampleInfo.sampleDuration = parts.getDuration(part_idx);
|
||||||
|
|
||||||
|
sampleInfo.sampleOffset = 0;
|
||||||
|
if (thisPacket.hasMember("offset")) {
|
||||||
|
sampleInfo.sampleOffset = thisPacket.getInt("offset");
|
||||||
|
}
|
||||||
|
|
||||||
|
sampleInfo.sampleSize = thisPacket.getDataStringLen()+extraBytes;
|
||||||
|
|
||||||
|
trunBox.setSampleInformation(sampleInfo, 0);
|
||||||
|
trafBox.setContent(trunBox, 2);
|
||||||
|
moofBox.setContent(trafBox, 1);
|
||||||
|
moofOut.append(moofBox.asBox(), moofBox.boxedSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
void OutMP4::sendFragmentHeaderTime(uint64_t startFragmentTime, uint64_t endFragmentTime){
|
void OutMP4::sendFragmentHeaderTime(uint64_t startFragmentTime, uint64_t endFragmentTime){
|
||||||
bool hasAudio = false;
|
bool hasAudio = false;
|
||||||
uint64_t mdatSize = 0;
|
uint64_t mdatSize = 0;
|
||||||
|
@ -942,13 +1060,12 @@ namespace Mist{
|
||||||
H.Chunkify(mdatHeader, 8, myConn);
|
H.Chunkify(mdatHeader, 8, myConn);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OutMP4::onFinish(){
|
|
||||||
H.Chunkify(0, 0, myConn);
|
|
||||||
wantRequest = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OutMP4::onHTTP(){
|
void OutMP4::onHTTP(){
|
||||||
|
|
||||||
|
if (webSock) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string dl;
|
std::string dl;
|
||||||
if (H.GetVar("dl").size()){
|
if (H.GetVar("dl").size()){
|
||||||
dl = H.GetVar("dl");
|
dl = H.GetVar("dl");
|
||||||
|
@ -1130,12 +1247,96 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutMP4::sendNext(){
|
void OutMP4::sendNext(){
|
||||||
static bool perfect = true;
|
|
||||||
|
|
||||||
|
if (!thisPacket.getData()) {
|
||||||
|
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;
|
||||||
thisPacket.getString("data", dataPointer, len);
|
thisPacket.getString("data", dataPointer, len);
|
||||||
|
|
||||||
|
// WebSockets send each packet directly. The packet is constructed in `appendSinglePacketMoof()`.
|
||||||
|
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();
|
||||||
|
sendWebsocketCodecData("tracks");
|
||||||
|
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);
|
||||||
|
appendSinglePacketMoof(webBuf);
|
||||||
|
|
||||||
|
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);
|
||||||
|
webSock->sendFrame(webBuf, webBuf.size(), 2);
|
||||||
|
|
||||||
|
if (stayLive && thisPacket.getFlag("keyframe")){liveSeek();}
|
||||||
|
// We must return here, the rest of this function won't work for websockets.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string subtitle;
|
std::string subtitle;
|
||||||
|
|
||||||
if (M.getLive()){
|
if (M.getLive()){
|
||||||
|
@ -1167,11 +1368,18 @@ namespace Mist{
|
||||||
|
|
||||||
// generate content in mdat, meaning: send right parts
|
// generate content in mdat, meaning: send right parts
|
||||||
DONTEVEN_MSG("Sending tid: %zu size: %zu", thisIdx, len);
|
DONTEVEN_MSG("Sending tid: %zu size: %zu", thisIdx, len);
|
||||||
H.Chunkify(dataPointer, len, myConn);
|
if (webSock) {
|
||||||
|
/* create packet */
|
||||||
|
webBuf.append(dataPointer, len);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
H.Chunkify(dataPointer, len, myConn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keyPart firstKeyPart = *sortSet.begin();
|
keyPart firstKeyPart = *sortSet.begin();
|
||||||
DTSC::Parts parts(M.parts(firstKeyPart.trackID));
|
DTSC::Parts parts(M.parts(firstKeyPart.trackID));
|
||||||
|
/*
|
||||||
if (thisIdx != firstKeyPart.trackID || thisPacket.getTime() != firstKeyPart.time ||
|
if (thisIdx != firstKeyPart.trackID || thisPacket.getTime() != firstKeyPart.time ||
|
||||||
len != parts.getSize(firstKeyPart.index)){
|
len != parts.getSize(firstKeyPart.index)){
|
||||||
if (thisPacket.getTime() > firstKeyPart.time || thisIdx > firstKeyPart.trackID){
|
if (thisPacket.getTime() > firstKeyPart.time || thisIdx > firstKeyPart.trackID){
|
||||||
|
@ -1191,6 +1399,7 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// The remainder of this function handles non-live situations
|
// The remainder of this function handles non-live situations
|
||||||
if (M.getLive()){
|
if (M.getLive()){
|
||||||
|
@ -1235,6 +1444,31 @@ namespace Mist{
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutMP4::sendHeader(){
|
void OutMP4::sendHeader(){
|
||||||
|
|
||||||
|
if (webSock) {
|
||||||
|
|
||||||
|
JSON::Value r;
|
||||||
|
r["type"] = "info";
|
||||||
|
r["data"]["msg"] = "Sending header";
|
||||||
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||||
|
r["data"]["tracks"].append(it->first);
|
||||||
|
}
|
||||||
|
webSock->sendFrame(r.toString());
|
||||||
|
|
||||||
|
Util::ResizeablePointer headerData;
|
||||||
|
if (!mp4Header(headerData, fileSize, 1)){
|
||||||
|
FAIL_MSG("Could not generate MP4 header!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
webSock->sendFrame(headerData, headerData.size(), 2);
|
||||||
|
std::ofstream bleh("/tmp/bleh.mp4");
|
||||||
|
bleh.write(headerData, headerData.size());
|
||||||
|
bleh.close();
|
||||||
|
sentHeader = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
vidTrack = getMainSelectedTrack();
|
vidTrack = getMainSelectedTrack();
|
||||||
|
|
||||||
if (M.getLive()){
|
if (M.getLive()){
|
||||||
|
@ -1260,4 +1494,301 @@ namespace Mist{
|
||||||
sentHeader = true;
|
sentHeader = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OutMP4::onWebsocketConnect() {
|
||||||
|
capa["name"] = "MP4/WS";
|
||||||
|
fragSeqNum = 0;
|
||||||
|
idleInterval = 1000;
|
||||||
|
maxSkipAhead = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutMP4::onWebsocketFrame() {
|
||||||
|
|
||||||
|
JSON::Value command = JSON::fromString(webSock->data, webSock->data.size());
|
||||||
|
if (!command.isMember("type")) {
|
||||||
|
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 no supported codecs are passed, assume autodetected capabilities
|
||||||
|
if (command.isMember("supported_codecs")) {
|
||||||
|
capa.removeMember("exceptions");
|
||||||
|
capa["codecs"].null();
|
||||||
|
std::set<std::string> dupes;
|
||||||
|
jsonForEach(command["supported_codecs"], i){
|
||||||
|
if (dupes.count(i->asStringRef())){continue;}
|
||||||
|
dupes.insert(i->asStringRef());
|
||||||
|
if (supportedVideo.count(i->asStringRef())){
|
||||||
|
capa["codecs"][0u][0u].append(i->asStringRef());
|
||||||
|
}else if (supportedAudio.count(i->asStringRef())){
|
||||||
|
capa["codecs"][0u][1u].append(i->asStringRef());
|
||||||
|
}else{
|
||||||
|
JSON::Value r;
|
||||||
|
r["type"] = "error";
|
||||||
|
r["data"] = "Unsupported codec: "+i->asStringRef();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectDefaultTracks();
|
||||||
|
sendWebsocketCodecData("codec_data");
|
||||||
|
}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()){
|
||||||
|
targetParams["audio"] = command["audio"].asString();
|
||||||
|
}else{
|
||||||
|
targetParams.erase("audio");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (command.isMember("video")){
|
||||||
|
if (!command["video"].isNull()){
|
||||||
|
targetParams["video"] = command["video"].asString();
|
||||||
|
}else{
|
||||||
|
targetParams.erase("video");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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()) {
|
||||||
|
uint64_t seekTarget = currentTime();
|
||||||
|
if (command.isMember("seek_time")){
|
||||||
|
seekTarget = command["seek_time"].asInt();
|
||||||
|
prevVidTrack = INVALID_TRACK_ID;
|
||||||
|
}
|
||||||
|
// Add the previous video track back, if we had one.
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && !userSelect.count(prevVidTrack)){
|
||||||
|
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;
|
||||||
|
sendWebsocketCodecData(command["type"]);
|
||||||
|
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;
|
||||||
|
sendWebsocketCodecData(command["type"]);
|
||||||
|
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;
|
||||||
|
}else{
|
||||||
|
prevVidTrack = INVALID_TRACK_ID;
|
||||||
|
}
|
||||||
|
onIdle();
|
||||||
|
return;
|
||||||
|
}else if (command["type"] == "set_speed") {
|
||||||
|
handleWebsocketSetSpeed(command);
|
||||||
|
}else if (command["type"] == "stop") {
|
||||||
|
Util::logExitReason("User requested stop");
|
||||||
|
myConn.close();
|
||||||
|
}else if (command["type"] == "play") {
|
||||||
|
parseData = true;
|
||||||
|
if (command.isMember("seek_time")){handleWebsocketSeek(command);}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutMP4::sendWebsocketCodecData(const std::string& type) {
|
||||||
|
JSON::Value r;
|
||||||
|
r["type"] = type;
|
||||||
|
r["data"]["current"] = currentTime();
|
||||||
|
std::map<size_t, Comms::Users>::const_iterator it = userSelect.begin();
|
||||||
|
while (it != userSelect.end()) {
|
||||||
|
if (prevVidTrack != INVALID_TRACK_ID && M.getType(it->first) == "video" && it->first != prevVidTrack){
|
||||||
|
//Skip future tracks
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::string codec = Util::codecString(M.getCodec(it->first), M.getInit(it->first));
|
||||||
|
if (!codec.size()) {
|
||||||
|
FAIL_MSG("Failed to get the codec string for track: %zu.", it->first);
|
||||||
|
++it;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
r["data"]["codecs"].append(codec);
|
||||||
|
r["data"]["tracks"].append(it->first);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||||
|
r["data"]["tracks"].append(it->first);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}// namespace Mist
|
}// namespace Mist
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,9 @@ namespace Mist{
|
||||||
uint64_t time;
|
uint64_t time;
|
||||||
uint64_t byteOffset; // Stores relative bpos for fragmented MP4
|
uint64_t byteOffset; // Stores relative bpos for fragmented MP4
|
||||||
uint64_t index;
|
uint64_t index;
|
||||||
|
size_t sampleSize;
|
||||||
|
uint16_t sampleDuration;
|
||||||
|
uint16_t sampleOffset;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SortSet{
|
class SortSet{
|
||||||
|
@ -95,22 +98,34 @@ namespace Mist{
|
||||||
uint64_t endFragmentTime); // this builds the moof box for fragmented MP4
|
uint64_t endFragmentTime); // this builds the moof box for fragmented MP4
|
||||||
|
|
||||||
void findSeekPoint(uint64_t byteStart, uint64_t &seekPoint, uint64_t headerSize);
|
void findSeekPoint(uint64_t byteStart, uint64_t &seekPoint, uint64_t headerSize);
|
||||||
|
void appendSinglePacketMoof(Util::ResizeablePointer& moofOut, size_t extraBytes = 0);
|
||||||
|
size_t fragmentHeaderSize(std::deque<size_t>& sortedTracks, std::set<keyPart>& trunOrder, uint64_t startFragmentTime, uint64_t endFragmentTime);
|
||||||
void onHTTP();
|
void onHTTP();
|
||||||
void sendNext();
|
void sendNext();
|
||||||
void sendHeader();
|
void sendHeader();
|
||||||
|
bool doesWebsockets() { return true; }
|
||||||
|
void onWebsocketConnect();
|
||||||
|
void onWebsocketFrame();
|
||||||
|
void onIdle();
|
||||||
virtual bool onFinish();
|
virtual bool onFinish();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void sendWebsocketCodecData(const std::string& type);
|
||||||
|
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;
|
||||||
uint64_t byteEnd;
|
uint64_t byteEnd;
|
||||||
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
|
||||||
|
@ -135,6 +150,7 @@ namespace Mist{
|
||||||
std::map<size_t, fragSet> currentPartSet;
|
std::map<size_t, fragSet> currentPartSet;
|
||||||
|
|
||||||
std::string protectionHeader(size_t idx);
|
std::string protectionHeader(size_t idx);
|
||||||
|
Util::ResizeablePointer webBuf;
|
||||||
};
|
};
|
||||||
}// namespace Mist
|
}// namespace Mist
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue