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