2555 lines
107 KiB
C++
2555 lines
107 KiB
C++
#include <algorithm>
|
|
#include <fcntl.h>
|
|
#include <iterator> //std::distance
|
|
#include <semaphore.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
#include <iomanip>
|
|
#include <fstream>
|
|
|
|
#include "output.h"
|
|
#include <mist/bitfields.h>
|
|
#include <mist/defines.h>
|
|
#include <mist/h264.h>
|
|
#include <mist/http_parser.h>
|
|
#include <mist/stream.h>
|
|
#include <mist/timing.h>
|
|
#include <mist/util.h>
|
|
#include <mist/urireader.h>
|
|
#include <sys/file.h>
|
|
#include <mist/encode.h>
|
|
|
|
/*LTS-START*/
|
|
#include <arpa/inet.h>
|
|
#include <mist/langcodes.h>
|
|
#include <mist/triggers.h>
|
|
#include <netdb.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
/*LTS-END*/
|
|
|
|
namespace Mist{
|
|
JSON::Value Output::capa = JSON::Value();
|
|
Util::Config *Output::config = NULL;
|
|
|
|
uint32_t getDTSCLen(char *mapped, uint64_t offset){return Bit::btohl(mapped + offset + 4);}
|
|
|
|
uint64_t getDTSCTime(char *mapped, uint64_t offset){return Bit::btohll(mapped + offset + 12);}
|
|
|
|
void Output::init(Util::Config *cfg){
|
|
capa["optional"]["debug"]["name"] = "debug";
|
|
capa["optional"]["debug"]["help"] = "The debug level at which messages need to be printed.";
|
|
capa["optional"]["debug"]["option"] = "--debug";
|
|
capa["optional"]["debug"]["type"] = "debug";
|
|
|
|
JSON::Value option;
|
|
option["long"] = "noinput";
|
|
option["short"] = "N";
|
|
option["help"] = "Do not start input if not already started";
|
|
option["value"].append(0);
|
|
cfg->addOption("noinput", option);
|
|
option.null();
|
|
|
|
|
|
capa["optional"]["default_track_sorting"]["name"] = "Default track sorting";
|
|
capa["optional"]["default_track_sorting"]["help"] = "What tracks are selected first when no specific track selector is used for playback.";
|
|
capa["optional"]["default_track_sorting"]["default"] = "";
|
|
capa["optional"]["default_track_sorting"]["type"] = "select";
|
|
capa["optional"]["default_track_sorting"]["option"] = "--default_track_sorting";
|
|
capa["optional"]["default_track_sorting"]["short"] = "S";
|
|
option.append("");
|
|
option.append("Default (last added for live, first added for VoD)");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("bps_lth");
|
|
option.append("Bit rate, low to high");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("bps_htl");
|
|
option.append("Bit rate, high to low");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("id_lth");
|
|
option.append("Track ID, low to high");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("id_htl");
|
|
option.append("Track ID, high to low");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("res_lth");
|
|
option.append("Resolution, low to high");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
option.null();
|
|
option.append("res_htl");
|
|
option.append("Resolution, high to low");
|
|
capa["optional"]["default_track_sorting"]["select"].append(option);
|
|
|
|
config = cfg;
|
|
}
|
|
|
|
Output::Output(Socket::Connection &conn) : myConn(conn){
|
|
dataWaitTimeout = 2500;
|
|
pushing = false;
|
|
recursingSync = false;
|
|
firstTime = Util::bootMS();
|
|
thisTime = 0;
|
|
firstPacketTime = 0xFFFFFFFFFFFFFFFFull;
|
|
lastPacketTime = 0;
|
|
tkn = "";
|
|
parseData = false;
|
|
wantRequest = true;
|
|
sought = false;
|
|
isInitialized = false;
|
|
isBlocking = false;
|
|
needsLookAhead = 0;
|
|
lastStats = 0xFFFFFFFFFFFFFFFFull;
|
|
maxSkipAhead = 7500;
|
|
uaDelay = 10;
|
|
realTime = 1000;
|
|
emptyCount = 0;
|
|
seekCount = 2;
|
|
firstData = true;
|
|
newUA = true;
|
|
lastPushUpdate = 0;
|
|
Util::Config::binaryType = Util::OUTPUT;
|
|
|
|
lastRecv = Util::bootSecs();
|
|
if (myConn){
|
|
setBlocking(true);
|
|
//Make sure that if the socket is a non-stdio socket, we close it when forking
|
|
if (myConn.getSocket() > 2){
|
|
Util::Procs::socketList.insert(myConn.getSocket());
|
|
}
|
|
}else{
|
|
WARN_MSG("Warning: MistOut created with closed socket!");
|
|
}
|
|
sentHeader = false;
|
|
isRecordingToFile = false;
|
|
|
|
// If we have a streamname option, set internal streamname to that option
|
|
if (!streamName.size() && config->hasOption("streamname")){
|
|
streamName = config->getString("streamname");
|
|
Util::setStreamName(streamName);
|
|
}
|
|
|
|
/*LTS-START*/
|
|
// If we have a target, scan for trailing ?, remove it, parse into targetParams
|
|
if (config->hasOption("target")){
|
|
std::string tgt = config->getString("target");
|
|
if (tgt.rfind('?') != std::string::npos){
|
|
INFO_MSG("Stripping target options: %s", tgt.substr(tgt.rfind('?') + 1).c_str());
|
|
HTTP::parseVars(tgt.substr(tgt.rfind('?') + 1), targetParams);
|
|
config->getOption("target", true).append(tgt.substr(0, tgt.rfind('?')));
|
|
}
|
|
}
|
|
if (targetParams.count("rate")){
|
|
long long int multiplier = JSON::Value(targetParams["rate"]).asInt();
|
|
if (multiplier){
|
|
realTime = 1000 / multiplier;
|
|
}else{
|
|
realTime = 0;
|
|
}
|
|
}
|
|
if (isRecording() && DTSC::trackValidMask == TRACK_VALID_EXT_HUMAN){
|
|
DTSC::trackValidMask = TRACK_VALID_EXT_PUSH;
|
|
if (targetParams.count("unmask")){DTSC::trackValidMask = TRACK_VALID_ALL;}
|
|
}
|
|
/*LTS-END*/
|
|
}
|
|
|
|
bool Output::isFileTarget(){
|
|
VERYHIGH_MSG("Default file target handler (false)");
|
|
return false;
|
|
}
|
|
|
|
void Output::listener(Util::Config &conf, int (*callback)(Socket::Connection &S)){
|
|
conf.serveForkedSocket(callback);
|
|
}
|
|
|
|
void Output::setBlocking(bool blocking){
|
|
isBlocking = blocking;
|
|
myConn.setBlocking(isBlocking);
|
|
}
|
|
|
|
bool Output::isRecording(){
|
|
return config->hasOption("target") && config->getString("target").size();
|
|
}
|
|
|
|
/// Called when stream initialization has failed.
|
|
/// The standard implementation will set isInitialized to false and close the client connection,
|
|
/// thus causing the process to exit cleanly.
|
|
void Output::onFail(const std::string &msg, bool critical){
|
|
if (critical){
|
|
FAIL_MSG("onFail '%s': %s", streamName.c_str(), msg.c_str());
|
|
}else{
|
|
MEDIUM_MSG("onFail '%s': %s", streamName.c_str(), msg.c_str());
|
|
}
|
|
Util::logExitReason(ER_UNKNOWN, msg.c_str());
|
|
isInitialized = false;
|
|
wantRequest = false;
|
|
parseData = false;
|
|
myConn.close();
|
|
}
|
|
|
|
void Output::initialize(){
|
|
MEDIUM_MSG("initialize");
|
|
if (isInitialized){return;}
|
|
if (streamName.size() < 1){
|
|
return; // abort - no stream to initialize...
|
|
}
|
|
reconnect();
|
|
// if the connection failed, fail
|
|
if (!meta || streamName.size() < 1){
|
|
onFail("Could not connect to stream", true);
|
|
return;
|
|
}
|
|
sought = false;
|
|
/*LTS-START*/
|
|
if (Triggers::shouldTrigger("CONN_PLAY", streamName)){
|
|
std::string payload =
|
|
streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n" + reqUrl;
|
|
if (!Triggers::doTrigger("CONN_PLAY", payload, streamName)){
|
|
onFail("Not allowed to play (CONN_PLAY)");
|
|
}
|
|
}
|
|
/*LTS-END*/
|
|
}
|
|
|
|
std::string Output::getConnectedHost(){return myConn.getHost();}
|
|
|
|
std::string Output::getConnectedBinHost(){
|
|
if (!prevHost.size()){
|
|
MEDIUM_MSG("Setting prevHost to %s", getConnectedHost().c_str());
|
|
prevHost = myConn.getBinHost();
|
|
if (!prevHost.size()){prevHost.assign("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16);}
|
|
}
|
|
return prevHost;
|
|
}
|
|
|
|
bool Output::isReadyForPlay(){
|
|
// If a protocol does not support any codecs, we assume you know what you're doing
|
|
if (!capa.isMember("codecs") || !capa["codecs"].size() || !capa["codecs"].isArray() || !capa["codecs"][0u].size()){return true;}
|
|
if (!isInitialized){return false;}
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
if (getSupportedTracks().size()){
|
|
size_t minTracks = 2;
|
|
size_t minMs = 5000;
|
|
if (targetParams.count("waittrackcount")){
|
|
minTracks = JSON::Value(targetParams["waittrackcount"]).asInt();
|
|
minMs = 120000;
|
|
}
|
|
if (targetParams.count("maxwaittrackms")){
|
|
minMs = JSON::Value(targetParams["maxwaittrackms"]).asInt();
|
|
}
|
|
if (!userSelect.size()){selectDefaultTracks();}
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (mainTrack != INVALID_TRACK_ID){
|
|
DTSC::Keys keys(M.keys(mainTrack));
|
|
if (keys.getValidCount() >= minTracks || M.getNowms(mainTrack) - M.getFirstms(mainTrack) > minMs){
|
|
return true;
|
|
}
|
|
HIGH_MSG("NOT READY YET (%zu tracks, main track: %zu, with %zu keys)",
|
|
M.getValidTracks().size(), getMainSelectedTrack(), keys.getValidCount());
|
|
}else{
|
|
HIGH_MSG("NOT READY YET (%zu tracks)", getSupportedTracks().size());
|
|
}
|
|
}else{
|
|
HIGH_MSG("NOT READY (%zu tracks)", getSupportedTracks().size());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Disconnects from all stat/user/metadata-related shared structures.
|
|
void Output::disconnect(){
|
|
MEDIUM_MSG("disconnect");
|
|
if (statComm){
|
|
stats(true);
|
|
statComm.unload();
|
|
myConn.resetCounter();
|
|
}
|
|
userSelect.clear();
|
|
isInitialized = false;
|
|
meta.clear();
|
|
}
|
|
|
|
/// Connects or reconnects to the stream.
|
|
/// Assumes streamName class member has been set already.
|
|
/// Will start input if not currently active, calls onFail() if this does not succeed.
|
|
void Output::reconnect(){
|
|
Comms::sessionConfigCache();
|
|
thisPacket.null();
|
|
if (config->hasOption("noinput") && config->getBool("noinput")){
|
|
Util::sanitizeName(streamName);
|
|
if (!Util::streamAlive(streamName)){
|
|
onFail("Stream not active already, aborting");
|
|
return;
|
|
}
|
|
}else{
|
|
if (!Util::startInput(streamName, "", true, isPushing())){
|
|
// If stream is configured, use fallback stream setting, if set.
|
|
JSON::Value strCnf = Util::getStreamConfig(streamName);
|
|
if (strCnf && strCnf["fallback_stream"].asStringRef().size()){
|
|
std::string defStrm = strCnf["fallback_stream"].asStringRef();
|
|
std::string newStrm = defStrm;
|
|
Util::streamVariables(newStrm, streamName, "");
|
|
INFO_MSG("Switching to configured fallback stream '%s' -> '%s'", defStrm.c_str(), newStrm.c_str());
|
|
streamName = newStrm;
|
|
Util::setStreamName(streamName);
|
|
reconnect();
|
|
return;
|
|
}
|
|
|
|
// Not configured or no fallback stream? Use the default stream handler instead
|
|
// Note: Since fallback stream is handled recursively, the defaultStream handler
|
|
// may still be triggered for the fallback stream! This is intentional.
|
|
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::setStreamName(streamName);
|
|
if (!Util::startInput(streamName, "", true, isPushing())){
|
|
onFail("Stream open failed (fallback stream for '" + origStream + "')", true);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Wipe currently selected tracks; metadata unload coming up
|
|
userSelect.clear();
|
|
|
|
//Connect to stream metadata
|
|
meta.reInit(streamName, false);
|
|
unsigned int attempts = 0;
|
|
while (!meta && ++attempts < 20 && Util::streamAlive(streamName)){
|
|
meta.reInit(streamName, false);
|
|
}
|
|
//Abort if this step failed
|
|
if (!meta){return;}
|
|
|
|
isInitialized = true;
|
|
|
|
//Connect to stats reporting, if not connected already
|
|
stats(true);
|
|
//Abort if the stats code shut us down just now
|
|
if (!isInitialized){return;}
|
|
|
|
//push inputs do not need to wait for stream to be ready for playback
|
|
if (isPushing()){return;}
|
|
|
|
//live streams that are no push outputs (recordings), wait for stream to be ready
|
|
if (!isRecording() && M.getLive() && !isReadyForPlay()){
|
|
uint64_t waitUntil = Util::bootSecs() + 45;
|
|
while (M.getLive() && !isReadyForPlay()){
|
|
if (Util::bootSecs() > waitUntil || (!userSelect.size() && Util::bootSecs() > waitUntil)){
|
|
INFO_MSG("Giving up waiting for playable tracks. IP: %s", getConnectedHost().c_str());
|
|
break;
|
|
}
|
|
Util::wait(500);
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
stats();
|
|
}
|
|
}
|
|
//Finally, select the default tracks
|
|
selectDefaultTracks();
|
|
}
|
|
|
|
std::set<size_t> Output::getSupportedTracks(const std::string &type) const{
|
|
return Util::getSupportedTracks(M, capa, type);
|
|
}
|
|
|
|
/// Automatically selects the tracks that are possible and/or wanted.
|
|
/// Returns true if the track selection changed in any way.
|
|
bool Output::selectDefaultTracks(){
|
|
if (!isInitialized){
|
|
initialize();
|
|
if (!isInitialized){return false;}
|
|
}
|
|
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
if (!M){
|
|
userSelect.clear();
|
|
buffer.clear();
|
|
return true;
|
|
}
|
|
|
|
bool autoSeek = buffer.size();
|
|
uint64_t seekTarget = buffer.getSyncMode()?thisTime:0;
|
|
std::set<size_t> newSelects =
|
|
Util::wouldSelect(M, targetParams, capa, UA, autoSeek ? seekTarget : 0);
|
|
|
|
if (autoSeek){
|
|
std::set<size_t> toRemove;
|
|
for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){
|
|
// autoSeeking and target not in bounds? Drop it too.
|
|
if (M.getNowms(*it) < std::max(seekTarget, (uint64_t)6000lu) - 6000){
|
|
toRemove.insert(*it);
|
|
}
|
|
}
|
|
// remove those from selectedtracks
|
|
for (std::set<size_t>::iterator it = toRemove.begin(); it != toRemove.end(); it++){
|
|
newSelects.erase(*it);
|
|
}
|
|
}
|
|
|
|
std::set<size_t> oldSelects;
|
|
buffer.getTrackList(oldSelects);
|
|
std::map<size_t, uint64_t> seekTargets;
|
|
buffer.getTrackList(seekTargets);
|
|
|
|
//No changes? Abort and return false;
|
|
if (oldSelects == newSelects){return false;}
|
|
|
|
//Temp set holding the differences between old and new track selections
|
|
std::set<size_t> diffs;
|
|
|
|
//Find elements in old selection but not in new selection
|
|
std::set_difference(oldSelects.begin(), oldSelects.end(), newSelects.begin(), newSelects.end(), std::inserter(diffs, diffs.end()));
|
|
if (diffs.size()){MEDIUM_MSG("Dropping %zu tracks", diffs.size());}
|
|
for (std::set<size_t>::iterator it = diffs.begin(); it != diffs.end(); it++){
|
|
HIGH_MSG("Dropping track %zu", *it);
|
|
userSelect.erase(*it);
|
|
}
|
|
|
|
//Find elements in new selection but not in old selection
|
|
diffs.clear();
|
|
std::set_difference(newSelects.begin(), newSelects.end(), oldSelects.begin(), oldSelects.end(), std::inserter(diffs, diffs.end()));
|
|
if (diffs.size()){MEDIUM_MSG("Adding %zu tracks", diffs.size());}
|
|
for (std::set<size_t>::iterator it = diffs.begin(); it != diffs.end(); it++){
|
|
HIGH_MSG("Adding track %zu", *it);
|
|
userSelect[*it].reload(streamName, *it);
|
|
if (!userSelect[*it]){
|
|
WARN_MSG("Could not select track %zu, dropping track", *it);
|
|
newSelects.erase(*it);
|
|
userSelect.erase(*it);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
newSelects.clear();
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); ++it){
|
|
newSelects.insert(it->first);
|
|
}
|
|
|
|
//After attempting to add/remove tracks, now no changes? Abort and return false;
|
|
if (oldSelects == newSelects){return false;}
|
|
|
|
if (autoSeek){
|
|
buffer.clear();
|
|
INFO_MSG("Automatically seeking to resume playback");
|
|
if (!seekTargets.size()){
|
|
initialSeek();
|
|
}else{
|
|
for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){
|
|
if (seekTargets.count(*it)){
|
|
seek(*it, seekTargets[*it], false);
|
|
}else{
|
|
seek(*it, seekTargets.begin()->second, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Clears the buffer, sets parseData to false, and generally makes not very much happen at all.
|
|
void Output::stop(){
|
|
buffer.clear();
|
|
parseData = false;
|
|
}
|
|
|
|
///Returns the timestamp of the next upcoming keyframe after thisPacket, or 0 if that cannot be determined (yet).
|
|
uint64_t Output::nextKeyTime(){
|
|
size_t trk = thisPacket.getTrackId();
|
|
//If this is a video packet, we assume this is the main track and we don't look anything up
|
|
if (M.getType(trk) != "video"){
|
|
//For non-video packets, we get the main selected track
|
|
trk = getMainSelectedTrack();
|
|
//Unless that gives us an invalid track ID, then we fall back to the current track
|
|
if (trk == INVALID_TRACK_ID){trk = thisPacket.getTrackId();}
|
|
}
|
|
//Abort if the track is not loaded
|
|
if (!M.trackLoaded(trk)){return 0;}
|
|
const DTSC::Keys &keys = M.keys(trk);
|
|
//Abort if there are no keys
|
|
if (!keys.getValidCount()){return 0;}
|
|
//Get the key for the current time
|
|
size_t keyNum = M.getKeyNumForTime(trk, thisTime);
|
|
if (keyNum == INVALID_KEY_NUM){return 0;}
|
|
if (keys.getEndValid() <= keyNum+1){return 0;}
|
|
//Return the next key
|
|
return keys.getTime(keyNum+1);
|
|
}
|
|
|
|
uint64_t Output::pageNumForKey(size_t trackId, size_t keyNum){
|
|
const Util::RelAccX &tPages = M.pages(trackId);
|
|
for (uint64_t i = tPages.getDeleted(); i < tPages.getEndPos(); i++){
|
|
uint64_t pageNum = tPages.getInt("firstkey", i);
|
|
if (pageNum > keyNum) continue;
|
|
uint64_t pageKeys = tPages.getInt("keycount", i);
|
|
if (keyNum > pageNum + pageKeys - 1) continue;
|
|
uint64_t pageAvail = tPages.getInt("avail", i);
|
|
return pageAvail == 0 ? INVALID_KEY_NUM : pageNum;
|
|
}
|
|
return INVALID_KEY_NUM;
|
|
}
|
|
|
|
/// Gets the highest page number available for the given trackId.
|
|
uint64_t Output::pageNumMax(size_t trackId){
|
|
const Util::RelAccX &tPages = M.pages(trackId);
|
|
uint64_t highest = 0;
|
|
for (uint64_t i = tPages.getDeleted(); i < tPages.getEndPos(); i++){
|
|
uint64_t pageNum = tPages.getInt("firstkey", i);
|
|
if (pageNum > highest){highest = pageNum;}
|
|
}
|
|
return highest;
|
|
}
|
|
|
|
/// \brief Removes entries in the playlist based on age or a maximum number of segments allowed
|
|
/// \param playlistBuffer: the contents of the playlist file. Will be edited to contain fewer entries if applicable
|
|
/// \param targetAge: maximum age of a segment in seconds. If 0, will not remove segments based on age
|
|
/// \param maxEntries: maximum amount of segments that are allowed to appear in the playlist
|
|
/// If 0, will not remove segments based on the segment count
|
|
/// \param segmentCount: current counter of segments that have been segmented as part of this stream
|
|
/// \param segmentsRemoved: counter of segments that have been removed previously from the playlist
|
|
/// \param curTime: the current local timestamp in milliseconds
|
|
/// \param targetDuration: value to fill in for the EXT-X-TARGETDURATION entry in the playlist
|
|
/// \param playlistLocation: the location of the playlist, used to find the path to segments when removing them
|
|
void Output::reinitPlaylist(std::string &playlistBuffer, uint64_t &targetAge, uint64_t &maxEntries,
|
|
uint64_t &segmentCount, uint64_t &segmentsRemoved, uint64_t &curTime,
|
|
std::string targetDuration, HTTP::URL &playlistLocation){
|
|
std::string newBuffer;
|
|
std::istringstream stream(playlistBuffer);
|
|
std::string line;
|
|
std::string curDateString;
|
|
std::string curDurationString;
|
|
// Quits early if we have no more segments we need to remove
|
|
bool done = false;
|
|
bool hasSegment = false;
|
|
while (std::getline(stream, line)){
|
|
if (!line.size()){continue;}
|
|
// Copy the rest of the file as is
|
|
if (done){
|
|
newBuffer += line + "\n";
|
|
continue;
|
|
}
|
|
// Ignore init fields
|
|
if (strncmp(line.c_str(), "#EXTM3U", 7) == 0){continue;}
|
|
if (strncmp(line.c_str(), "#EXT-X-VERSION", 14) == 0){continue;}
|
|
if (strncmp(line.c_str(), "#EXT-X-PLAYLIST-TYPE", 20) == 0){continue;}
|
|
if (strncmp(line.c_str(), "#EXT-X-TARGETDURATION", 21) == 0){continue;}
|
|
if (strncmp(line.c_str(), "#EXT-X-MEDIA-SEQUENCE", 21) == 0){continue;}
|
|
if (!hasSegment && strncmp(line.c_str(), "#EXT-X-DISCONTINUITY", 20) == 0){continue;}
|
|
// Save current segment info
|
|
if (strncmp(line.c_str(), "#EXTINF", 7) == 0){
|
|
curDurationString = line;
|
|
continue;
|
|
}
|
|
if (strncmp(line.c_str(), "#EXT-X-PROGRAM-DATE-TIME", 21) == 0){
|
|
curDateString = line;
|
|
continue;
|
|
}
|
|
// Pass along any other lines starting with a # character as is
|
|
if (line[0] == '#'){
|
|
newBuffer += line + "\n";
|
|
continue;
|
|
}
|
|
// The current line should be a segment path at this point
|
|
// If we are above the max segment count or age, ignore this segment and reset info fields
|
|
if (maxEntries && (segmentCount - segmentsRemoved >= maxEntries)){
|
|
HIGH_MSG("Dropping segment #%" PRIu64 " from the playlist due to the playlist reaching it's max size of %" PRIu64 " segments", segmentsRemoved, maxEntries);
|
|
curDateString = "";
|
|
curDurationString = "";
|
|
segmentsRemoved++;
|
|
std::string segPath = playlistLocation.link(line).getFilePath();
|
|
if(unlink(segPath.c_str())){
|
|
FAIL_MSG("Failed to remove segment at '%s'. Error: '%s'", segPath.c_str(), strerror(errno));
|
|
}else{
|
|
INFO_MSG("Removed segment at '%s'", segPath.c_str());
|
|
}
|
|
continue;
|
|
}
|
|
if (targetAge && curDateString.size() > 25){
|
|
uint64_t segmentDiff = Util::getUTCTimeDiff(curDateString.substr(25), curTime);
|
|
if (segmentDiff > targetAge){
|
|
HIGH_MSG("Dropping segment #%" PRIu64 " from the playlist due to old age (%" PRIu64 " s)", segmentsRemoved, segmentDiff);
|
|
// If the segment is too old, ignore and reset fields
|
|
curDurationString = "";
|
|
curDateString = "";
|
|
segmentsRemoved++;
|
|
std::string segPath = playlistLocation.link(line).getFilePath();
|
|
if(unlink(segPath.c_str())){
|
|
FAIL_MSG("Failed to remove segment at '%s'. Error: '%s'", segPath.c_str(), strerror(errno));
|
|
}else{
|
|
INFO_MSG("Removed segment at '%s'", segPath.c_str());
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
hasSegment = true;
|
|
// Write segment info to the new buffer
|
|
if (curDateString.size()){
|
|
newBuffer += curDateString + "\n";
|
|
curDateString = "";
|
|
}
|
|
if (curDurationString.size()){
|
|
newBuffer += curDurationString + "\n";
|
|
curDurationString = "";
|
|
}
|
|
newBuffer += line + "\n";
|
|
// If we reach this point, the conditions of max age and entries have been met
|
|
done = true;
|
|
}
|
|
// Write out new init data to the playlist buffer
|
|
playlistBuffer = "#EXTM3U\n#EXT-X-VERSION:3\n";
|
|
// Set the playlist as immutable when segmenting a non-live input
|
|
if (!M.getLive()){
|
|
playlistBuffer += "#EXT-X-PLAYLIST-TYPE:VOD\n";
|
|
// Set the playlist as append only when segmenting a live input without removing older entries
|
|
} else if (!maxEntries && !targetAge){
|
|
playlistBuffer += "#EXT-X-PLAYLIST-TYPE:EVENT\n";
|
|
}
|
|
// Else don't add a playlist type at all, as the playlist will not be append only or immutable
|
|
playlistBuffer += "#EXT-X-TARGETDURATION:" + targetDuration + "\n#EXT-X-MEDIA-SEQUENCE:" + JSON::Value(segmentsRemoved).asString() + "\n";
|
|
// Finally append the rest of the playlist
|
|
playlistBuffer += newBuffer;
|
|
}
|
|
|
|
/// Loads the page for the given trackId and keyNum into memory.
|
|
/// Overwrites any existing page for the same trackId.
|
|
/// Automatically calls thisPacket.null() if necessary.
|
|
void Output::loadPageForKey(size_t trackId, size_t keyNum){
|
|
if (!M.trackValid(trackId)){
|
|
WARN_MSG("Load for track %zu key %zu aborted - track does not exist", trackId, keyNum);
|
|
return;
|
|
}
|
|
if (!M.trackLoaded(trackId)){meta.reloadReplacedPagesIfNeeded();}
|
|
DTSC::Keys keys(M.keys(trackId));
|
|
if (!keys.getValidCount()){
|
|
WARN_MSG("Load for track %zu key %zu aborted - track is empty", trackId, keyNum);
|
|
return;
|
|
}
|
|
size_t lastAvailKey = keys.getEndValid() - 1;
|
|
if (!meta.getLive() && keyNum > lastAvailKey){
|
|
INFO_MSG("Load for track %zu key %zu aborted, is > %zu", trackId, keyNum, lastAvailKey);
|
|
curPage.erase(trackId);
|
|
currentPage.erase(trackId);
|
|
return;
|
|
}
|
|
uint64_t micros = Util::getMicros();
|
|
VERYHIGH_MSG("Loading track %zu, containing key %zu", trackId, keyNum);
|
|
uint32_t timeout = 0;
|
|
uint32_t pageNum = pageNumForKey(trackId, keyNum);
|
|
while (keepGoing() && pageNum == INVALID_KEY_NUM){
|
|
if (!timeout){HIGH_MSG("Requesting page with key %zu:%zu", trackId, keyNum);}
|
|
++timeout;
|
|
//Time out after 15 seconds
|
|
if (timeout > 300){
|
|
FAIL_MSG("Timeout while waiting for requested key %zu for track %zu. Aborting.", keyNum, trackId);
|
|
curPage.erase(trackId);
|
|
currentPage.erase(trackId);
|
|
return;
|
|
}
|
|
if (!userSelect.count(trackId) || !userSelect[trackId]){
|
|
WARN_MSG("Loading page for non-selected track %zu", trackId);
|
|
}else{
|
|
userSelect[trackId].setKeyNum(keyNum);
|
|
}
|
|
|
|
stats(true);
|
|
playbackSleep(50);
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
pageNum = pageNumForKey(trackId, keyNum);
|
|
}
|
|
|
|
if (!keepGoing()){
|
|
INFO_MSG("Aborting page load due to shutdown");
|
|
return;
|
|
}
|
|
|
|
if (!userSelect.count(trackId) || !userSelect[trackId]){
|
|
WARN_MSG("Loading page for non-selected track %zu", trackId);
|
|
}else{
|
|
userSelect[trackId].setKeyNum(keyNum);
|
|
}
|
|
|
|
stats(true);
|
|
|
|
if (currentPage.count(trackId) && currentPage[trackId] == pageNum){return;}
|
|
// If we're loading the track thisPacket is on, null it to prevent accesses.
|
|
if (thisPacket && thisIdx == trackId){thisPacket.null();}
|
|
char id[NAME_BUFFER_SIZE];
|
|
snprintf(id, NAME_BUFFER_SIZE, SHM_TRACK_DATA, streamName.c_str(), trackId, pageNum);
|
|
curPage[trackId].init(id, DEFAULT_DATA_PAGE_SIZE);
|
|
if (!(curPage[trackId].mapped)){
|
|
FAIL_MSG("Initializing page %s failed", curPage[trackId].name.c_str());
|
|
currentPage.erase(trackId);
|
|
return;
|
|
}
|
|
currentPage[trackId] = pageNum;
|
|
micros = Util::getMicros(micros);
|
|
if (micros > 2000000){
|
|
INFO_MSG("Page %s loaded for %s in %.2fms", id, streamName.c_str(), micros/1000.0);
|
|
}else{
|
|
VERYHIGH_MSG("Page %s loaded for %s in %.2fms", id, streamName.c_str(), micros/1000.0);
|
|
}
|
|
}
|
|
|
|
/// Return the current time of the media buffer, or 0 if no buffer available.
|
|
uint64_t Output::currentTime(){
|
|
if (!buffer.size()){return 0;}
|
|
return buffer.begin()->time;
|
|
}
|
|
|
|
/// Return the intended target current time of the media buffer (as opposed to actual)
|
|
/// This takes into account the current playback speed as well as the maxSkipAhead setting.
|
|
uint64_t Output::targetTime(){
|
|
if (!realTime){return currentTime();}
|
|
return (((Util::bootMS() - firstTime) * 1000) / realTime + maxSkipAhead);
|
|
}
|
|
|
|
/// Return the start time of the selected tracks.
|
|
/// Returns the start time of earliest track if nothing is selected.
|
|
/// Returns zero if no tracks exist.
|
|
uint64_t Output::startTime(){
|
|
std::set<size_t> validTracks = M.getValidTracks();
|
|
if (!validTracks.size()){return 0;}
|
|
uint64_t start = 0xFFFFFFFFFFFFFFFFull;
|
|
if (userSelect.size()){
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
if (M.trackValid(it->first) && start > M.getFirstms(it->first)){
|
|
start = M.getFirstms(it->first);
|
|
}
|
|
}
|
|
}else{
|
|
for (std::set<size_t>::iterator it = validTracks.begin(); it != validTracks.end(); it++){
|
|
if (start > M.getFirstms(*it)){start = M.getFirstms(*it);}
|
|
}
|
|
}
|
|
return start;
|
|
}
|
|
|
|
/// Return the end time of the selected tracks, or 0 if unknown or live.
|
|
/// Returns the end time of latest track if nothing is selected.
|
|
/// Returns zero if no tracks exist.
|
|
uint64_t Output::endTime(){
|
|
std::set<size_t> validTracks = M.getValidTracks();
|
|
if (!validTracks.size()){return 0;}
|
|
uint64_t end = 0;
|
|
if (userSelect.size()){
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
if (M.trackValid(it->first) && end < M.getLastms(it->first)){
|
|
end = meta.getLastms(it->first);
|
|
}
|
|
}
|
|
}else{
|
|
for (std::set<size_t>::iterator it = validTracks.begin(); it != validTracks.end(); it++){
|
|
if (end < meta.getLastms(*it)){end = meta.getLastms(*it);}
|
|
}
|
|
}
|
|
return end;
|
|
}
|
|
|
|
/// Prepares all tracks from selectedTracks for seeking to the specified ms position.
|
|
void Output::seekKeyframesIn(unsigned long long pos, unsigned long long maxDelta){
|
|
sought = true;
|
|
if (!isInitialized){initialize();}
|
|
buffer.clear();
|
|
thisPacket.null();
|
|
MEDIUM_MSG("Seeking keyframes near %llums, max delta of %llu", pos, maxDelta);
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
if (!M.getValidTracks().count(it->first)){continue;}
|
|
uint64_t time = M.getTimeForKeyIndex(it->first, M.getKeyIndexForTime(it->first, pos));
|
|
uint64_t timeDelta = M.getTimeForKeyIndex(it->first, M.getKeyIndexForTime(it->first, pos + maxDelta));
|
|
if (time >= (pos - maxDelta)){
|
|
pos = time;
|
|
}else if (timeDelta >= (pos - maxDelta)){
|
|
pos = timeDelta;
|
|
}
|
|
seek(it->first, pos, false);
|
|
}
|
|
}
|
|
|
|
/// Prepares all tracks from selectedTracks for seeking to the specified ms position.
|
|
/// If toKey is true, clips the seek to the nearest keyframe if the main track is a video track.
|
|
bool Output::seek(uint64_t pos, bool toKey){
|
|
sought = true;
|
|
if (!isInitialized){initialize();}
|
|
buffer.clear();
|
|
thisPacket.null();
|
|
if (toKey){
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (mainTrack == INVALID_TRACK_ID){
|
|
WARN_MSG("Sync-seeking impossible (main track invalid); performing regular seek instead");
|
|
return seek(pos);
|
|
}
|
|
if (M.getType(mainTrack) == "video"){
|
|
DTSC::Keys keys(M.keys(mainTrack));
|
|
uint32_t keyNum = M.getKeyNumForTime(mainTrack, pos);
|
|
if (keyNum == INVALID_KEY_NUM){
|
|
FAIL_MSG("Attempted seek on empty track %zu", mainTrack);
|
|
return false;
|
|
}
|
|
pos = keys.getTime(keyNum);
|
|
}
|
|
}
|
|
MEDIUM_MSG("Seeking to %" PRIu64 "ms (%s)", pos, toKey ? "sync" : "direct");
|
|
std::set<size_t> seekTracks;
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
seekTracks.insert(it->first);
|
|
}
|
|
//Seek all seek positions, first
|
|
for (std::set<size_t>::iterator it = seekTracks.begin(); it != seekTracks.end(); it++){
|
|
userSelect[*it].setKeyNum(M.getKeyNumForTime(*it, pos));
|
|
}
|
|
bool ret = seekTracks.size();
|
|
for (std::set<size_t>::iterator it = seekTracks.begin(); it != seekTracks.end(); it++){
|
|
ret &= seek(*it, pos, false);
|
|
}
|
|
firstTime = Util::bootMS() - (currentTime() * realTime / 1000);
|
|
return ret;
|
|
}
|
|
|
|
bool Output::seek(size_t tid, uint64_t pos, bool getNextKey){
|
|
if (!M.trackValid(tid)){
|
|
MEDIUM_MSG("Aborting seek to %" PRIu64 "ms in track %zu: Invalid track id.", pos, tid);
|
|
userSelect.erase(tid);
|
|
return false;
|
|
}
|
|
if (!M.trackLoaded(tid)){meta.reloadReplacedPagesIfNeeded();}
|
|
if (!userSelect.count(tid) || !userSelect[tid]){
|
|
WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: user select failure (%s)", pos, tid, userSelect.count(tid)?"not connected":"not selected");
|
|
userSelect.erase(tid);
|
|
return false;
|
|
}
|
|
|
|
HIGH_MSG("Seeking for pos %" PRIu64, pos);
|
|
if (meta.getLive() && meta.getNowms(tid) < pos){
|
|
unsigned int maxTime = 0;
|
|
while (meta.getNowms(tid) < pos && myConn && ++maxTime <= 20 && keepGoing()){
|
|
Util::wait(500);
|
|
stats();
|
|
}
|
|
}
|
|
if (meta.getNowms(tid) < pos){
|
|
WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: past end of track (= %" PRIu64 "ms).",
|
|
pos, tid, meta.getNowms(tid));
|
|
userSelect.erase(tid);
|
|
return false;
|
|
}
|
|
DTSC::Keys keys(M.keys(tid));
|
|
if (M.getLive() && !pos && !buffer.getSyncMode()){
|
|
uint64_t tmpTime = (M.getFirstms(tid) + M.getLastms(tid))/2;
|
|
uint32_t tmpKey = M.getKeyNumForTime(tid, tmpTime);
|
|
pos = keys.getTime(tmpKey);
|
|
}
|
|
uint32_t keyNum = M.getKeyNumForTime(tid, pos);
|
|
if (keyNum == INVALID_KEY_NUM){
|
|
FAIL_MSG("Attempted seek on empty track %zu", tid);
|
|
return false;
|
|
}
|
|
uint64_t actualKeyTime = keys.getTime(keyNum);
|
|
HIGH_MSG("Seeking to track %zu key %" PRIu32 " => time %" PRIu64, tid, keyNum, pos);
|
|
emptyCount = 0;
|
|
if (actualKeyTime > pos){
|
|
pos = actualKeyTime;
|
|
userSelect[tid].setKeyNum(keyNum);
|
|
}
|
|
loadPageForKey(tid, keyNum + (getNextKey ? 1 : 0));
|
|
if (!curPage.count(tid) || !curPage[tid].mapped){
|
|
//Sometimes the page load fails because of a connection loss to the user. This is fine.
|
|
if (keepGoing()){
|
|
WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: not available.", pos, tid);
|
|
userSelect.erase(tid);
|
|
}
|
|
return false;
|
|
}
|
|
Util::sortedPageInfo tmp;
|
|
tmp.tid = tid;
|
|
tmp.offset = 0;
|
|
tmp.partIndex = 0;
|
|
DTSC::Packet tmpPack;
|
|
tmpPack.reInit(curPage[tid].mapped + tmp.offset, 0, true);
|
|
tmp.time = tmpPack.getTime();
|
|
char *mpd = curPage[tid].mapped;
|
|
uint64_t nowMs = M.getNowms(tid);
|
|
while (tmp.time < pos && tmpPack){
|
|
tmp.offset += tmpPack.getDataLen();
|
|
tmpPack.reInit(mpd + tmp.offset, 0, true);
|
|
tmp.time = tmpPack.getTime();
|
|
}
|
|
if (tmpPack){
|
|
tmp.ghostPacket = false;
|
|
HIGH_MSG("Sought to time %" PRIu64 " in %s", tmp.time, curPage[tid].name.c_str());
|
|
tmp.partIndex = M.getPartIndex(tmpPack.getTime(), tmp.tid);
|
|
buffer.insert(tmp);
|
|
return true;
|
|
}
|
|
tmp.partIndex = M.getPartIndex(nowMs, tmp.tid);
|
|
tmp.ghostPacket = true;
|
|
tmp.time = nowMs;
|
|
buffer.insert(tmp);
|
|
return true;
|
|
}
|
|
|
|
/// This function decides where in the stream initial playback starts.
|
|
/// The default implementation calls seek(0) for VoD.
|
|
/// For live, it seeks to the last sync'ed keyframe of the main track, no closer than
|
|
/// needsLookAhead+minKeepAway ms from the end. Unless lastms < 5000, then it seeks to the first
|
|
/// keyframe of the main track. Aborts if there is no main track or it has no keyframes.
|
|
void Output::initialSeek(bool dryRun){
|
|
if (!meta){return;}
|
|
meta.removeLimiter();
|
|
uint64_t seekPos = 0;
|
|
if (meta.getLive() && buffer.getSyncMode()){
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (mainTrack == INVALID_TRACK_ID){return;}
|
|
DTSC::Keys keys(M.keys(mainTrack));
|
|
if (!keys.getValidCount()){return;}
|
|
// seek to the newest keyframe, unless that is <5s, then seek to the oldest keyframe
|
|
uint32_t firstKey = keys.getFirstValid();
|
|
uint32_t lastKey = keys.getEndValid() - 1;
|
|
for (int64_t i = lastKey; i >= firstKey; i--){
|
|
seekPos = keys.getTime(i);
|
|
if (seekPos < 5000){continue;}// if we're near the start, skip back
|
|
bool good = true;
|
|
// check if all tracks have data for this point in time
|
|
for (std::map<size_t, Comms::Users>::iterator ti = userSelect.begin(); ti != userSelect.end(); ++ti){
|
|
if (meta.getNowms(ti->first) < seekPos + needsLookAhead){
|
|
good = false;
|
|
break;
|
|
}
|
|
if (mainTrack == ti->first){continue;}// skip self
|
|
if (!M.trackValid(ti->first)){
|
|
HIGH_MSG("Skipping track %zu, not in tracks", ti->first);
|
|
continue;
|
|
}// ignore missing tracks
|
|
if (M.getNowms(ti->first) < seekPos + needsLookAhead + M.getMinKeepAway(ti->first)){
|
|
good = false;
|
|
break;
|
|
}
|
|
if (meta.getNowms(ti->first) == M.getFirstms(ti->first)){
|
|
HIGH_MSG("Skipping track %zu, last equals first", ti->first);
|
|
continue;
|
|
}// ignore point-tracks
|
|
if (meta.getNowms(ti->first) < seekPos){
|
|
good = false;
|
|
break;
|
|
}
|
|
HIGH_MSG("Track %zu is good", ti->first);
|
|
}
|
|
// if yes, seek here
|
|
if (good){break;}
|
|
}
|
|
}
|
|
/*LTS-START*/
|
|
if (isRecordingToFile){
|
|
if (M.getLive()){
|
|
MEDIUM_MSG("Stream currently contains data from %" PRIu64 " ms to %" PRIu64 " ms", startTime(), endTime());
|
|
}
|
|
// Overwrite recstart/recstop with recstartunix/recstopunix if set
|
|
if (M.getLive() && (
|
|
targetParams.count("recstartunix") || targetParams.count("recstopunix") ||
|
|
targetParams.count("startunix") || targetParams.count("stopunix") ||
|
|
targetParams.count("unixstart") || targetParams.count("unixstop")
|
|
)){
|
|
uint64_t zUTC = M.getUTCOffset();
|
|
if (!zUTC){
|
|
if (!M.getLive()){
|
|
WARN_MSG("Attempting to set unix-based start/stop time for a VoD asset without known UTC timestamp! This will likely not work as you expect, since we have nothing to base the timestamps on");
|
|
}else{
|
|
zUTC = M.getBootMsOffset() + Util::getGlobalConfig("systemBoot").asInt();
|
|
}
|
|
}
|
|
if (targetParams.count("recstartunix")){
|
|
int64_t startUnix = atoll(targetParams["recstartunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (startUnix <= 36000000){startUnix += Util::unixMS();}
|
|
targetParams["recstart"] = JSON::Value(startUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("recstopunix")){
|
|
int64_t stopUnix = atoll(targetParams["recstopunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (stopUnix <= 36000000){stopUnix += Util::unixMS();}
|
|
targetParams["recstop"] = JSON::Value(stopUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("unixstart")){
|
|
int64_t startUnix = atoll(targetParams["unixstart"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (startUnix <= 36000000){startUnix += Util::unixMS();}
|
|
targetParams["recstart"] = JSON::Value(startUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("unixstop")){
|
|
int64_t stopUnix = atoll(targetParams["unixstop"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (stopUnix <= 36000000){stopUnix += Util::unixMS();}
|
|
targetParams["recstop"] = JSON::Value(stopUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("startunix")){
|
|
int64_t startUnix = atoll(targetParams["startunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (startUnix <= 36000000){startUnix += Util::unixMS();}
|
|
targetParams["recstart"] = JSON::Value(startUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("stopunix")){
|
|
int64_t stopUnix = atoll(targetParams["stopunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (stopUnix <= 36000000){stopUnix += Util::unixMS();}
|
|
targetParams["recstop"] = JSON::Value(stopUnix - zUTC).asString();
|
|
}
|
|
}
|
|
//Autoconvert start/stop to recstart/recstop to improve usability
|
|
if (targetParams.count("start") && !targetParams.count("recstart")){targetParams["recstart"] = targetParams["start"];}
|
|
if (targetParams.count("stop") && !targetParams.count("recstop")){targetParams["recstop"] = targetParams["stop"];}
|
|
// Check recstart/recstop for correctness
|
|
if (targetParams.count("recstop")){
|
|
uint64_t endRec = atoll(targetParams["recstop"].c_str());
|
|
if (endRec < startTime()){
|
|
onFail("Entire recording range is in the past", true);
|
|
return;
|
|
}
|
|
}
|
|
if (targetParams.count("recstart") && atoll(targetParams["recstart"].c_str()) != 0){
|
|
int64_t startRec = atoll(targetParams["recstart"].c_str());
|
|
if (startRec < 0){startRec = 0;}
|
|
if (startRec > endTime()){
|
|
if (!M.getLive()){
|
|
onFail("Recording start past end of non-live source", true);
|
|
return;
|
|
}
|
|
}
|
|
if (startRec < startTime()){
|
|
startRec = startTime();
|
|
WARN_MSG("Record begin at %lld ms not available, starting at %" PRIu64
|
|
" ms instead", atoll(targetParams["recstart"].c_str()), startRec);
|
|
targetParams["recstart"] = JSON::Value(startRec).asString();
|
|
}
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (M.getType(mainTrack) == "video"){
|
|
seekPos = M.getTimeForKeyIndex(mainTrack, M.getKeyIndexForTime(mainTrack, startRec));
|
|
if (seekPos != startRec){
|
|
INFO_MSG("Shifting recording start from %" PRIu64 " to %" PRIu64 " so that it starts with a keyframe", startRec, seekPos);
|
|
}
|
|
}else{
|
|
seekPos = startRec;
|
|
}
|
|
}
|
|
|
|
if (targetParams.count("split")){
|
|
long long endRec = atoll(targetParams["split"].c_str()) * 1000;
|
|
INFO_MSG("Will split recording every %lld seconds", atoll(targetParams["split"].c_str()));
|
|
targetParams["nxt-split"] = JSON::Value((int64_t)(seekPos + endRec)).asString();
|
|
}
|
|
// Duration to record in seconds. Overrides recstop.
|
|
if (targetParams.count("duration")){
|
|
int64_t endRec;
|
|
if (targetParams.count("recstart")){
|
|
endRec = atoll(targetParams["recstart"].c_str()) + atoll(targetParams["duration"].c_str()) * 1000;
|
|
}else{
|
|
endRec = seekPos + atoll(targetParams["duration"].c_str()) * 1000;
|
|
}
|
|
targetParams["recstop"] = JSON::Value(endRec).asString();
|
|
// Recheck recording end time
|
|
endRec = atoll(targetParams["recstop"].c_str());
|
|
if (endRec < 0 || endRec < startTime()){
|
|
onFail("Entire recording range is in the past", true);
|
|
return;
|
|
}
|
|
}
|
|
// Print calculated start and stop time
|
|
if (targetParams.count("recstart")){
|
|
INFO_MSG("Recording will start at timestamp %llu ms", atoll(targetParams["recstart"].c_str()));
|
|
} else{
|
|
INFO_MSG("Recording will start at timestamp %" PRIu64 " ms", endTime());
|
|
}
|
|
if (targetParams.count("recstop")){
|
|
INFO_MSG("Recording will stop at timestamp %llu ms", atoll(targetParams["recstop"].c_str()));
|
|
}
|
|
// Wait for the stream to catch up to the starttime
|
|
uint64_t streamAvail = endTime();
|
|
uint64_t lastUpdated = Util::getMS();
|
|
if (atoll(targetParams["recstart"].c_str()) > streamAvail){
|
|
INFO_MSG("Waiting for stream to reach recording starting point. Recording will start in " PRETTY_PRINT_TIME, PRETTY_ARG_TIME((atoll(targetParams["recstart"].c_str()) - streamAvail) / 1000));
|
|
while (Util::getMS() - lastUpdated < 10000 && atoll(targetParams["recstart"].c_str()) > streamAvail && keepGoing()){
|
|
Util::sleep(250);
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
if (endTime() > streamAvail){
|
|
stats();
|
|
streamAvail = endTime();
|
|
lastUpdated = Util::getMS();
|
|
}
|
|
}
|
|
}
|
|
// If we have a stop position and it's within available range,
|
|
// apply a limiter to the stream to make it appear like a VoD asset
|
|
if (targetParams.count("recstop") || !M.getLive()){
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
uint64_t stopPos = M.getLastms(mainTrack);
|
|
if (targetParams.count("recstop")){stopPos = atoll(targetParams["recstop"].c_str());}
|
|
if (!M.getLive() || stopPos <= M.getLastms(mainTrack)){meta.applyLimiter(seekPos, stopPos);}
|
|
}
|
|
}else{
|
|
if (M.getLive() && targetParams.count("pushdelay")){
|
|
INFO_MSG("Converting pushdelay syntax into corresponding recstart+realtime options");
|
|
|
|
uint64_t delayTime = JSON::Value(targetParams["pushdelay"]).asInt()*1000;
|
|
if (endTime() - startTime() < delayTime){
|
|
uint64_t waitTime = delayTime - (endTime() - startTime());
|
|
uint64_t waitTarget = Util::bootMS() + waitTime;
|
|
INFO_MSG("Waiting for buffer to fill up: waiting %" PRIu64 "ms", waitTime);
|
|
while (Util::bootMS() < waitTarget && keepGoing()){
|
|
Util::sleep(250);
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
stats();
|
|
}
|
|
if (endTime() - startTime() < delayTime){
|
|
WARN_MSG("Waited for %" PRIu64 "ms, but buffer still too small for a push delay of %" PRIu64 "ms. Doing the best we can.", waitTime, delayTime);
|
|
}
|
|
}
|
|
if (endTime() < delayTime){
|
|
INFO_MSG("Waiting for stream to reach playback starting point. Current last ms is '%" PRIu64 "'", endTime());
|
|
while (endTime() < delayTime && keepGoing()){Util::wait(250);}
|
|
}
|
|
targetParams["start"] = JSON::Value(endTime() - delayTime).asString();
|
|
targetParams["realtime"] = "1"; //force real-time speed
|
|
maxSkipAhead = 1;
|
|
}
|
|
if (targetParams.count("startunix") || targetParams.count("stopunix")){
|
|
uint64_t zUTC = M.getUTCOffset();
|
|
if (!zUTC){
|
|
if (!M.getLive()){
|
|
WARN_MSG("Attempting to set unix-based start/stop time for a VoD asset without known UTC timestamp! This will likely not work as you expect, since we have nothing to base the timestamps on");
|
|
}else{
|
|
zUTC = M.getBootMsOffset() + Util::getGlobalConfig("systemBoot").asInt();
|
|
}
|
|
}
|
|
if (targetParams.count("startunix")){
|
|
int64_t startUnix = atoll(targetParams["startunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (startUnix <= 36000000){startUnix += Util::unixMS();}
|
|
targetParams["start"] = JSON::Value(startUnix - zUTC).asString();
|
|
}
|
|
if (targetParams.count("stopunix")){
|
|
int64_t stopUnix = atoll(targetParams["stopunix"].c_str()) * 1000;
|
|
// If the time is before the first 10 hours of unix epoch, assume relative time
|
|
if (stopUnix <= 36000000){stopUnix += Util::unixMS();}
|
|
targetParams["stop"] = JSON::Value(stopUnix - zUTC).asString();
|
|
}
|
|
}
|
|
if (targetParams.count("start") && atoll(targetParams["start"].c_str()) != 0){
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
int64_t startRec = atoll(targetParams["start"].c_str());
|
|
if (startRec > M.getNowms(mainTrack)){
|
|
if (!M.getLive()){
|
|
onFail("Playback start past end of non-live source", true);
|
|
return;
|
|
}
|
|
int64_t streamAvail = M.getNowms(mainTrack);
|
|
int64_t lastUpdated = Util::getMS();
|
|
INFO_MSG("Waiting for stream to reach playback starting point (%" PRIu64 " -> %" PRIu64 "). Time left: " PRETTY_PRINT_MSTIME, startRec, streamAvail, PRETTY_ARG_MSTIME(startRec - streamAvail));
|
|
while (Util::getMS() - lastUpdated < 5000 && startRec > streamAvail && keepGoing()){
|
|
Util::sleep(500);
|
|
if (M.getNowms(mainTrack) > streamAvail){
|
|
HIGH_MSG("Waiting for stream to reach playback starting point (%" PRIu64 " -> %" PRIu64 "). Time left: " PRETTY_PRINT_MSTIME, startRec, streamAvail, PRETTY_ARG_MSTIME(startRec - streamAvail));
|
|
stats();
|
|
streamAvail = M.getNowms(mainTrack);
|
|
lastUpdated = Util::getMS();
|
|
}
|
|
}
|
|
}
|
|
if (startRec < 0 || startRec < startTime()){
|
|
WARN_MSG("Playback begin at %" PRId64 " ms not available, starting at %" PRIu64
|
|
" ms instead",
|
|
startRec, startTime());
|
|
startRec = startTime();
|
|
}
|
|
if (M.getType(mainTrack) == "video"){
|
|
seekPos = M.getTimeForKeyIndex(mainTrack, M.getKeyIndexForTime(mainTrack, startRec));
|
|
if (seekPos != startRec){
|
|
INFO_MSG("Shifting recording start from %" PRIu64 " to %" PRIu64 " so that it starts with a keyframe", startRec, seekPos);
|
|
}
|
|
}else{
|
|
seekPos = startRec;
|
|
}
|
|
INFO_MSG("Playback will start at %" PRIu64, seekPos);
|
|
}
|
|
// Duration to record in seconds. Overrides stop.
|
|
if (targetParams.count("duration")){
|
|
int64_t endRec;
|
|
if (targetParams.count("start")){
|
|
endRec = atoll(targetParams["start"].c_str()) + atoll(targetParams["duration"].c_str()) * 1000;
|
|
}else{
|
|
endRec = seekPos + atoll(targetParams["duration"].c_str()) * 1000;
|
|
}
|
|
targetParams["stop"] = JSON::Value(endRec).asString();
|
|
}
|
|
if (targetParams.count("stop")){
|
|
int64_t endRec = atoll(targetParams["stop"].c_str());
|
|
if (endRec < 0 || endRec < startTime()){
|
|
onFail("Entire range is in the past", true);
|
|
return;
|
|
}
|
|
INFO_MSG("Playback will stop at %" PRIu64, endRec);
|
|
}
|
|
// If we have a stop position and it's within available range,
|
|
// apply a limiter to the stream to make it appear like a VoD asset
|
|
if (targetParams.count("stop") || !M.getLive()){
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
uint64_t stopPos = M.getLastms(mainTrack);
|
|
if (targetParams.count("stop")){stopPos = atoll(targetParams["stop"].c_str());}
|
|
if (!M.getLive() || stopPos <= M.getLastms(mainTrack)){
|
|
meta.applyLimiter(seekPos, stopPos);
|
|
}else{
|
|
// End point past end of track? Don't limit the end point.
|
|
meta.applyLimiter(seekPos, 0xFFFFFFFFFFFFFFFFull);
|
|
}
|
|
}else{
|
|
// No stop point, only apply limiter if a start point is set, and never limit the end point.
|
|
if (targetParams.count("start")){
|
|
meta.applyLimiter(seekPos, 0xFFFFFFFFFFFFFFFFull);
|
|
}
|
|
}
|
|
}
|
|
if (dryRun){return;}
|
|
/*LTS-END*/
|
|
if (!keepGoing()){
|
|
ERROR_MSG("Aborting seek to %" PRIu64 " since the stream is no longer active", seekPos);
|
|
return;
|
|
}
|
|
if (endTime() >= atoll(targetParams["recstart"].c_str())) {
|
|
MEDIUM_MSG("Initial seek to %" PRIu64 "ms", seekPos);
|
|
seek(seekPos);
|
|
}else{
|
|
ERROR_MSG("Aborting seek to %" PRIu64 " since stream only has available from %" PRIu64 " ms to %" PRIu64 " ms", seekPos, startTime(), endTime());
|
|
}
|
|
}
|
|
|
|
/// Returns the highest getMinKeepAway of all selected tracks
|
|
uint64_t Output::getMinKeepAway(){
|
|
uint64_t r = 0;
|
|
for (std::map<size_t, Comms::Users>::iterator ti = userSelect.begin(); ti != userSelect.end(); ++ti){
|
|
if (ti->first == INVALID_TRACK_ID){continue;}
|
|
if (M.getMinKeepAway(ti->first) > r){r = M.getMinKeepAway(ti->first);}
|
|
}
|
|
//Limit the value to the maxKeepAway setting
|
|
uint64_t maxKeepAway = M.getMaxKeepAway();
|
|
if (maxKeepAway){
|
|
if (r > maxKeepAway){r = maxKeepAway;}
|
|
}
|
|
return r;
|
|
}
|
|
|
|
/// This function attempts to forward playback in live streams to a more live point.
|
|
/// It seeks to the last sync'ed keyframe of the main track, no closer than needsLookAhead+minKeepAway ms from the end.
|
|
/// Aborts if not live, there is no main track or it has no keyframes.
|
|
bool Output::liveSeek(bool rateOnly){
|
|
if (!realTime){return false;}//Makes no sense when playing in turbo mode
|
|
if (maxSkipAhead == 1){return false;}//A skipAhead of 1 signifies disabling the skipping/rate control system entirely.
|
|
if (!meta.getLive()){return false;}
|
|
uint64_t seekPos = 0;
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (mainTrack == INVALID_TRACK_ID){return false;}
|
|
uint64_t lMs = meta.getLastms(mainTrack);
|
|
uint64_t cTime = thisPacket.getTime();
|
|
uint64_t mKa = getMinKeepAway();
|
|
if (!maxSkipAhead){
|
|
bool noReturn = false;
|
|
uint64_t newSpeed = 1000;
|
|
if (lMs - mKa - needsLookAhead > cTime + 50){
|
|
// We need to speed up!
|
|
uint64_t diff = (lMs - mKa - needsLookAhead) - cTime;
|
|
if (!rateOnly && diff > 3000){
|
|
noReturn = true;
|
|
newSpeed = 1000;
|
|
}else if (diff > 1000){
|
|
newSpeed = 750;
|
|
}else if (diff > 500){
|
|
newSpeed = 900;
|
|
}else{
|
|
newSpeed = 950;
|
|
}
|
|
}
|
|
if (realTime != newSpeed){
|
|
HIGH_MSG("Changing playback speed from %" PRIu64 " to %" PRIu64 "(%" PRIu64 " ms LA, %" PRIu64 " ms mKA)", realTime, newSpeed, needsLookAhead, mKa);
|
|
firstTime = Util::bootMS() - (cTime * newSpeed / 1000);
|
|
realTime = newSpeed;
|
|
}
|
|
if (!noReturn){return false;}
|
|
}
|
|
// cancel if there are no keys in the main track
|
|
if (mainTrack == INVALID_TRACK_ID){return false;}
|
|
DTSC::Keys mainKeys(meta.keys(mainTrack));
|
|
if (!mainKeys.getValidCount()){return false;}
|
|
|
|
for (uint32_t keyNum = mainKeys.getEndValid() - 1; keyNum >= mainKeys.getFirstValid(); keyNum--){
|
|
seekPos = mainKeys.getTime(keyNum);
|
|
// Only skip forward if we can win a decent amount (100ms)
|
|
if (seekPos <= cTime + 100 * seekCount){break;}
|
|
bool good = true;
|
|
// check if all tracks have data for this point in time
|
|
for (std::map<size_t, Comms::Users>::iterator ti = userSelect.begin(); ti != userSelect.end(); ++ti){
|
|
if (ti->first == INVALID_TRACK_ID){
|
|
HIGH_MSG("Skipping track %zu, not in tracks", ti->first);
|
|
continue;
|
|
}// ignore missing tracks
|
|
if (meta.getLastms(ti->first) < seekPos + needsLookAhead + mKa){
|
|
good = false;
|
|
break;
|
|
}
|
|
if (mainTrack == ti->first){continue;}// skip self
|
|
if (meta.getLastms(ti->first) == meta.getFirstms(ti->first)){
|
|
HIGH_MSG("Skipping track %zu, last equals first", ti->first);
|
|
continue;
|
|
}// ignore point-tracks
|
|
HIGH_MSG("Track %zu is good", ti->first);
|
|
}
|
|
// if yes, seek here
|
|
if (good){
|
|
HIGH_MSG("Skipping forward %" PRIu64 "ms (%" PRIu64 " ms LA, %" PRIu64
|
|
" ms mKA, > %" PRIu32 "ms, mSa %" PRIu64 " ms)",
|
|
seekPos - cTime, needsLookAhead, mKa, seekCount * 100, maxSkipAhead);
|
|
if (seekCount < 20){++seekCount;}
|
|
seek(seekPos);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Output::requestHandler(){
|
|
if ((firstData && myConn.Received().size()) || myConn.spool()){
|
|
firstData = false;
|
|
DONTEVEN_MSG("onRequest");
|
|
onRequest();
|
|
lastRecv = Util::bootSecs();
|
|
}else{
|
|
if (!isBlocking && !parseData){
|
|
if (Util::bootSecs() - lastRecv > 300){
|
|
WARN_MSG("Disconnecting 5 minute idle connection");
|
|
onFail("Connection idle for 5 minutes");
|
|
}else{
|
|
Util::sleep(20);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Waits for the given amount of millis, increasing the realtime playback
|
|
/// related times as needed to keep smooth playback intact.
|
|
void Output::playbackSleep(uint64_t millis){
|
|
if (realTime && M.getLive() && buffer.getSyncMode()){
|
|
firstTime += millis;
|
|
}
|
|
Util::wait(millis);
|
|
}
|
|
|
|
/// Called right before sendNext(). Should return true if this is a stopping point.
|
|
bool Output::reachedPlannedStop(){
|
|
// If we're recording to file and reached the target position, stop
|
|
if (isRecordingToFile && targetParams.count("recstop") &&
|
|
atoll(targetParams["recstop"].c_str()) <= thisTime){
|
|
INFO_MSG("End of planned recording reached");
|
|
return true;
|
|
}
|
|
// Regardless of playback method, if we've reached the wanted stop point, stop
|
|
if (targetParams.count("stop") && atoll(targetParams["stop"].c_str()) <= thisTime){
|
|
INFO_MSG("End of planned playback reached");
|
|
return true;
|
|
}
|
|
// check if we need to split here
|
|
if (inlineRestartCapable() && targetParams.count("split")){
|
|
// Make sure that inlineRestartCapable outputs with splitting enabled only stop right before
|
|
// keyframes This works because this function is executed right BEFORE sendNext(), causing
|
|
// thisPacket to be the next packet in the newly splitted file.
|
|
if (thisIdx != getMainSelectedTrack() || (!thisPacket.getFlag("keyframe") && M.getType(thisIdx) == "video")){
|
|
return false;
|
|
}
|
|
// If splitting would result in a tiny segment at the end, do not split
|
|
if (!M.getLive() && (endTime() - lastPacketTime) < (atoll(targetParams["split"].c_str()) * 500)){
|
|
return false;
|
|
}
|
|
// is this a split point?
|
|
if (targetParams.count("nxt-split") && atoll(targetParams["nxt-split"].c_str()) <= thisTime){
|
|
INFO_MSG("Split point reached");
|
|
return true;
|
|
}
|
|
}
|
|
// Otherwise, we're not stopping
|
|
return false;
|
|
}
|
|
|
|
/// \triggers
|
|
/// The `"CONN_OPEN"` trigger is stream-specific, and is ran when a connection is made or passed to a new handler. Its payload is:
|
|
/// ~~~~~~~~~~~~~~~
|
|
/// streamname
|
|
/// connected client host
|
|
/// output handler name
|
|
/// request URL (if any)
|
|
/// ~~~~~~~~~~~~~~~
|
|
/// The `"CONN_CLOSE"` trigger is stream-specific, and is ran when a connection closes. Its payload is:
|
|
/// ~~~~~~~~~~~~~~~
|
|
/// streamname
|
|
/// connected client host
|
|
/// output handler name
|
|
/// request URL (if any)
|
|
/// ~~~~~~~~~~~~~~~
|
|
int Output::run(){
|
|
// Variables used for segmenting the output
|
|
uint64_t segmentCount = 0;
|
|
uint64_t segmentsRemoved = 0;
|
|
HTTP::URL playlistLocation;
|
|
std::string playlistLocationString;
|
|
std::string playlistBuffer;
|
|
std::string currentTarget;
|
|
uint64_t currentStartTime = 0;
|
|
uint64_t maxEntries = 0;
|
|
uint64_t targetAge = 0;
|
|
std::string targetDuration;
|
|
bool reInitPlaylist = false;
|
|
bool autoAdjustSplit = false;
|
|
Socket::Connection plsConn;
|
|
uint64_t systemBoot;
|
|
|
|
std::string origTarget;
|
|
const char* origTargetPtr = getenv("MST_ORIG_TARGET");
|
|
if (origTargetPtr){
|
|
origTarget = origTargetPtr;
|
|
if (origTarget.rfind('?') != std::string::npos){
|
|
std::map<std::string, std::string> tmpParams;
|
|
HTTP::parseVars(origTarget.substr(origTarget.rfind('?') + 1), tmpParams);
|
|
origTarget.erase(origTarget.rfind('?'));
|
|
if (tmpParams.count("m3u8")){
|
|
targetParams["m3u8"] = tmpParams["m3u8"];
|
|
}
|
|
if (tmpParams.count("segment")){
|
|
targetParams["segment"] = tmpParams["segment"];
|
|
}
|
|
}
|
|
}else if (config->hasOption("target")){
|
|
origTarget = config->getString("target");
|
|
}
|
|
// Check if the target segment contains any of the required variables
|
|
if (targetParams.count("m3u8")){
|
|
std::string tmpTarget;
|
|
if (targetParams.count("segment")){
|
|
tmpTarget = targetParams["segment"];
|
|
}else{
|
|
tmpTarget = origTarget;
|
|
}
|
|
if (tmpTarget.find("$currentMediaTime") == std::string::npos && tmpTarget.find("$segmentCounter") == std::string::npos){
|
|
FAIL_MSG("Target segmented output does not contain a currentMediaTime or segmentCounter: %s", tmpTarget.c_str());
|
|
Util::logExitReason("Target segmented output does not contain a currentMediaTime or segmentCounter: %s", tmpTarget.c_str());
|
|
return 1;
|
|
}
|
|
}
|
|
if (targetParams.count("maxEntries")){
|
|
maxEntries = atoll(targetParams["maxEntries"].c_str());
|
|
}
|
|
if (targetParams.count("targetAge")){
|
|
targetAge = atoll(targetParams["targetAge"].c_str());
|
|
}
|
|
if (targetParams.count("adjustSplit")){
|
|
autoAdjustSplit = true;
|
|
}
|
|
// When segmenting to a playlist, handle any existing files and init some data
|
|
if (targetParams.count("m3u8")){
|
|
// Load system boot time from the global config
|
|
systemBoot = Util::getGlobalConfig("systemBoot").asInt();
|
|
// fall back to local calculation if loading from global config fails
|
|
if (!systemBoot){systemBoot = (Util::unixMS() - Util::bootMS());}
|
|
// Create a new or connect to an existing playlist file
|
|
if (!plsConn){
|
|
std::string plsRel = targetParams["m3u8"];
|
|
Util::streamVariables(plsRel, streamName);
|
|
playlistLocation = HTTP::localURIResolver().link(config->getString("target")).link(plsRel);
|
|
if (playlistLocation.isLocalPath()){
|
|
playlistLocationString = playlistLocation.getFilePath();
|
|
INFO_MSG("Segmenting to local playlist '%s'", playlistLocationString.c_str());
|
|
// Check if we already have a playlist at the target location
|
|
std::ifstream inFile(playlistLocationString.c_str());
|
|
if (inFile.good()){
|
|
std::string line;
|
|
// If appending, remove endlist and count segments
|
|
if (targetParams.count("append")){
|
|
while (std::getline(inFile, line)) {
|
|
if (strncmp("#EXTINF", line.c_str(), 7) == 0){
|
|
segmentCount++;
|
|
}else if (strcmp("#EXT-X-ENDLIST", line.c_str()) == 0){
|
|
INFO_MSG("Stripping line `#EXT-X-ENDLIST`");
|
|
continue;
|
|
}
|
|
playlistBuffer += line + '\n';
|
|
}
|
|
playlistBuffer += "#EXT-X-DISCONTINUITY\n";
|
|
INFO_MSG("Appending to existing local playlist file '%s'", playlistLocationString.c_str());
|
|
INFO_MSG("Found %" PRIu64 " prior segments", segmentCount);
|
|
}else{
|
|
// Remove all segments referenced in the playlist
|
|
while (std::getline(inFile, line)) {
|
|
if (line[0] == '#'){
|
|
continue;
|
|
}else{
|
|
std::string segPath = playlistLocation.link(line).getFilePath();
|
|
if(unlink(segPath.c_str())){
|
|
FAIL_MSG("Failed to remove segment at '%s'. Error: '%s'", segPath.c_str(), strerror(errno));
|
|
}else{
|
|
INFO_MSG("Removed segment at '%s'", segPath.c_str());
|
|
}
|
|
}
|
|
}
|
|
INFO_MSG("Overwriting existing local playlist file '%s'", playlistLocationString.c_str());
|
|
reInitPlaylist = true;
|
|
}
|
|
}else{
|
|
INFO_MSG("Creating new local playlist file '%s'", playlistLocationString.c_str());
|
|
reInitPlaylist = true;
|
|
}
|
|
config->getOption("target", true).append(playlistLocationString);
|
|
}else{
|
|
playlistLocationString = playlistLocation.getUrl();
|
|
// Disable sliding window playlists, as the current external writer
|
|
// implementation requires us to keep a single connection to the playlist open
|
|
maxEntries = 0;
|
|
targetAge = 0;
|
|
// Check if there is an existing playlist at the target location
|
|
HTTP::URIReader outFile(playlistLocationString);
|
|
if (outFile){
|
|
// If so, init the buffer with remote data
|
|
if (targetParams.count("append")){
|
|
char *dataPtr;
|
|
size_t dataLen;
|
|
outFile.readAll(dataPtr, dataLen);
|
|
std::string existingBuffer(dataPtr, dataLen);
|
|
std::istringstream inFile(existingBuffer);
|
|
std::string line;
|
|
while (std::getline(inFile, line)) {
|
|
if (strncmp("#EXTINF", line.c_str(), 7) == 0){
|
|
segmentCount++;
|
|
}else if (strcmp("#EXT-X-ENDLIST", line.c_str()) == 0){
|
|
INFO_MSG("Stripping line `#EXT-X-ENDLIST`");
|
|
continue;
|
|
}
|
|
playlistBuffer += line + '\n';
|
|
}
|
|
playlistBuffer += "#EXT-X-DISCONTINUITY\n";
|
|
INFO_MSG("Found %" PRIu64 " prior segments", segmentCount);
|
|
INFO_MSG("Appending to existing remote playlist file '%s'", playlistLocationString.c_str());
|
|
}else{
|
|
WARN_MSG("Overwriting existing remote playlist file '%s'", playlistLocationString.c_str());
|
|
reInitPlaylist = true;
|
|
}
|
|
}else{
|
|
INFO_MSG("Creating new remote playlist file '%s'", playlistLocationString.c_str());
|
|
reInitPlaylist = true;
|
|
}
|
|
}
|
|
}
|
|
// By default split into a new segment after 60 seconds
|
|
if (!targetParams.count("split")){
|
|
targetParams["split"] = "60";
|
|
}
|
|
targetDuration = targetParams["split"];
|
|
}
|
|
Comms::sessionConfigCache();
|
|
/*LTS-START*/
|
|
// Connect to file target, if needed
|
|
if (isFileTarget()){
|
|
isRecordingToFile = true;
|
|
if (!streamName.size()){
|
|
WARN_MSG("Recording unconnected %s output to file! Cancelled.", capa["name"].asString().c_str());
|
|
onFail("Unconnected recording output", true);
|
|
recEndTrigger();
|
|
return 2;
|
|
}
|
|
initialize();
|
|
if (!M.getValidTracks().size() || !userSelect.size() || !keepGoing()){
|
|
INFO_MSG("Stream not available - aborting");
|
|
onFail("Stream not available for recording", true);
|
|
recEndTrigger();
|
|
return 3;
|
|
}
|
|
initialSeek();
|
|
// Initialises the playlist if we are segmenting the output with a playlist
|
|
if (targetParams.count("m3u8")){
|
|
if (reInitPlaylist){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
reinitPlaylist(playlistBuffer, targetAge, maxEntries, segmentCount, segmentsRemoved, unixMs, targetDuration, playlistLocation);
|
|
}
|
|
// Do not open the playlist just yet if this is a non-live source
|
|
if (M.getLive()){
|
|
connectToFile(playlistLocationString, false, &plsConn);
|
|
// Write initial contents to the playlist file
|
|
if (!plsConn){
|
|
FAIL_MSG("Failed to open a connection to playlist file `%s` for segmenting", playlistLocationString.c_str());
|
|
Util::logExitReason("Failed to open a connection to playlist file `%s` for segmenting", playlistLocationString.c_str());
|
|
return 1;
|
|
}else if (playlistBuffer.size()){
|
|
// Do not write to the playlist intermediately if we are outputting a VOD playlist
|
|
plsConn.SendNow(playlistBuffer);
|
|
// Clear the buffer if we will only be appending lines instead of overwriting the entire playlist file
|
|
if (!maxEntries && !targetAge) {playlistBuffer = "";}
|
|
}
|
|
}
|
|
}
|
|
currentStartTime = currentTime();
|
|
std::string newTarget = origTarget;
|
|
Util::replace(newTarget, "$currentMediaTime", JSON::Value(currentStartTime).asString());
|
|
Util::replace(newTarget, "$segmentCounter", JSON::Value(segmentCount).asString());
|
|
Util::streamVariables(newTarget, streamName);
|
|
currentTarget = newTarget;
|
|
if (newTarget == "-"){
|
|
INFO_MSG("Outputting %s to stdout with %s format", streamName.c_str(),
|
|
capa["name"].asString().c_str());
|
|
}else{
|
|
if (!connectToFile(newTarget, targetParams.count("append"))){
|
|
onFail("Could not connect to the target for recording", true);
|
|
recEndTrigger();
|
|
return 3;
|
|
}
|
|
INFO_MSG("Recording %s to %s with %s format", streamName.c_str(),
|
|
newTarget.c_str(), capa["name"].asString().c_str());
|
|
}
|
|
parseData = true;
|
|
wantRequest = false;
|
|
if (!targetParams.count("realtime")){
|
|
realTime = 0;
|
|
}
|
|
}
|
|
// Handle CONN_OPEN trigger, if needed
|
|
if (Triggers::shouldTrigger("CONN_OPEN", streamName)){
|
|
std::string payload =
|
|
streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n" + reqUrl;
|
|
if (!Triggers::doTrigger("CONN_OPEN", payload, streamName)){return 1;}
|
|
}
|
|
/*LTS-END*/
|
|
DONTEVEN_MSG("MistOut client handler started");
|
|
while (keepGoing() && (wantRequest || parseData)){
|
|
Comms::sessionConfigCache();
|
|
if (wantRequest){requestHandler();}
|
|
if (parseData){
|
|
if (!isInitialized){
|
|
initialize();
|
|
if (!isInitialized){
|
|
onFail("Stream initialization failed");
|
|
break;
|
|
}
|
|
}
|
|
if (!sought){initialSeek();}
|
|
if (!sentHeader && keepGoing()){
|
|
DONTEVEN_MSG("sendHeader");
|
|
sendHeader();
|
|
}
|
|
if (prepareNext()){
|
|
if (thisPacket){
|
|
lastPacketTime = thisTime;
|
|
if (firstPacketTime == 0xFFFFFFFFFFFFFFFFull){
|
|
firstPacketTime = lastPacketTime;
|
|
}
|
|
|
|
// slow down processing, if real time speed is wanted
|
|
if (realTime && buffer.getSyncMode()){
|
|
uint64_t amount = thisTime - targetTime();
|
|
size_t i = (amount / 1000) + 6;
|
|
while (--i && thisTime > targetTime() &&
|
|
keepGoing()){
|
|
amount = thisTime - targetTime();
|
|
if (amount > 1000){amount = 1000;}
|
|
idleTime(amount);
|
|
//Make sure we stay responsive to requests and stats while waiting
|
|
if (wantRequest){
|
|
requestHandler();
|
|
if (!realTime){break;}
|
|
}
|
|
stats();
|
|
}
|
|
if (!thisPacket){continue;}
|
|
}
|
|
|
|
// delay the stream until metadata has caught up, if needed
|
|
if (needsLookAhead && M.getLive()){
|
|
// we sleep in 20ms increments, or less if the lookahead time itself is less
|
|
uint32_t sleepTime = std::min((uint64_t)20, needsLookAhead);
|
|
// wait at most double the look ahead time, plus ten seconds
|
|
uint64_t timeoutTries = (needsLookAhead / sleepTime) * 2 + (10000 / sleepTime);
|
|
uint64_t needsTime = thisTime + needsLookAhead;
|
|
bool firstLookahead = true;
|
|
while (--timeoutTries && keepGoing()){
|
|
bool lookReady = true;
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin();
|
|
it != userSelect.end(); it++){
|
|
if (meta.getNowms(it->first) <= needsTime){
|
|
if (timeoutTries == 1){
|
|
WARN_MSG("Track %zu: %" PRIu64 " <= %" PRIu64, it->first,
|
|
meta.getNowms(it->first), needsTime);
|
|
}
|
|
lookReady = false;
|
|
break;
|
|
}
|
|
}
|
|
if (lookReady){break;}
|
|
if (firstLookahead){
|
|
firstLookahead = false;
|
|
}else{
|
|
playbackSleep(sleepTime);
|
|
}
|
|
//Make sure we stay responsive to requests and stats while waiting
|
|
if (wantRequest){requestHandler();}
|
|
stats();
|
|
meta.reloadReplacedPagesIfNeeded();
|
|
}
|
|
if (!timeoutTries){
|
|
WARN_MSG("Waiting for lookahead (%" PRIu64 "ms in %zu tracks) timed out - resetting lookahead!", needsLookAhead, userSelect.size());
|
|
needsLookAhead = 0;
|
|
}
|
|
}
|
|
|
|
if (reachedPlannedStop()){
|
|
targetParams.erase("nxt-split");
|
|
if (inlineRestartCapable() && !reachedPlannedStop()){
|
|
// Write the segment to the playlist if applicable
|
|
if (targetParams.count("m3u8")){
|
|
// We require an active connection to the playlist
|
|
// except for VOD, where we connect and write at the end of segmenting
|
|
if (!plsConn && M.getLive()){
|
|
FAIL_MSG("Lost connection to playlist file `%s` during segmenting", playlistLocationString.c_str());
|
|
Util::logExitReason("Lost connection to playlist file `%s` during segmenting", playlistLocationString.c_str());
|
|
break;
|
|
}
|
|
std::string segment = HTTP::localURIResolver().link(currentTarget).getLinkFrom(playlistLocation);
|
|
if (M.getLive()){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
playlistBuffer += "#EXT-X-PROGRAM-DATE-TIME:" + Util::getUTCStringMillis(unixMs) + "\n";
|
|
}
|
|
INFO_MSG("Adding new segment `%s` of %" PRIu64 "ms to playlist '%s'", segment.c_str(), lastPacketTime - currentStartTime, playlistLocationString.c_str());
|
|
// Append duration & TS filename to playlist file
|
|
std::stringstream tmp;
|
|
double segmentDuration = (lastPacketTime - currentStartTime) / 1000.0;
|
|
tmp << "#EXTINF:" << std::fixed << std::setprecision(3) << segmentDuration << ",\n"+ segment + "\n";
|
|
playlistBuffer += tmp.str();
|
|
// Adjust split time up to half a segment duration
|
|
if (autoAdjustSplit && (segmentDuration / 2) > atoll(targetParams["split"].c_str())){
|
|
targetParams["split"] = JSON::Value(segmentDuration / 2).asString();
|
|
}
|
|
// Always adjust the targetDuration in the playlist upwards
|
|
if (segmentDuration > JSON::Value(targetDuration).asDouble()){
|
|
// Set the new targetDuration to the ceil of the segment duration
|
|
WARN_MSG("Segment #%" PRIu64 " is longer than the target duration. Adjusting the targetDuration from %s to %f s", segmentCount, targetDuration.c_str(), segmentDuration);
|
|
targetDuration = JSON::Value((uint64_t)segmentDuration + 1).asString();
|
|
// Modify the buffer to contain the new targetDuration
|
|
if (!M.getLive()){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
reinitPlaylist(playlistBuffer, targetAge, maxEntries, segmentCount, segmentsRemoved, unixMs, targetDuration, playlistLocation);
|
|
}else if (!maxEntries && !targetAge && playlistLocation.isLocalPath()){
|
|
// If we are appending to an existing playlist, we need to recover the playlistBuffer and reopen the playlist
|
|
HTTP::URIReader inFile(playlistLocationString);
|
|
char *newBuffer;
|
|
size_t bytesRead;
|
|
inFile.readAll(newBuffer, bytesRead);
|
|
playlistBuffer = std::string(newBuffer, bytesRead) + playlistBuffer;
|
|
// Reinit the playlist with the new targetDuration
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
reinitPlaylist(playlistBuffer, targetAge, maxEntries, segmentCount, segmentsRemoved, unixMs, targetDuration, playlistLocation);
|
|
connectToFile(playlistLocationString, false, &plsConn);
|
|
}
|
|
// Else we are in a sliding window playlist, so it will automatically get overwritten
|
|
}
|
|
// Remove older entries in the playlist
|
|
if (maxEntries || targetAge){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
reinitPlaylist(playlistBuffer, targetAge, maxEntries, segmentCount, segmentsRemoved, unixMs, targetDuration, playlistLocation);
|
|
}
|
|
// Do not write to the playlist intermediately if we are outputting a VOD playlist
|
|
if (M.getLive()){
|
|
// Clear the buffer if we will only be appending lines instead of overwriting the entire playlist file
|
|
if (!maxEntries && !targetAge) {
|
|
plsConn.SendNow(playlistBuffer);
|
|
playlistBuffer = "";
|
|
// Else re-open the file to force an overwrite
|
|
}else if(connectToFile(playlistLocationString, false, &plsConn)){
|
|
plsConn.SendNow(playlistBuffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep track of filenames written, so that they can be added to the playlist file
|
|
std::string newTarget;
|
|
if (targetParams.count("segment")){
|
|
HTTP::URL targetUrl = HTTP::URL(config->getString("target")).link(targetParams["segment"]);
|
|
if (targetUrl.isLocalPath()){
|
|
newTarget = targetUrl.getFilePath();
|
|
}else{
|
|
newTarget = targetUrl.getUrl();
|
|
}
|
|
}else{
|
|
newTarget = origTarget;
|
|
}
|
|
currentStartTime = lastPacketTime;
|
|
segmentCount++;
|
|
Util::replace(newTarget, "$currentMediaTime", JSON::Value(currentStartTime).asString());
|
|
Util::replace(newTarget, "$segmentCounter", JSON::Value(segmentCount).asString());
|
|
Util::streamVariables(newTarget, streamName);
|
|
if (newTarget.rfind('?') != std::string::npos){
|
|
newTarget.erase(newTarget.rfind('?'));
|
|
}
|
|
currentTarget = newTarget;
|
|
INFO_MSG("Switching to next push target filename: %s", newTarget.c_str());
|
|
if (!connectToFile(newTarget)){
|
|
FAIL_MSG("Failed to open file, aborting: %s", newTarget.c_str());
|
|
Util::logExitReason(ER_WRITE_FAILURE, "failed to open file, aborting: %s", newTarget.c_str());
|
|
onFinish();
|
|
break;
|
|
}
|
|
uint64_t endRec = thisTime + atoll(targetParams["split"].c_str()) * 1000;
|
|
targetParams["nxt-split"] = JSON::Value(endRec).asString();
|
|
sentHeader = false;
|
|
sendHeader();
|
|
}else{
|
|
if (!onFinish()){
|
|
INFO_MSG("Shutting down because planned stopping point reached");
|
|
Util::logExitReason(ER_CLEAN_INTENDED_STOP, "planned stopping point reached");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
sendNext();
|
|
}else{
|
|
parseData = false;
|
|
/*LTS-START*/
|
|
if (Triggers::shouldTrigger("CONN_STOP", streamName)){
|
|
std::string payload =
|
|
streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n";
|
|
Triggers::doTrigger("CONN_STOP", payload, streamName);
|
|
}
|
|
/*LTS-END*/
|
|
if (!onFinish()){
|
|
Util::logExitReason(ER_CLEAN_EOF, "end of stream");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!meta){
|
|
Util::logExitReason(ER_SHM_LOST, "lost internal connection to stream data");
|
|
break;
|
|
}
|
|
}
|
|
stats();
|
|
}
|
|
if (!config->is_active){Util::logExitReason(ER_UNKNOWN, "set inactive");}
|
|
if (!myConn){Util::logExitReason(ER_CLEAN_REMOTE_CLOSE, "connection closed");}
|
|
if (strncmp(Util::exitReason, "connection closed", 17) == 0){
|
|
MEDIUM_MSG("Client handler shutting down, exit reason: %s", Util::exitReason);
|
|
}else{
|
|
INFO_MSG("Client handler shutting down, exit reason: %s", Util::exitReason);
|
|
}
|
|
onFinish();
|
|
// Write last segment
|
|
if (targetParams.count("m3u8") && (firstPacketTime != 0xFFFFFFFFFFFFFFFFull) && (lastPacketTime - firstPacketTime > 0)){
|
|
// If this is a non-live source, we can finally open up the connection to the playlist file
|
|
if (!M.getLive()){connectToFile(playlistLocationString, false, &plsConn);}
|
|
if (plsConn){
|
|
|
|
if (lastPacketTime - currentStartTime > 0){
|
|
std::string segment = HTTP::localURIResolver().link(currentTarget).getLinkFrom(playlistLocation);
|
|
INFO_MSG("Adding final segment `%s` of %" PRIu64 "ms to playlist '%s'", segment.c_str(), lastPacketTime - currentStartTime, playlistLocationString.c_str());
|
|
if (M.getLive()){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
playlistBuffer += "#EXT-X-PROGRAM-DATE-TIME:" + Util::getUTCStringMillis(unixMs) + "\n";
|
|
}
|
|
// Append duration & TS filename to playlist file
|
|
std::stringstream tmp;
|
|
tmp << "#EXTINF:" << std::fixed << std::setprecision(3) << (lastPacketTime - currentStartTime) / 1000.0 << ",\n"+ segment + "\n";
|
|
playlistBuffer += tmp.str();
|
|
}
|
|
|
|
if (!M.getLive() || (!maxEntries && !targetAge)){playlistBuffer += "#EXT-X-ENDLIST\n";}
|
|
// Remove older entries in the playlist
|
|
if (maxEntries || targetAge){
|
|
uint64_t unixMs = M.getBootMsOffset() + systemBoot + currentStartTime;
|
|
reinitPlaylist(playlistBuffer, targetAge, maxEntries, segmentCount, segmentsRemoved, unixMs, targetDuration, playlistLocation);
|
|
}
|
|
// Append the final contents to the playlist
|
|
if (!maxEntries && !targetAge) {
|
|
plsConn.SendNow(playlistBuffer);
|
|
// Else re-open the file to force an overwrite
|
|
}else if(connectToFile(playlistLocationString, false, &plsConn)){
|
|
plsConn.SendNow(playlistBuffer);
|
|
}
|
|
playlistBuffer.clear();
|
|
}else{
|
|
FAIL_MSG("Lost connection to the playlist file `%s` during segmenting", playlistLocationString.c_str());
|
|
Util::logExitReason("Lost connection to the playlist file `%s` during segmenting", playlistLocationString.c_str());
|
|
}
|
|
}
|
|
|
|
/*LTS-START*/
|
|
if (Triggers::shouldTrigger("CONN_CLOSE", streamName)){
|
|
std::string payload =
|
|
streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n" + reqUrl;
|
|
Triggers::doTrigger("CONN_CLOSE", payload, streamName);
|
|
}
|
|
if (isRecordingToFile){
|
|
recEndTrigger();
|
|
}
|
|
outputEndTrigger();
|
|
/*LTS-END*/
|
|
|
|
disconnect();
|
|
stats(true);
|
|
userSelect.clear();
|
|
myConn.close();
|
|
return 0;
|
|
}
|
|
|
|
void Output::dropTrack(size_t trackId, const std::string &reason, bool probablyBad){
|
|
//We can drop from the buffer without any checks, it's a no-op if no entry exists
|
|
buffer.dropTrack(trackId);
|
|
// depending on whether this is probably bad and the current debug level, print a message
|
|
size_t printLevel = (probablyBad ? DLVL_WARN : DLVL_INFO);
|
|
//The rest of the operations depends on userSelect, so we ignore it if it doesn't exist.
|
|
if (!userSelect.count(trackId)){
|
|
DEBUG_MSG(printLevel, "Dropping %s track %zu (lastP=%" PRIu64 "): %s",
|
|
meta.getCodec(trackId).c_str(), trackId, pageNumMax(trackId), reason.c_str());
|
|
return;
|
|
}
|
|
const Comms::Users &usr = userSelect.at(trackId);
|
|
if (!usr){
|
|
DEBUG_MSG(printLevel, "Dropping %s track %zu (lastP=%" PRIu64 "): %s",
|
|
meta.getCodec(trackId).c_str(), trackId, pageNumMax(trackId), reason.c_str());
|
|
}else{
|
|
DEBUG_MSG(printLevel, "Dropping %s track %zu@k%zu (nextP=%" PRIu64 ", lastP=%" PRIu64 "): %s",
|
|
meta.getCodec(trackId).c_str(), trackId, usr.getKeyNum() + 1,
|
|
pageNumForKey(trackId, usr.getKeyNum() + 1), pageNumMax(trackId), reason.c_str());
|
|
}
|
|
userSelect.erase(trackId);
|
|
}
|
|
|
|
/// Assumes at least one video track is selected.
|
|
/// Seeks back in the buffer to the newest keyframe with a timestamp less than the current
|
|
/// timestamp. Sets thisPacket to that frame, and then undoes the seek. The next call to
|
|
/// prepareNext continues as if this function was never called.
|
|
bool Output::getKeyFrame(){
|
|
// store copy of current state
|
|
Util::packetSorter tmp_buffer = buffer;
|
|
std::map<size_t, Comms::Users> tmp_userSelect = userSelect;
|
|
std::map<size_t, uint32_t> tmp_currentPage = currentPage;
|
|
|
|
// reset the current packet to null, assuming failure
|
|
thisPacket.null();
|
|
|
|
// find the main track, check if it is video. Abort if not.
|
|
size_t mainTrack = getMainSelectedTrack();
|
|
if (M.getType(mainTrack) != "video"){return false;}
|
|
|
|
// we now know that mainTrack is a video track - let's do some work!
|
|
// first, we remove all selected tracks and the buffer. Then we select only the main track.
|
|
uint64_t currTime = currentTime();
|
|
buffer.clear();
|
|
userSelect.clear();
|
|
userSelect[mainTrack].reload(streamName, mainTrack);
|
|
// now, seek to the exact timestamp of the keyframe
|
|
DTSC::Keys keys(M.keys(mainTrack));
|
|
uint32_t targetKey = M.getKeyNumForTime(mainTrack, currTime);
|
|
bool ret = false;
|
|
if (targetKey == INVALID_KEY_NUM){
|
|
FAIL_MSG("No keyframes available on track %zu", mainTrack);
|
|
}else{
|
|
seek(keys.getTime(targetKey));
|
|
// attempt to load the key into thisPacket
|
|
ret = prepareNext();
|
|
if (!ret){
|
|
WARN_MSG("Failed to load keyframe for %" PRIu64 "ms - continuing without it", currTime);
|
|
}
|
|
}
|
|
|
|
// restore state to before the seek/load
|
|
// most of these can simply be copied back...
|
|
buffer = tmp_buffer;
|
|
userSelect = tmp_userSelect;
|
|
// but the currentPage map must also load keys as needed
|
|
for (std::map<size_t, uint32_t>::iterator it = tmp_currentPage.begin(); it != tmp_currentPage.end(); ++it){
|
|
loadPageForKey(it->first, it->second);
|
|
}
|
|
// now we are back to normal and can return safely
|
|
return ret;
|
|
}
|
|
|
|
/// Attempts to prepare a new packet for output.
|
|
/// If it returns true and thisPacket evaluates to false, playback has completed.
|
|
/// Could be called repeatedly in a loop if you really really want a new packet.
|
|
/// \returns true if thisPacket was filled with the next packet.
|
|
/// \returns false if we could not reliably determine the next packet yet.
|
|
bool Output::prepareNext(){
|
|
if (!buffer.size()){
|
|
thisPacket.null();
|
|
INFO_MSG("Buffer completely played out");
|
|
return true;
|
|
}
|
|
// check if we have a next seek point for every track that is selected
|
|
if (buffer.size() != userSelect.size()){
|
|
INFO_MSG("Buffer/select mismatch: %zu/%zu - correcting", buffer.size(), userSelect.size());
|
|
std::set<size_t> dropTracks;
|
|
if (buffer.size() < userSelect.size()){
|
|
// prepare to drop any selectedTrack without buffer entry
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); ++it){
|
|
if (!buffer.hasEntry(it->first)){dropTracks.insert(it->first);}
|
|
}
|
|
}else{
|
|
// prepare to drop any buffer entry without selectedTrack
|
|
buffer.getTrackList(dropTracks);
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); ++it){
|
|
dropTracks.erase(it->first);
|
|
}
|
|
}
|
|
if (!dropTracks.size()){
|
|
FAIL_MSG("Could not equalize tracks! This is very very very bad and I am now going to shut down to prevent worse.");
|
|
Util::logExitReason(ER_INTERNAL_ERROR, "Could not equalize tracks");
|
|
parseData = false;
|
|
config->is_active = false;
|
|
return false;
|
|
}
|
|
// actually drop what we found.
|
|
// if both of the above cases occur, the next prepareNext iteration will take care of that
|
|
for (std::set<size_t>::iterator it = dropTracks.begin(); it != dropTracks.end(); ++it){
|
|
dropTrack(*it, "seek/select mismatch");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Util::sortedPageInfo nxt;
|
|
|
|
uint64_t nextTime;
|
|
size_t trackTries = 0;
|
|
//In case we're not in sync mode, we might have to retry a few times
|
|
for (; trackTries < buffer.size(); ++trackTries){
|
|
|
|
nxt = *(buffer.begin());
|
|
|
|
if (meta.reloadReplacedPagesIfNeeded()){return false;}
|
|
if (!M.getValidTracks().count(nxt.tid)){
|
|
dropTrack(nxt.tid, "disappeared from metadata");
|
|
return false;
|
|
}
|
|
|
|
// if we're going to read past the end of the data page...
|
|
if (nxt.offset >= curPage[nxt.tid].len ||
|
|
(!memcmp(curPage[nxt.tid].mapped + nxt.offset, "\000\000\000\000", 4))){
|
|
// For non-live, we may have just reached the end of the track. That's normal and fine, drop it.
|
|
if (!M.getLive() && nxt.time >= M.getLastms(nxt.tid)){
|
|
dropTrack(nxt.tid, "end of VoD track reached", false);
|
|
return false;
|
|
}
|
|
// Check if there is a next page for the timestamp we're looking for.
|
|
if (M.getPageNumberForTime(nxt.tid, nxt.time) != currentPage[nxt.tid]){
|
|
loadPageForKey(nxt.tid, M.getPageNumberForTime(nxt.tid, nxt.time));
|
|
nxt.offset = 0;
|
|
//Only read the next time if the page load succeeded and there is a packet to read from
|
|
if (curPage[nxt.tid].mapped && curPage[nxt.tid].mapped[0] == 'D'){
|
|
nxt.time = getDTSCTime(curPage[nxt.tid].mapped, 0);
|
|
nxt.ghostPacket = false;
|
|
}else{
|
|
nxt.ghostPacket = true;
|
|
}
|
|
buffer.replaceFirst(nxt);
|
|
return false;
|
|
}
|
|
// We're still on the same page; ghost packets should update their time and retry later
|
|
if (nxt.ghostPacket){
|
|
nxt.time = M.getNowms(nxt.tid);
|
|
buffer.replaceFirst(nxt);
|
|
return false;
|
|
}
|
|
if (nxt.offset >= curPage[nxt.tid].len){
|
|
INFO_MSG("Reading past end of page %s: %" PRIu64 " > %" PRIu64 " for time %" PRIu64 " on track %zu", curPage[nxt.tid].name.c_str(), nxt.offset, curPage[nxt.tid].len, nxt.time, nxt.tid);
|
|
dropTrack(nxt.tid, "reading past end of page");
|
|
}else{
|
|
INFO_MSG("Invalid packet: no data @%" PRIu64 " in %s for time %" PRIu64 " on track %zu", nxt.offset, curPage[nxt.tid].name.c_str(), nxt.time, nxt.tid);
|
|
dropTrack(nxt.tid, "zero packet");
|
|
}
|
|
return false;
|
|
}
|
|
// We know this packet will be valid, pre-load it so we know its length
|
|
DTSC::Packet preLoad(curPage[nxt.tid].mapped + nxt.offset, 0, true);
|
|
nxt.time = preLoad.getTime();
|
|
|
|
nextTime = 0;
|
|
|
|
// Check if we have a next valid packet
|
|
if (curPage[nxt.tid].len > nxt.offset+preLoad.getDataLen()+20 && memcmp(curPage[nxt.tid].mapped + nxt.offset + preLoad.getDataLen(), "\000\000\000\000", 4)){
|
|
nextTime = getDTSCTime(curPage[nxt.tid].mapped, nxt.offset + preLoad.getDataLen());
|
|
if (!nextTime){
|
|
WARN_MSG("Next packet is available (offset %" PRIu64 " / %" PRIu64 " on %s), but has no time. Please warn the developers if you see this message!", nxt.offset, curPage[nxt.tid].len, curPage[nxt.tid].name.c_str());
|
|
dropTrack(nxt.tid, "EOP: invalid next packet");
|
|
return false;
|
|
}
|
|
if (nextTime < nxt.time){
|
|
std::stringstream errMsg;
|
|
errMsg << "next packet has timestamp " << nextTime << " but current timestamp is " << nxt.time;
|
|
dropTrack(nxt.tid, errMsg.str().c_str());
|
|
return false;
|
|
}
|
|
break;//Packet valid!
|
|
}
|
|
|
|
//no next packet on the current page
|
|
|
|
//Check if this is the last packet of a VoD stream. Return success and drop the track.
|
|
if (!M.getLive() && nxt.time >= M.getLastms(nxt.tid)){
|
|
thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true);
|
|
thisIdx = nxt.tid;
|
|
thisTime = nxt.time;
|
|
dropTrack(nxt.tid, "end of non-live track reached", false);
|
|
return true;
|
|
}
|
|
|
|
//Check if there exists a different page for the next key
|
|
uint32_t thisKey = M.getKeyNumForTime(nxt.tid, nxt.time);
|
|
uint32_t nextKeyPage = INVALID_KEY_NUM;
|
|
//Make sure we only try to read the page for the next key if it actually should be available
|
|
DTSC::Keys keys(M.keys(nxt.tid));
|
|
if (keys.getEndValid() >= thisKey+1){nextKeyPage = M.getPageNumberForKey(nxt.tid, thisKey + 1);}
|
|
if (nextKeyPage != INVALID_KEY_NUM && nextKeyPage != currentPage[nxt.tid]){
|
|
// If so, the next key is our next packet
|
|
nextTime = keys.getTime(thisKey + 1);
|
|
userSelect[nxt.tid].setKeyNum(thisKey + 1);
|
|
|
|
//If the next packet should've been before the current packet, something is wrong. Abort, abort!
|
|
if (nextTime < nxt.time){
|
|
std::stringstream errMsg;
|
|
errMsg << "next key (" << (thisKey+1) << ") time " << nextTime << " but current time " << nxt.time;
|
|
errMsg << "; currPage=" << currentPage[nxt.tid] << ", nxtPage=" << nextKeyPage;
|
|
errMsg << ", firstKey=" << keys.getFirstValid() << ", endKey=" << keys.getEndValid();
|
|
dropTrack(nxt.tid, errMsg.str().c_str());
|
|
return false;
|
|
}
|
|
break;//Valid packet!
|
|
}
|
|
|
|
// Force valid packet if nowMs is higher than current packet time
|
|
if (M.getNowms(nxt.tid) > nxt.time){break;}
|
|
|
|
//Okay, there's no next page yet, and no next packet on this page either.
|
|
//That means we're waiting for data to show up, somewhere.
|
|
|
|
//In non-sync mode, shuffle the just-tried packet to the end of queue and retry
|
|
if (!buffer.getSyncMode()){
|
|
buffer.moveFirstToEnd();
|
|
continue;
|
|
}
|
|
|
|
// in sync mode, after ~25 seconds, give up and drop the track.
|
|
if (++emptyCount >= dataWaitTimeout){
|
|
//curPage[nxt.tid].mapped + nxt.offset + preLoad.getDataLen()
|
|
WARN_MSG("Waiting at %s byte %zu", curPage[nxt.tid].name.c_str(), nxt.offset + preLoad.getDataLen());
|
|
dropTrack(nxt.tid, "EOP: data wait timeout");
|
|
return false;
|
|
}
|
|
//every ~1 second, check if the stream is not offline
|
|
if (emptyCount % 100 == 0 && Util::getStreamStatus(streamName) == STRMSTAT_OFF){
|
|
if (M.getLive()){
|
|
Util::logExitReason(ER_CLEAN_EOF, "Live stream source shut down");
|
|
thisPacket.null();
|
|
return true;
|
|
}else if (!Util::startInput(streamName)){
|
|
Util::logExitReason(ER_UNKNOWN, "VoD stream source shut down and could not be restarted");
|
|
thisPacket.null();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//Fine! We didn't want a packet, anyway. Let's try again later.
|
|
playbackSleep(10);
|
|
return false;
|
|
}
|
|
|
|
if (trackTries == buffer.size()){
|
|
//Fine! We didn't want a packet, anyway. Let's try again later.
|
|
playbackSleep(10);
|
|
return false;
|
|
}
|
|
|
|
// we've handled all special cases - at this point the packet should exist
|
|
// let's load it
|
|
thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true);
|
|
// if it failed, drop the track and continue
|
|
if (!thisPacket){
|
|
dropTrack(nxt.tid, "packet load failure");
|
|
return false;
|
|
}
|
|
emptyCount = 0; // valid packet - reset empty counter
|
|
thisIdx = nxt.tid;
|
|
thisTime = nxt.time;
|
|
|
|
if (!userSelect[nxt.tid]){
|
|
dropTrack(nxt.tid, "track is not alive!");
|
|
return false;
|
|
}
|
|
|
|
//Update keynum only when the second flips over in the timestamp
|
|
//We do this because DTSC::Keys is pretty CPU-heavy
|
|
if (nxt.time / 5000 < nextTime/5000){
|
|
uint32_t thisKey = M.getKeyNumForTime(nxt.tid, nxt.time);
|
|
userSelect[nxt.tid].setKeyNum(thisKey);
|
|
}
|
|
|
|
// we assume the next packet is the next on this same page
|
|
nxt.offset += thisPacket.getDataLen();
|
|
if (!nextTime){
|
|
// If time is not known yet, insert a ghostPacket with a known safe time
|
|
nxt.time = M.getNowms(nxt.tid);
|
|
nxt.ghostPacket = true;
|
|
}else{
|
|
nxt.time = nextTime;
|
|
nxt.ghostPacket = false;
|
|
}
|
|
++nxt.partIndex;
|
|
|
|
// exchange the current packet in the buffer for the next one
|
|
buffer.replaceFirst(nxt);
|
|
return true;
|
|
}
|
|
|
|
/// Returns the name as it should be used in statistics.
|
|
/// Outputs used as an input should return INPUT, outputs used for automation should return OUTPUT, others should return their proper name.
|
|
/// The default implementation is usually good enough for all the non-INPUT types.
|
|
std::string Output::getStatsName(){
|
|
if (isPushing()){return "INPUT:" + capa["name"].asStringRef();}
|
|
if (config->hasOption("target") && config->getString("target").size()){
|
|
return "OUTPUT:" + capa["name"].asStringRef();
|
|
}
|
|
return capa["name"].asStringRef();
|
|
}
|
|
|
|
void Output::stats(bool force){
|
|
// cancel stats update if not initialized
|
|
if (!isInitialized){return;}
|
|
// also cancel if it has been less than a second since the last update
|
|
// unless force is set to true
|
|
uint64_t now = Util::bootSecs();
|
|
if (now <= lastStats && !force){return;}
|
|
|
|
if (isRecording()){
|
|
if(lastPushUpdate == 0){
|
|
lastPushUpdate = now;
|
|
}
|
|
|
|
if (lastPushUpdate + 5 <= now){
|
|
JSON::Value pStat;
|
|
pStat["push_status_update"]["id"] = getpid();
|
|
JSON::Value & pData = pStat["push_status_update"]["status"];
|
|
pData["mediatime"] = currentTime();
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
pData["tracks"].append((uint64_t)it->first);
|
|
}
|
|
pData["bytes"] = statComm.getUp();
|
|
uint64_t pktCntNow = statComm.getPacketCount();
|
|
if (pktCntNow){
|
|
uint64_t pktLosNow = statComm.getPacketLostCount();
|
|
static uint64_t prevPktCount = pktCntNow;
|
|
static uint64_t prevLosCount = pktLosNow;
|
|
uint64_t pktCntDiff = pktCntNow-prevPktCount;
|
|
uint64_t pktLosDiff = pktLosNow-prevLosCount;
|
|
if (pktCntDiff){
|
|
pData["pkt_loss_perc"] = (pktLosDiff*100) / pktCntDiff;
|
|
}
|
|
pData["pkt_loss_count"] = pktLosNow;
|
|
pData["pkt_retrans_count"] = statComm.getPacketRetransmitCount();
|
|
prevPktCount = pktCntNow;
|
|
prevLosCount = pktLosNow;
|
|
}
|
|
pData["active_seconds"] = statComm.getTime();
|
|
Util::sendUDPApi(pStat);
|
|
lastPushUpdate = now;
|
|
}
|
|
}
|
|
|
|
// Disable stats for HTTP internal output
|
|
if (Comms::sessionStreamInfoMode == SESS_HTTP_DISABLED && capa["name"].asStringRef() == "HTTP"){return;}
|
|
|
|
// Set the token to the pid for outputs which do not generate it in the requestHandler
|
|
if (!tkn.size()){ tkn = JSON::Value(getpid()).asString(); }
|
|
|
|
if (!statComm){
|
|
statComm.reload(streamName, getConnectedBinHost(), tkn, getStatsName(), reqUrl);
|
|
}
|
|
if (!statComm || statComm.getExit()){
|
|
onFail("Shutting down since this session is not allowed to view this stream");
|
|
statComm.unload();
|
|
return;
|
|
}
|
|
|
|
lastStats = now;
|
|
|
|
VERYHIGH_MSG("Writing stats: %s, %s, %s, %" PRIu64 ", %" PRIu64, getConnectedHost().c_str(), streamName.c_str(),
|
|
tkn.c_str(), myConn.dataUp(), myConn.dataDown());
|
|
/*LTS-START*/
|
|
if (statComm.getStatus() & COMM_STATUS_REQDISCONNECT){
|
|
onFail("Shutting down on controller request");
|
|
statComm.unload();
|
|
return;
|
|
}
|
|
/*LTS-END*/
|
|
statComm.setNow(now);
|
|
connStats(now, statComm);
|
|
statComm.setLastSecond(thisPacket ? thisPacket.getTime()/1000 : 0);
|
|
statComm.setPid(getpid());
|
|
|
|
/*LTS-START*/
|
|
// Tag the session with the user agent
|
|
if (newUA && ((now - myConn.connTime()) >= uaDelay || !myConn) && UA.size()){
|
|
JSON::Value APIcall;
|
|
APIcall["tag_sessid"][statComm.sessionId] = "UA:"+UA;
|
|
Util::sendUDPApi(APIcall);
|
|
newUA = false;
|
|
}
|
|
/*LTS-END*/
|
|
|
|
if (isPushing()){
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
if (it->second.getStatus() & COMM_STATUS_REQDISCONNECT){
|
|
if (dropPushTrack(it->second.getTrack(), "disconnect request from buffer")){break;}
|
|
}
|
|
if (!it->second){
|
|
if (dropPushTrack(it->second.getTrack(), "track mapping no longer valid")){break;}
|
|
}
|
|
//if (Util::bootSecs() - M.getLastUpdated(it->first) > 5){
|
|
// if (dropPushTrack(it->second.getTrack(), "track updates being ignored by buffer")){break;}
|
|
//}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Output::connStats(uint64_t now, Comms::Connections &statComm){
|
|
statComm.setUp(myConn.dataUp());
|
|
statComm.setDown(myConn.dataDown());
|
|
statComm.setTime(now - myConn.connTime());
|
|
}
|
|
|
|
bool Output::dropPushTrack(uint32_t trackId, const std::string & dropReason){
|
|
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
|
if (it->second.getTrack() == trackId){
|
|
WARN_MSG("Dropping input track %" PRIu32 ": %s", trackId, dropReason.c_str());
|
|
userSelect.erase(it);
|
|
return true;
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Output::onRequest(){
|
|
// simply clear the buffer, we don't support any kind of input by default
|
|
myConn.Received().clear();
|
|
wantRequest = false;
|
|
}
|
|
|
|
void Output::sendHeader(){
|
|
// just set the sentHeader bool to true, by default
|
|
sentHeader = true;
|
|
}
|
|
|
|
/// \brief Makes the generic writer available to output classes
|
|
/// \param file target URL or filepath
|
|
/// \param append whether to open this connection in truncate or append mode
|
|
/// \param conn connection which will be used to send data. Will use Output's internal myConn if not initialised
|
|
bool Output::connectToFile(std::string file, bool append, Socket::Connection *conn){
|
|
int outFile = -1;
|
|
if (!conn) {conn = &myConn;}
|
|
bool isFileTarget = HTTP::localURIResolver().link(file).isLocalPath();
|
|
if (!Util::externalWriter(file, outFile, append)){return false;}
|
|
if (*conn && isFileTarget) {
|
|
flock(conn->getSocket(), LOCK_UN | LOCK_NB);
|
|
}
|
|
// Lock the file in exclusive mode to ensure no other processes write to it
|
|
if(isFileTarget && flock(outFile, LOCK_EX | LOCK_NB)){
|
|
ERROR_MSG("Failed to lock file %s, error: %s", file.c_str(), strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
//Ensure the Socket::Connection is valid before we overwrite the socket
|
|
if (!*conn){
|
|
static int tmpFd = open("/dev/null", O_RDWR);
|
|
conn->open(tmpFd);
|
|
//We always want to close sockets opened in this way on fork
|
|
Util::Procs::socketList.insert(tmpFd);
|
|
}
|
|
|
|
int r = dup2(outFile, conn->getSocket());
|
|
if (r == -1){
|
|
ERROR_MSG("Failed to create an alias for the socket %d -> %d using dup2: %s.", outFile, conn->getSocket(), strerror(errno));
|
|
return false;
|
|
}
|
|
close(outFile);
|
|
return true;
|
|
}
|
|
|
|
std::string Output::getExitTriggerPayload(){
|
|
uint64_t rightNow = Util::epoch();
|
|
std::stringstream payl;
|
|
payl << streamName << '\n';
|
|
payl << config->getString("target") << '\n';
|
|
payl << capa["name"].asStringRef() << '\n';
|
|
payl << myConn.dataUp() << '\n';
|
|
payl << (Util::bootSecs() - myConn.connTime()) << '\n';
|
|
payl << (rightNow - (Util::bootSecs() - myConn.connTime())) << '\n';
|
|
payl << rightNow << '\n';
|
|
if (firstPacketTime != 0xFFFFFFFFFFFFFFFFull){
|
|
payl << (lastPacketTime - firstPacketTime) << '\n';
|
|
}else{
|
|
payl << 0 << '\n';
|
|
}
|
|
payl << firstPacketTime << '\n';
|
|
payl << lastPacketTime << '\n';
|
|
payl << Util::mRExitReason << '\n';
|
|
payl << Util::exitReason << '\n';
|
|
return payl.str();
|
|
}
|
|
|
|
void Output::recEndTrigger(){
|
|
if (Util::Config::binaryType == Util::OUTPUT && config->hasOption("target") && Triggers::shouldTrigger("RECORDING_END", streamName)){
|
|
Triggers::doTrigger("RECORDING_END", getExitTriggerPayload(), streamName);
|
|
}
|
|
}
|
|
|
|
void Output::outputEndTrigger(){
|
|
if (Util::Config::binaryType == Util::OUTPUT && config->hasOption("target") && Triggers::shouldTrigger("OUTPUT_END", streamName)){
|
|
Triggers::doTrigger("OUTPUT_END", getExitTriggerPayload(), streamName);
|
|
}
|
|
}
|
|
|
|
/// Checks if the set streamName allows pushes from this connector/IP/password combination.
|
|
/// Runs all appropriate triggers and checks.
|
|
/// Returns true if the push should continue, false otherwise.
|
|
bool Output::allowPush(const std::string &passwd){
|
|
pushing = true;
|
|
std::string strmSource;
|
|
|
|
// Initialize the stream source if needed, connect to it
|
|
waitForStreamPushReady();
|
|
// pull the source setting from metadata
|
|
if (meta){strmSource = meta.getSource();}
|
|
|
|
if (!strmSource.size()){
|
|
FAIL_MSG("Push rejected - stream %s not configured or unavailable", streamName.c_str());
|
|
pushing = false;
|
|
return false;
|
|
}
|
|
if (strmSource.substr(0, 7) != "push://"){
|
|
FAIL_MSG("Push rejected - stream %s not a push-able stream. (%s != push://*)",
|
|
streamName.c_str(), strmSource.c_str());
|
|
pushing = false;
|
|
return false;
|
|
}
|
|
|
|
std::string source = strmSource.substr(7);
|
|
std::string IP = source.substr(0, source.find('@'));
|
|
|
|
/*LTS-START*/
|
|
std::string password;
|
|
if (source.find('@') != std::string::npos){
|
|
password = source.substr(source.find('@') + 1);
|
|
if (password != ""){
|
|
if (password == passwd){
|
|
INFO_MSG("Password accepted - ignoring IP settings.");
|
|
IP = "";
|
|
}else{
|
|
INFO_MSG("Password rejected - checking IP.");
|
|
if (IP == ""){IP = "deny-all.invalid";}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Triggers::shouldTrigger("STREAM_PUSH", streamName)){
|
|
std::string payload = streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n" + reqUrl;
|
|
if (!Triggers::doTrigger("STREAM_PUSH", payload, streamName)){
|
|
WARN_MSG("Push from %s rejected by STREAM_PUSH trigger", getConnectedHost().c_str());
|
|
pushing = false;
|
|
return false;
|
|
}
|
|
}
|
|
/*LTS-END*/
|
|
|
|
if (IP != ""){
|
|
if (!myConn.isAddress(IP)){
|
|
WARN_MSG("Push from %s rejected; not whitelisted", getConnectedHost().c_str());
|
|
pushing = false;
|
|
return false;
|
|
}
|
|
}
|
|
initialize();
|
|
return true;
|
|
}
|
|
|
|
/// Attempts to wait for a stream to finish shutting down if it is, then restarts and reconnects.
|
|
void Output::waitForStreamPushReady(){
|
|
uint8_t streamStatus = Util::getStreamStatus(streamName);
|
|
MEDIUM_MSG("Current status for %s buffer is %u", streamName.c_str(), streamStatus);
|
|
if (streamStatus == STRMSTAT_READY){
|
|
reconnect();
|
|
std::set<size_t> vTracks = M.getValidTracks(true);
|
|
INFO_MSG("Stream already active (%zu valid tracks) - check if it's not shutting down...", vTracks.size());
|
|
uint64_t oneTime = 0;
|
|
uint64_t twoTime = 0;
|
|
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){
|
|
if (M.getNowms(*it) > oneTime){oneTime = M.getNowms(*it);}
|
|
}
|
|
Util::wait(2000);
|
|
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){
|
|
if (M.getNowms(*it) > twoTime){twoTime = M.getNowms(*it);}
|
|
}
|
|
if (twoTime <= oneTime+500){
|
|
disconnect();
|
|
INFO_MSG("Waiting for stream reset before attempting push input accept (%" PRIu64 " <= %" PRIu64 "+500)", twoTime, oneTime);
|
|
while (streamStatus != STRMSTAT_OFF && keepGoing()){
|
|
userSelect.clear();
|
|
Util::wait(250);
|
|
streamStatus = Util::getStreamStatus(streamName);
|
|
}
|
|
reconnect();
|
|
}
|
|
}
|
|
while (((streamStatus != STRMSTAT_WAIT && streamStatus != STRMSTAT_READY) || !meta) && keepGoing()){
|
|
INFO_MSG("Waiting for %s buffer to be ready... (%u)", streamName.c_str(), streamStatus);
|
|
disconnect();
|
|
streamStatus = Util::getStreamStatus(streamName);
|
|
if (streamStatus == STRMSTAT_OFF || streamStatus == STRMSTAT_WAIT || streamStatus == STRMSTAT_READY){
|
|
INFO_MSG("Reconnecting to %s buffer... (%u)", streamName.c_str(), streamStatus);
|
|
reconnect();
|
|
streamStatus = Util::getStreamStatus(streamName);
|
|
}
|
|
if (((streamStatus != STRMSTAT_WAIT && streamStatus != STRMSTAT_READY) || !meta) && keepGoing()){
|
|
Util::wait(100);
|
|
}
|
|
}
|
|
if (streamStatus == STRMSTAT_READY || streamStatus == STRMSTAT_WAIT){reconnect();}
|
|
if (!meta){
|
|
onFail("Could not connect to stream data", true);
|
|
}
|
|
}
|
|
|
|
void Output::selectAllTracks(){
|
|
std::set<size_t> tracks = getSupportedTracks();
|
|
for (std::set<size_t>::iterator it = tracks.begin(); it != tracks.end(); it++){
|
|
userSelect[*it].reload(streamName, *it);
|
|
}
|
|
}
|
|
}// namespace Mist
|