diff --git a/lib/defines.h b/lib/defines.h index 462b34fb..895e5d36 100644 --- a/lib/defines.h +++ b/lib/defines.h @@ -133,6 +133,7 @@ static inline void show_stackframe(){} #define SHM_STREAM_INDEX "MstSTRM%s" //%s stream name #define SHM_STREAM_STATE "MstSTATE%s" //%s stream name #define SHM_STREAM_CONF "MstSCnf%s" //%s stream name +#define SHM_GLOBAL_CONF "MstGlobalConfig" #define STRMSTAT_OFF 0 #define STRMSTAT_INIT 1 #define STRMSTAT_BOOT 2 diff --git a/lib/stream.cpp b/lib/stream.cpp index daf37550..3474c5c2 100644 --- a/lib/stream.cpp +++ b/lib/stream.cpp @@ -205,12 +205,44 @@ JSON::Value Util::getStreamConfig(const std::string &streamname){ Util::DTSCShmReader rStrmConf(tmpBuf); DTSC::Scan stream_cfg = rStrmConf.getScan(); if (!stream_cfg){ - WARN_MSG("Could not get stream '%s' config!", smp.c_str()); + if (!Util::getGlobalConfig("defaultStream")){ + WARN_MSG("Could not get stream '%s' config!", smp.c_str()); + }else{ + INFO_MSG("Could not get stream '%s' config, not emitting WARN message because fallback is configured", smp.c_str()); + } return result; } return stream_cfg.asJSON(); } +JSON::Value Util::getGlobalConfig(const std::string &optionName){ + IPC::sharedPage globCfg(SHM_GLOBAL_CONF); + if (!globCfg.mapped){ + FAIL_MSG("Could not open global configuration options to read setting for '%s'", optionName.c_str()); + return JSON::Value(); + } + Util::RelAccX cfgData(globCfg.mapped); + if (!cfgData.isReady()){ + FAIL_MSG("Global configuration options not ready; cannot read setting for '%s'", optionName.c_str()); + return JSON::Value(); + } + Util::RelAccXFieldData dataField = cfgData.getFieldData(optionName); + switch (dataField.type & 0xF0){ + case RAX_INT: + case RAX_UINT: + //Integer types, return JSON::Value integer + return JSON::Value(cfgData.getInt(dataField)); + case RAX_RAW: + case RAX_STRING: + //String types, return JSON::Value string + return JSON::Value(std::string(cfgData.getPointer(dataField), cfgData.getSize(optionName))); + default: + //Unimplemented types + FAIL_MSG("Global configuration setting for '%s' is not an implemented datatype!", optionName.c_str()); + return JSON::Value(); + } +} + DTSC::Meta Util::getStreamMeta(const std::string &streamname){ DTSC::Meta ret; char pageId[NAME_BUFFER_SIZE]; diff --git a/lib/stream.h b/lib/stream.h index 8a1b71f6..eab26acf 100644 --- a/lib/stream.h +++ b/lib/stream.h @@ -17,6 +17,7 @@ namespace Util { bool startInput(std::string streamname, std::string filename = "", bool forkFirst = true, bool isProvider = false, const std::map & overrides = std::map(), pid_t * spawn_pid = NULL); int startPush(const std::string & streamname, std::string & target); JSON::Value getStreamConfig(const std::string & streamname); + JSON::Value getGlobalConfig(const std::string & optionName); JSON::Value getInputBySource(const std::string & filename, bool isProvider = false); DTSC::Meta getStreamMeta(const std::string & streamname); uint8_t getStreamStatus(const std::string & streamname); diff --git a/src/controller/controller_api.cpp b/src/controller/controller_api.cpp index de4c6904..05374ba8 100644 --- a/src/controller/controller_api.cpp +++ b/src/controller/controller_api.cpp @@ -494,6 +494,9 @@ void Controller::handleAPICommands(JSON::Value & Request, JSON::Value & Response out["prometheus"] = in["prometheus"]; Controller::prometheus = out["prometheus"].asStringRef(); } + if (in.isMember("defaultStream")){ + out["defaultStream"] = in["defaultStream"]; + } } if (Request.isMember("bandwidth")){ if (Request["bandwidth"].isObject()){ diff --git a/src/controller/controller_capabilities.cpp b/src/controller/controller_capabilities.cpp index 83b9a75c..765e258e 100644 --- a/src/controller/controller_capabilities.cpp +++ b/src/controller/controller_capabilities.cpp @@ -160,6 +160,13 @@ namespace Controller { trgs["LIVE_BANDWIDTH"]["response"] = "always"; trgs["LIVE_BANDWIDTH"]["response_action"] = "If false, shuts down the stream buffer."; trgs["LIVE_BANDWIDTH"]["argument"] = "Triggers only if current bytes per second exceeds this amount (integer)"; + + trgs["DEFAULT_STREAM"]["when"] = "When any user attempts to open a stream that cannot be opened (because it is either offline or not configured), allows rewriting the stream to a different one as fallback. Supports variable substitution."; + trgs["DEFAULT_STREAM"]["stream_specific"] = true; + trgs["DEFAULT_STREAM"]["payload"] = "current defaultStream setting (string)\nrequested stream name (string)\nviewer host (string)\noutput type (string)\nfull request URL (string, may be blank for non-URL-based requests!)"; + trgs["DEFAULT_STREAM"]["response"] = "always"; + trgs["DEFAULT_STREAM"]["response_action"] = "Overrides the default stream setting (for this view) to the response value. If empty, fails loading the stream and returns an error to the viewer/user."; + } ///Aquire list of available protocols, storing in global 'capabilities' JSON::Value. diff --git a/src/controller/controller_storage.cpp b/src/controller/controller_storage.cpp index c578a4c4..328eed0e 100644 --- a/src/controller/controller_storage.cpp +++ b/src/controller/controller_storage.cpp @@ -334,6 +334,27 @@ namespace Controller{ writeStream(it.key(), *it); } + { + //Global configuration options, if any + IPC::sharedPage globCfg; + globCfg.init(SHM_GLOBAL_CONF, 4096, false, false); + if (!globCfg.mapped){ + globCfg.init(SHM_GLOBAL_CONF, 4096, true, false); + } + if (globCfg.mapped){ + Util::RelAccX globAccX(globCfg.mapped, false); + if (!globAccX.isReady()){ + globAccX.addField("defaultStream", RAX_128STRING); + globAccX.setRCount(1); + globAccX.setEndPos(1); + globAccX.setReady(); + } + globAccX.setString("defaultStream", Storage["config"]["defaultStream"].asStringRef()); + globCfg.master = false;//leave the page after closing + } + } + + /*LTS-START*/ static std::map pageForType; // should contain one page for every trigger type static JSON::Value writtenTrigs; diff --git a/src/output/output.cpp b/src/output/output.cpp index 64fc1641..66ce6253 100644 --- a/src/output/output.cpp +++ b/src/output/output.cpp @@ -372,8 +372,31 @@ namespace Mist{ } }else{ if (!Util::startInput(streamName, "", true, isPushing())){ - onFail("Stream open failed", true); - return; + JSON::Value defStrmJson = Util::getGlobalConfig("defaultStream"); + std::string defStrm = defStrmJson.asString(); + if(Triggers::shouldTrigger("DEFAULT_STREAM", streamName)){ + std::string payload = defStrm+"\n"+streamName+"\n" + getConnectedHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl; + //The return value is ignored, because the response (defStrm in this case) tells us what to do next, if anything. + Triggers::doTrigger("DEFAULT_STREAM", payload, streamName, false, defStrm); + } + if (!defStrm.size()){ + onFail("Stream open failed", true); + return; + } + std::string newStrm = defStrm; + Util::streamVariables(newStrm, streamName, ""); + if (streamName == newStrm){ + onFail("Stream open failed; nothing to fall back to ("+defStrm+" == "+newStrm+")", true); + return; + } + INFO_MSG("Stream open failed; falling back to default stream '%s' -> '%s'", defStrm.c_str(), newStrm.c_str()); + std::string origStream = streamName; + streamName = newStrm; + Util::Config::streamName = streamName; + if (!Util::startInput(streamName, "", true, isPushing())){ + onFail("Stream open failed (fallback stream for '"+origStream+"')", true); + return; + } } } disconnect(); diff --git a/src/output/output_http_internal.cpp b/src/output/output_http_internal.cpp index 10f0bf43..a41fa5ee 100644 --- a/src/output/output_http_internal.cpp +++ b/src/output/output_http_internal.cpp @@ -6,6 +6,7 @@ #include "flashPlayer.h" #include "oldFlashPlayer.h" #include +#include namespace Mist { /// Helper function to find the protocol entry for a given port number @@ -358,8 +359,36 @@ namespace Mist { if (config->getString("nostreamtext") != ""){ json_resp["on_error"] = config->getString("nostreamtext"); } + //Make note of any defaultStream-based redirection + if (origStreamName.size() && origStreamName != streamName){ + json_resp["redirected"].append(origStreamName); + json_resp["redirected"].append(streamName); + } uint8_t streamStatus = Util::getStreamStatus(streamName); if (streamStatus != STRMSTAT_READY){ + //If we haven't rewritten the stream name yet to a fallback, attempt to do so + if (origStreamName == streamName){ + JSON::Value defStrmJson = Util::getGlobalConfig("defaultStream"); + std::string defStrm = defStrmJson.asString(); + if(Triggers::shouldTrigger("DEFAULT_STREAM", streamName)){ + std::string payload = defStrm+"\n"+streamName+"\n" + getConnectedHost() +"\n"+capa["name"].asStringRef()+"\n"+reqUrl; + //The return value is ignored, because the response (defStrm in this case) tells us what to do next, if anything. + Triggers::doTrigger("DEFAULT_STREAM", payload, streamName, false, defStrm); + } + if (defStrm.size()){ + std::string newStrm = defStrm; + Util::streamVariables(newStrm, streamName, ""); + if (streamName != newStrm){ + INFO_MSG("Falling back to default stream '%s' -> '%s'", defStrm.c_str(), newStrm.c_str()); + origStreamName = streamName; + streamName = newStrm; + Util::Config::streamName = streamName; + reconnect(); + return getStatusJSON(reqHost, useragent); + } + } + origStreamName.clear();//no fallback, don't check again + } switch (streamStatus){ case STRMSTAT_OFF: json_resp["error"] = "Stream is offline"; @@ -504,6 +533,7 @@ namespace Mist { } void OutHTTP::onHTTP(){ + origStreamName = streamName; std::string method = H.method; //Handle certbot validations @@ -907,6 +937,8 @@ namespace Mist { Util::startInput(streamName, "", true, false); char pageName[NAME_BUFFER_SIZE]; + std::string currStreamName; + currStreamName = streamName; snprintf(pageName, NAME_BUFFER_SIZE, SHM_STREAM_STATE, streamName.c_str()); IPC::sharedPage streamStatus(pageName, 1, false, false); uint8_t prevState, newState, metaCounter; @@ -925,6 +957,11 @@ namespace Mist { disconnect(); } JSON::Value resp = getStatusJSON(reqHost, useragent); + if (currStreamName != streamName){ + currStreamName = streamName; + snprintf(pageName, NAME_BUFFER_SIZE, SHM_STREAM_STATE, streamName.c_str()); + streamStatus.close(); + } ws.sendFrame(resp.toString()); prevState = newState; }else{ diff --git a/src/output/output_http_internal.h b/src/output/output_http_internal.h index f0342f68..aca3587c 100644 --- a/src/output/output_http_internal.h +++ b/src/output/output_http_internal.h @@ -20,6 +20,8 @@ namespace Mist { virtual bool onFinish(){ return stayConnected; } + private: + std::string origStreamName; }; }