From 9417fa8dc21673c83af851b7f68609c7e280accb Mon Sep 17 00:00:00 2001 From: Thulinma Date: Wed, 25 Nov 2020 15:39:28 +0100 Subject: [PATCH] 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. --- src/output/output.cpp | 3 +- src/output/output_mp4.cpp | 557 +++++++++++++++++++++++++++++++++++++- src/output/output_mp4.h | 20 +- 3 files changed, 564 insertions(+), 16 deletions(-) diff --git a/src/output/output.cpp b/src/output/output.cpp index d4b414d7..da47fcfd 100644 --- a/src/output/output.cpp +++ b/src/output/output.cpp @@ -1243,10 +1243,11 @@ namespace Mist{ } stats(); } + if (!thisPacket){continue;} } // 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 uint32_t sleepTime = std::min(20ul, needsLookAhead); // wait at most double the look ahead time, plus ten seconds diff --git a/src/output/output_mp4.cpp b/src/output/output_mp4.cpp index 1c7f5d6c..525f5462 100644 --- a/src/output/output_mp4.cpp +++ b/src/output/output_mp4.cpp @@ -7,8 +7,13 @@ #include #include #include - +#include /* for `Util::codecString()` when streaming mp4 over websockets and playback using media source extensions. */ +#include #include +#include + +std::set supportedAudio; +std::set supportedVideo; namespace Mist{ std::string toUTF16(const std::string &original){ @@ -101,10 +106,14 @@ namespace Mist{ } OutMP4::OutMP4(Socket::Connection &conn) : HTTPOutput(conn){ + prevVidTrack = INVALID_TRACK_ID; nextHeaderTime = 0xffffffffffffffffull; startTime = 0; endTime = 0xffffffffffffffffull; realBaseOffset = 1; + stayLive = true; + target_rate = 0.0; + forwardTo = 0; } OutMP4::~OutMP4(){} @@ -113,7 +122,6 @@ namespace Mist{ capa["name"] = "MP4"; capa["friendly"] = "MP4 over HTTP"; capa["desc"] = "Pseudostreaming in MP4 format over HTTP"; - capa["url_rel"] = "/$.mp4"; capa["url_match"][0u] = "/$.mp4"; capa["url_match"][1u] = "/$.3gp"; capa["url_match"][2u] = "/$.fmp4"; @@ -122,9 +130,16 @@ namespace Mist{ capa["codecs"][0u][1u].append("AAC"); capa["codecs"][0u][1u].append("MP3"); 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]["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 capa["exceptions"]["live"] = JSON::fromString("[[\"blacklist\",[\"iPad\",\"iPhone\",\"iPod\",\"Safari\"]], " @@ -336,7 +351,8 @@ namespace Mist{ bool OutMP4::mp4Header(Util::ResizeablePointer & headOut, uint64_t &size, int fragmented){ uint32_t mainTrack = M.mainTrack(); 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 size = 0; // 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; for (std::map::const_iterator it = userSelect.begin(); it != userSelect.end(); it++){ + if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;} lastms = std::max(lastms, M.getLastms(it->first)); firstms = std::min(firstms, M.getFirstms(it->first)); } @@ -378,6 +395,7 @@ namespace Mist{ moovBox.setContent(mvhdBox, moovOffset++); for (std::map::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)); size_t partCount = parts.getValidCount(); uint64_t tDuration = M.getLastms(it->first) - M.getFirstms(it->first); @@ -646,6 +664,7 @@ namespace Mist{ mvexBox.setContent(mehdBox, curBox++); for (std::map::const_iterator it = userSelect.begin(); it != userSelect.end(); it++){ + if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;} MP4::TREX trexBox(it->first + 1); trexBox.setDefaultSampleDuration(1000); mvexBox.setContent(trexBox, curBox++); @@ -653,6 +672,7 @@ namespace Mist{ moovBox.setContent(mvexBox, moovOffset++); for (std::map::const_iterator it = userSelect.begin(); it != userSelect.end(); it++){ + if (prevVidTrack != INVALID_TRACK_ID && it->first == prevVidTrack){continue;} if (M.getEncryption(it->first) != ""){ MP4::PSSH psshBox; psshBox.setSystemIDHex(Encodings::Hex::decode("9a04f07998404286ab92e65be0885f95")); @@ -691,6 +711,7 @@ namespace Mist{ SortSet sortSet; // filling sortset for interleaving parts for (std::map::const_iterator subIt = userSelect.begin(); subIt != userSelect.end(); subIt++){ + if (prevVidTrack != INVALID_TRACK_ID && subIt->first == prevVidTrack){continue;} keyPart temp; temp.trackID = subIt->first; temp.time = M.getFirstms(subIt->first); @@ -784,6 +805,103 @@ namespace Mist{ // That's technically legal, of course. } + // ------------------------------------------------------------ + + size_t OutMP4::fragmentHeaderSize(std::deque& sortedTracks, std::set& 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){ bool hasAudio = false; uint64_t mdatSize = 0; @@ -942,13 +1060,12 @@ namespace Mist{ H.Chunkify(mdatHeader, 8, myConn); } - bool OutMP4::onFinish(){ - H.Chunkify(0, 0, myConn); - wantRequest = true; - return true; - } - void OutMP4::onHTTP(){ + + if (webSock) { + return; + } + std::string dl; if (H.GetVar("dl").size()){ dl = H.GetVar("dl"); @@ -1130,12 +1247,96 @@ namespace Mist{ } 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 char *dataPointer = 0; size_t len = 0; 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; if (M.getLive()){ @@ -1167,11 +1368,18 @@ namespace Mist{ // generate content in mdat, meaning: send right parts 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(); DTSC::Parts parts(M.parts(firstKeyPart.trackID)); + /* if (thisIdx != firstKeyPart.trackID || thisPacket.getTime() != firstKeyPart.time || len != parts.getSize(firstKeyPart.index)){ if (thisPacket.getTime() > firstKeyPart.time || thisIdx > firstKeyPart.trackID){ @@ -1191,6 +1399,7 @@ namespace Mist{ } return; } + */ // The remainder of this function handles non-live situations if (M.getLive()){ @@ -1235,6 +1444,31 @@ namespace Mist{ } void OutMP4::sendHeader(){ + + if (webSock) { + + JSON::Value r; + r["type"] = "info"; + r["data"]["msg"] = "Sending header"; + for (std::map::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(); if (M.getLive()){ @@ -1260,4 +1494,301 @@ namespace Mist{ 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 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 prevSelTracks; + prevVidTrack = INVALID_TRACK_ID; + for (std::map::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 newSelTracks; + newSelTracks.insert(prevVidTrack); + for (std::map::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::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::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 + diff --git a/src/output/output_mp4.h b/src/output/output_mp4.h index fde593b1..21855ef6 100644 --- a/src/output/output_mp4.h +++ b/src/output/output_mp4.h @@ -15,6 +15,9 @@ namespace Mist{ uint64_t time; uint64_t byteOffset; // Stores relative bpos for fragmented MP4 uint64_t index; + size_t sampleSize; + uint16_t sampleDuration; + uint16_t sampleOffset; }; class SortSet{ @@ -95,22 +98,34 @@ namespace Mist{ uint64_t endFragmentTime); // this builds the moof box for fragmented MP4 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& sortedTracks, std::set& trunOrder, uint64_t startFragmentTime, uint64_t endFragmentTime); void onHTTP(); void sendNext(); void sendHeader(); + bool doesWebsockets() { return true; } + void onWebsocketConnect(); + void onWebsocketFrame(); + void onIdle(); virtual bool onFinish(); - 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 byteStart; uint64_t byteEnd; int64_t leftOver; uint64_t currPos; uint64_t seekPoint; + uint64_t forwardTo; uint64_t nextHeaderTime; uint64_t headerSize; + size_t prevVidTrack; // variables for standard MP4 std::set sortSet; // needed for unfragmented MP4, remembers the order of keyparts @@ -135,6 +150,7 @@ namespace Mist{ std::map currentPartSet; std::string protectionHeader(size_t idx); + Util::ResizeablePointer webBuf; }; }// namespace Mist