diff --git a/src/output/output_h264.cpp b/src/output/output_h264.cpp index 4fbf9064..518b66ee 100644 --- a/src/output/output_h264.cpp +++ b/src/output/output_h264.cpp @@ -1,10 +1,15 @@ #include "output_h264.h" #include #include +#include namespace Mist{ OutH264::OutH264(Socket::Connection &conn) : HTTPOutput(conn){ - if (targetParams.count("keysonly")){keysOnly = 1;} + prevVidTrack = INVALID_TRACK_ID; + keysOnly = targetParams.count("keysonly")?1:0; + stayLive = true; + target_rate = 0.0; + forwardTo = 0; if (config->getString("target").size()){ if (!streamName.size()){ WARN_MSG("Recording unconnected H264 output to file! Cancelled."); @@ -28,14 +33,319 @@ namespace Mist{ } } + void OutH264::onWebsocketConnect() { + capa["name"] = "Raw/WS"; + idleInterval = 1000; + maxSkipAhead = 0; + } + + void OutH264::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 (i->asStringRef() == "H264" || i->asStringRef() == "HEVC"){ + capa["codecs"][0u][0u].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 OutH264::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 OutH264::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 OutH264::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 OutH264::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 OutH264::onFinish() { + if (!webSock){ + H.Chunkify(0, 0, myConn); + wantRequest = true; + return true; + } + JSON::Value r; + r["type"] = "on_stop"; + r["data"]["current"] = currentTime(); + r["data"]["begin"] = Output::startTime(); + r["data"]["end"] = Output::endTime(); + webSock->sendFrame(r.toString()); + parseData = false; + return false; + } + void OutH264::init(Util::Config *cfg){ HTTPOutput::init(cfg); capa["name"] = "H264"; - capa["friendly"] = "H264 over HTTP"; - capa["desc"] = "Pseudostreaming in raw H264 Annex B format over HTTP"; + capa["friendly"] = "H264/H265 over HTTP"; + capa["desc"] = "Pseudostreaming in raw H264/H265 Annex B format over HTTP"; capa["url_rel"] = "/$.h264"; capa["url_match"] = "/$.h264"; capa["codecs"][0u][0u].append("H264"); + capa["codecs"][0u][0u].append("HEVC"); + + capa["methods"][0u]["handler"] = "http"; + capa["methods"][0u]["type"] = "html5/video/raw"; + capa["methods"][0u]["hrn"] = "Raw progressive"; + capa["methods"][0u]["priority"] = 1; + capa["methods"][0u]["url_rel"] = "/$.h264"; + capa["methods"][1u]["handler"] = "ws"; + capa["methods"][1u]["type"] = "ws/video/raw"; + capa["methods"][1u]["hrn"] = "Raw WebSocket"; + capa["methods"][1u]["priority"] = 2; + capa["methods"][1u]["url_rel"] = "/$.h264"; JSON::Value opt; opt["arg"] = "string"; @@ -53,39 +363,140 @@ namespace Mist{ size_t len = 0; thisPacket.getString("data", dataPointer, len); + 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(); + } + + + webBuf.truncate(0); + webBuf.append("\000\000\000\000\000\000\000\000\000\000\000\000", 12); + webBuf[0] = thisIdx; + webBuf[1] = thisPacket.getFlag("keyframe")?1:0; + Bit::htobll(webBuf+2, thisTime); + if (thisPacket.hasMember("offset")) { + Bit::htobs(webBuf+10, thisPacket.getInt("offset")); + }else{ + Bit::htobs(webBuf+10, 0); + } + + unsigned int i = 0; + while (i + 4 < len){ + uint32_t ThisNaluSize = Bit::btohl(dataPointer + i); + webBuf.append("\000\000\000\001", 4); + webBuf.append(dataPointer + i + 4, ThisNaluSize); + i += ThisNaluSize + 4; + } + 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; + } + unsigned int i = 0; while (i + 4 < len){ uint32_t ThisNaluSize = Bit::btohl(dataPointer + i); - myConn.SendNow("\000\000\000\001", 4); - myConn.SendNow(dataPointer + i + 4, ThisNaluSize); + H.Chunkify("\000\000\000\001", 4, myConn); + H.Chunkify(dataPointer + i + 4, ThisNaluSize, myConn); i += ThisNaluSize + 4; } } void OutH264::sendHeader(){ - MP4::AVCC avccbox; + size_t mainTrack = getMainSelectedTrack(); if (mainTrack != INVALID_TRACK_ID){ - avccbox.setPayload(M.getInit(mainTrack)); - myConn.SendNow(avccbox.asAnnexB()); + + 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; + + headerData.append("\000\000\000\000\000\000\000\000\000\000\000\000", 12); + headerData[0] = thisIdx; + headerData[1] = 2; + + if (M.getCodec(mainTrack) == "H264"){ + MP4::AVCC avccbox; + avccbox.setPayload(M.getInit(mainTrack)); + headerData.append(avccbox.asAnnexB()); + } + if (M.getCodec(mainTrack) == "HEVC"){ + MP4::HVCC hvccbox; + hvccbox.setPayload(M.getInit(mainTrack)); + headerData.append(hvccbox.asAnnexB()); + } + webSock->sendFrame(headerData, headerData.size(), 2); + sentHeader = true; + return; + } + + if (M.getCodec(mainTrack) == "H264"){ + MP4::AVCC avccbox; + avccbox.setPayload(M.getInit(mainTrack)); + H.Chunkify(avccbox.asAnnexB(), myConn); + } + if (M.getCodec(mainTrack) == "HEVC"){ + MP4::HVCC hvccbox; + hvccbox.setPayload(M.getInit(mainTrack)); + H.Chunkify(hvccbox.asAnnexB(), myConn); + } + sentHeader = true; } - sentHeader = true; } - void OutH264::onHTTP(){ - std::string method = H.method; - // Set mode to key frames only - keysOnly = (H.GetVar("keysonly") != ""); - H.Clean(); - H.SetHeader("Content-Type", "video/H264"); - H.protocol = "HTTP/1.0"; - H.setCORSHeaders(); - if (method == "OPTIONS" || method == "HEAD"){ - H.SendResponse("200", "OK", myConn); - return; - } - H.SendResponse("200", "OK", myConn); + void OutH264::respondHTTP(const HTTP::Parser & req, bool headersOnly){ + //Set global defaults + HTTPOutput::respondHTTP(req, headersOnly); + + size_t mainTrk = getMainSelectedTrack(); + H.SetHeader("Content-Type", "video/"+M.getCodec(mainTrk)); + H.StartResponse("200", "OK", req, myConn); + if (headersOnly){return;} parseData = true; wantRequest = false; } + }// namespace Mist diff --git a/src/output/output_h264.h b/src/output/output_h264.h index 4eb2ac01..c6fef5c4 100644 --- a/src/output/output_h264.h +++ b/src/output/output_h264.h @@ -5,9 +5,24 @@ namespace Mist{ public: OutH264(Socket::Connection &conn); static void init(Util::Config *cfg); - void onHTTP(); + void respondHTTP(const HTTP::Parser & req, bool headersOnly); 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; + uint64_t forwardTo; + double target_rate; ///< Target playback speed rate (1.0 = normal, 0 = auto) + size_t prevVidTrack; + Util::ResizeablePointer webBuf; private: bool isRecording();