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:
Thulinma 2022-11-09 10:35:07 +01:00
parent c337fff614
commit 3e2a17ff93
36 changed files with 1054 additions and 469 deletions

View file

@ -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(){