Various fixes, among which:
- Fixed segfault when attempting to initialseek on disconnected streams - Fix 100% CPU bug in controller's stats code - WebRTC UDP bind socket improvements - Several segfault fixes - Increased packet reordering buffer size from 30 to 150 packets - Tweaks to default output/buffer behaviour for incoming pushes - Added message for load balancer checks - Fixed HLS content type - Stats fixes - Exit reason fixes - Fixed socket IP address detection - Fixed non-string arguments for stream settings - Added caching for getConnectedBinHost() - Added WebRTC playback rate control - Added/completed VP8/VP9 support to WebRTC/RTSP - Added live seek option to WebRTC - Fixed seek to exactly newest timestamp - Fixed HLS input # Conflicts: # lib/defines.h # src/input/input.cpp
This commit is contained in:
parent
2b99f2f5ea
commit
0af992d405
75 changed files with 1512 additions and 790 deletions
|
@ -12,7 +12,7 @@ int spawnForked(Socket::Connection &S){
|
|||
void handleUSR1(int signum, siginfo_t *sigInfo, void *ignore){
|
||||
HIGH_MSG("USR1 received - triggering rolling restart");
|
||||
Util::Config::is_restarting = true;
|
||||
Util::Config::logExitReason("setting is_active to false because of received USR1");
|
||||
Util::logExitReason("signal USR1");
|
||||
Util::Config::is_active = false;
|
||||
}
|
||||
|
||||
|
@ -47,5 +47,7 @@ int main(int argc, char *argv[]){
|
|||
return tmp.run();
|
||||
}
|
||||
}
|
||||
INFO_MSG("Exit reason: %s", Util::exitReason);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,6 @@ namespace Mist{
|
|||
|
||||
Output::Output(Socket::Connection &conn) : myConn(conn){
|
||||
pushing = false;
|
||||
pushIsOngoing = false;
|
||||
firstTime = 0;
|
||||
firstPacketTime = 0xFFFFFFFFFFFFFFFFull;
|
||||
lastPacketTime = 0;
|
||||
|
@ -67,6 +66,10 @@ namespace Mist{
|
|||
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!");
|
||||
}
|
||||
|
@ -129,6 +132,7 @@ namespace Mist{
|
|||
}else{
|
||||
MEDIUM_MSG("onFail '%s': %s", streamName.c_str(), msg.c_str());
|
||||
}
|
||||
Util::logExitReason(msg.c_str());
|
||||
isInitialized = false;
|
||||
wantRequest = true;
|
||||
parseData = false;
|
||||
|
@ -143,7 +147,7 @@ namespace Mist{
|
|||
}
|
||||
reconnect();
|
||||
// if the connection failed, fail
|
||||
if (streamName.size() < 1){
|
||||
if (!meta || streamName.size() < 1){
|
||||
onFail("Could not connect to stream", true);
|
||||
return;
|
||||
}
|
||||
|
@ -181,19 +185,8 @@ namespace Mist{
|
|||
if (shmSessions.mapped){
|
||||
char shmEmpty[SHM_SESSIONS_ITEM];
|
||||
memset(shmEmpty, 0, SHM_SESSIONS_ITEM);
|
||||
std::string host = statComm.getHost();
|
||||
if (host.substr(0, 12) ==
|
||||
std::string("\000\000\000\000\000\000\000\000\000\000\377\377", 12)){
|
||||
char tmpstr[16];
|
||||
snprintf(tmpstr, 16, "%hhu.%hhu.%hhu.%hhu", host[12], host[13], host[14], host[15]);
|
||||
host = tmpstr;
|
||||
}else{
|
||||
char tmpstr[40];
|
||||
snprintf(tmpstr, 40, "%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x:%.2x%.2x",
|
||||
host[0], host[1], host[2], host[3], host[4], host[5], host[6], host[7],
|
||||
host[8], host[9], host[10], host[11], host[12], host[13], host[14], host[15]);
|
||||
host = tmpstr;
|
||||
}
|
||||
std::string host;
|
||||
Socket::hostBytesToStr(statComm.getHost().data(), 16, host);
|
||||
uint32_t shmOffset = 0;
|
||||
const std::string &cName = capa["name"].asStringRef();
|
||||
while (shmOffset + SHM_SESSIONS_ITEM < SHM_SESSIONS_SIZE){
|
||||
|
@ -259,12 +252,19 @@ namespace Mist{
|
|||
|
||||
std::string Output::getConnectedHost(){return myConn.getHost();}
|
||||
|
||||
std::string Output::getConnectedBinHost(){return myConn.getBinHost();}
|
||||
std::string Output::getConnectedBinHost(){
|
||||
if (!prevHost.size()){
|
||||
if (myConn && myConn.getPureSocket() != -1){
|
||||
prevHost = myConn.getBinHost();
|
||||
}
|
||||
if (!prevHost.size()){prevHost.assign("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\001", 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")){return true;}
|
||||
if (isPushing()){return true;}
|
||||
if (!isInitialized){initialize();}
|
||||
meta.refresh();
|
||||
if (getSupportedTracks().size()){
|
||||
|
@ -363,27 +363,25 @@ namespace Mist{
|
|||
while (!meta && ++attempts < 20 && Util::streamAlive(streamName)){
|
||||
meta.reInit(streamName, false);
|
||||
}
|
||||
if (!meta){
|
||||
onFail("Could not connect to stream data", true);
|
||||
return;
|
||||
}
|
||||
if (!meta){return;}
|
||||
meta.refresh();
|
||||
isInitialized = true;
|
||||
statComm.reload();
|
||||
stats(true);
|
||||
if (!pushing){selectDefaultTracks();}
|
||||
if (!M.getVod() && !isReadyForPlay()){
|
||||
uint64_t waitUntil = Util::epoch() + 30;
|
||||
if (isPushing()){return;}
|
||||
if (!isRecording() && !M.getVod() && !isReadyForPlay()){
|
||||
uint64_t waitUntil = Util::bootSecs() + 45;
|
||||
while (!M.getVod() && !isReadyForPlay()){
|
||||
if (Util::epoch() > waitUntil + 45 || (!userSelect.size() && Util::epoch() > waitUntil)){
|
||||
INFO_MSG("Giving up waiting for playable tracks. Stream: %s, IP: %s", streamName.c_str(),
|
||||
getConnectedHost().c_str());
|
||||
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.refresh();
|
||||
stats();
|
||||
}
|
||||
}
|
||||
selectDefaultTracks();
|
||||
}
|
||||
|
||||
std::set<size_t> Output::getSupportedTracks(const std::string &type) const{
|
||||
|
@ -403,22 +401,11 @@ namespace Mist{
|
|||
bool autoSeek = buffer.size();
|
||||
uint64_t seekTarget = currentTime();
|
||||
std::set<size_t> newSelects =
|
||||
Util::wouldSelect(myMeta, targetParams, capa, UA, autoSeek ? seekTarget : 0);
|
||||
|
||||
std::set<size_t> oldSel;
|
||||
for (std::set<unsigned long>::iterator selIt = selectedTracks.begin();
|
||||
selIt != selectedTracks.end(); ++selIt){
|
||||
oldSel.insert(*selIt);
|
||||
}
|
||||
|
||||
if (oldSel == newSelects){
|
||||
// No new selections? Do nothing, return no change.
|
||||
return false;
|
||||
}
|
||||
Util::wouldSelect(M, targetParams, capa, UA, autoSeek ? seekTarget : 0);
|
||||
|
||||
if (autoSeek){
|
||||
std::set<size_t> toRemove;
|
||||
for (std::set<size_t>::iterator it = wouldSelect.begin(); it != wouldSelect.end(); it++){
|
||||
for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){
|
||||
// autoSeeking and target not in bounds? Drop it too.
|
||||
if (M.getLastms(*it) < std::max(seekTarget, (uint64_t)6000lu) - 6000){
|
||||
toRemove.insert(*it);
|
||||
|
@ -426,7 +413,7 @@ namespace Mist{
|
|||
}
|
||||
// remove those from selectedtracks
|
||||
for (std::set<size_t>::iterator it = toRemove.begin(); it != toRemove.end(); it++){
|
||||
wouldSelect.erase(*it);
|
||||
newSelects.erase(*it);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -435,7 +422,7 @@ namespace Mist{
|
|||
userSelect.clear();
|
||||
|
||||
// Select tracks here!
|
||||
for (std::set<size_t>::iterator it = wouldSelect.begin(); it != wouldSelect.end(); it++){
|
||||
for (std::set<size_t>::iterator it = newSelects.begin(); it != newSelects.end(); it++){
|
||||
userSelect[*it].reload(streamName, *it);
|
||||
}
|
||||
|
||||
|
@ -453,6 +440,14 @@ namespace Mist{
|
|||
parseData = false;
|
||||
}
|
||||
|
||||
///Returns the timestamp of the next upcoming keyframe after thisPacket, or 0 if that cannot be determined (yet).
|
||||
uint64_t Output::nextKeyTime(){
|
||||
DTSC::Keys keys(M.keys(getMainSelectedTrack()));
|
||||
if (!keys.getValidCount()){return 0;}
|
||||
size_t keyNum = keys.getNumForTime(lastPacketTime);
|
||||
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++){
|
||||
|
@ -671,7 +666,7 @@ namespace Mist{
|
|||
for (std::set<size_t>::iterator it = seekTracks.begin(); it != seekTracks.end(); it++){
|
||||
seek(*it, pos, false);
|
||||
}
|
||||
firstTime = Util::bootMS() - currentTime();
|
||||
firstTime = Util::bootMS() - (currentTime() * realTime / 1000);
|
||||
}
|
||||
|
||||
bool Output::seek(size_t tid, uint64_t pos, bool getNextKey){
|
||||
|
@ -690,7 +685,7 @@ namespace Mist{
|
|||
stats();
|
||||
}
|
||||
}
|
||||
if (meta.getLastms(tid) <= pos){
|
||||
if (meta.getLastms(tid) < pos){
|
||||
WARN_MSG("Aborting seek to %" PRIu64 "ms in track %zu: past end of track (= %" PRIu64 "ms).",
|
||||
pos, tid, meta.getLastms(tid));
|
||||
userSelect.erase(tid);
|
||||
|
@ -751,7 +746,7 @@ namespace Mist{
|
|||
if (curPage[tid].mapped[tmp.offset]){return seek(tid, pos, getNextKey);}
|
||||
FAIL_MSG("Track %zu no data (key %zu@%" PRIu64 ") - timeout", tid, keyNum + (getNextKey ? 1 : 0), tmp.offset);
|
||||
userSelect.erase(tid);
|
||||
firstTime = Util::bootMS() - buffer.begin()->time;
|
||||
firstTime = Util::bootMS() - (buffer.begin()->time * realTime / 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -761,6 +756,7 @@ namespace Mist{
|
|||
/// 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(){
|
||||
if (!meta){return;}
|
||||
uint64_t seekPos = 0;
|
||||
if (meta.getLive()){
|
||||
size_t mainTrack = getMainSelectedTrack();
|
||||
|
@ -980,6 +976,7 @@ namespace Mist{
|
|||
/// 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(){
|
||||
if (!realTime){return false;}//Makes no sense when playing in turbo mode
|
||||
static uint32_t seekCount = 2;
|
||||
uint64_t seekPos = 0;
|
||||
if (!meta.getLive()){return false;}
|
||||
|
@ -989,11 +986,15 @@ namespace Mist{
|
|||
uint64_t cTime = thisPacket.getTime();
|
||||
uint64_t mKa = getMinKeepAway();
|
||||
if (!maxSkipAhead){
|
||||
bool noReturn = false;
|
||||
uint64_t newSpeed = 1000;
|
||||
if (lMs - mKa - needsLookAhead - extraKeepAway > cTime + 50){
|
||||
// We need to speed up!
|
||||
uint64_t diff = (lMs - mKa - needsLookAhead - extraKeepAway) - cTime;
|
||||
if (diff > 1000){
|
||||
if (diff > 3000){
|
||||
noReturn = true;
|
||||
newSpeed = 1000;
|
||||
}else if (diff > 1000){
|
||||
newSpeed = 750;
|
||||
}else if (diff > 500){
|
||||
newSpeed = 900;
|
||||
|
@ -1002,13 +1003,11 @@ namespace Mist{
|
|||
}
|
||||
}
|
||||
if (realTime != newSpeed){
|
||||
HIGH_MSG("Changing playback speed from %" PRIu64 " to %" PRIu64 "(%" PRIu64
|
||||
" ms LA, %" PRIu64 " ms mKA, %lu eKA)",
|
||||
realTime, newSpeed, needsLookAhead, mKa, extraKeepAway);
|
||||
firstTime = Util::bootMS() - cTime;
|
||||
HIGH_MSG("Changing playback speed from %" PRIu64 " to %" PRIu64 "(%" PRIu64 " ms LA, %" PRIu64 " ms mKA, %lu eKA)", realTime, newSpeed, needsLookAhead, mKa, extraKeepAway);
|
||||
firstTime = Util::bootMS() - (cTime * newSpeed / 1000);
|
||||
realTime = newSpeed;
|
||||
}
|
||||
return false;
|
||||
if (!noReturn){return false;}
|
||||
}
|
||||
// cancel if there are no keys in the main track
|
||||
if (mainTrack == INVALID_TRACK_ID){return false;}
|
||||
|
@ -1167,8 +1166,14 @@ namespace Mist{
|
|||
while (keepGoing() && (wantRequest || parseData)){
|
||||
if (wantRequest){requestHandler();}
|
||||
if (parseData){
|
||||
if (!isInitialized){initialize();}
|
||||
if (!sentHeader){
|
||||
if (!isInitialized){
|
||||
initialize();
|
||||
if (!isInitialized){
|
||||
onFail("Stream initialization failed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!sentHeader && keepGoing()){
|
||||
DONTEVEN_MSG("sendHeader");
|
||||
sendHeader();
|
||||
}
|
||||
|
@ -1186,6 +1191,8 @@ namespace Mist{
|
|||
Util::sleep(std::min(thisPacket.getTime() -
|
||||
((((Util::bootMS() - firstTime) * 1000) + maxSkipAhead) / realTime),
|
||||
1000ul));
|
||||
//Make sure we stay responsive to requests and stats while waiting
|
||||
if (wantRequest){requestHandler();}
|
||||
stats();
|
||||
}
|
||||
}
|
||||
|
@ -1217,6 +1224,8 @@ namespace Mist{
|
|||
}else{
|
||||
playbackSleep(sleepTime);
|
||||
}
|
||||
//Make sure we stay responsive to requests and stats while waiting
|
||||
if (wantRequest){requestHandler();}
|
||||
stats();
|
||||
}
|
||||
if (!timeoutTries){
|
||||
|
@ -1237,6 +1246,7 @@ namespace Mist{
|
|||
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("failed to open file, aborting: %s", newTarget.c_str());
|
||||
onFinish();
|
||||
break;
|
||||
}
|
||||
|
@ -1247,13 +1257,14 @@ namespace Mist{
|
|||
}else{
|
||||
if (!onFinish()){
|
||||
INFO_MSG("Shutting down because planned stopping point reached");
|
||||
Util::logExitReason("planned stopping point reached");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
sendNext();
|
||||
}else{
|
||||
INFO_MSG("Shutting down because of stream end");
|
||||
Util::logExitReason("end of stream");
|
||||
/*LTS-START*/
|
||||
if (Triggers::shouldTrigger("CONN_STOP", streamName)){
|
||||
std::string payload =
|
||||
|
@ -1265,18 +1276,14 @@ namespace Mist{
|
|||
}
|
||||
}
|
||||
if (!meta){
|
||||
Util::Config::logExitReason("No connection to the metadata");
|
||||
Util::logExitReason("lost internal connection to stream data");
|
||||
break;
|
||||
}
|
||||
}
|
||||
stats();
|
||||
}
|
||||
MEDIUM_MSG("MistOut client handler shutting down: %s, %s, %s",
|
||||
myConn.connected() ? "conn_active" : "conn_closed", wantRequest ? "want_request" : "no_want_request",
|
||||
parseData ? "parsing_data" : "not_parsing_data");
|
||||
if (Util::Config::exitReason.size()){
|
||||
INFO_MSG("Logged exit reason: %s", Util::Config::exitReason.c_str());
|
||||
}
|
||||
if (!myConn){Util::logExitReason("remote connection closed");}
|
||||
INFO_MSG("Client handler shutting down, exit reason: %s", Util::exitReason);
|
||||
onFinish();
|
||||
|
||||
/*LTS-START*/
|
||||
|
@ -1460,42 +1467,66 @@ namespace Mist{
|
|||
|
||||
DTSC::Keys keys(M.keys(nxt.tid));
|
||||
size_t thisKey = keys.getNumForTime(nxt.time);
|
||||
while (!nextTime && keepGoing()){
|
||||
// The next packet is either not available yet, or on another page
|
||||
// Check if we have a next valid packet
|
||||
if (memcmp(curPage[nxt.tid].mapped + nxt.offset + preLoad.getDataLen(), "\000\000\000\000", 4)){
|
||||
nextTime = getDTSCTime(curPage[nxt.tid].mapped, nxt.offset + preLoad.getDataLen());
|
||||
// After 500ms, we assume the time will not update anymore
|
||||
if (++emptyCount >= 20){break;}
|
||||
|
||||
// Check if we have a next valid packet
|
||||
if (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, but has no time. Please warn the developers if you see this message!");
|
||||
dropTrack(nxt.tid, "EOP: invalid next packet");
|
||||
return false;
|
||||
}
|
||||
}else{
|
||||
//no next packet yet!
|
||||
//Check if this is the last packet of a VoD stream. Return success and drop the track.
|
||||
if (M.getVod() && nxt.time >= M.getLastms(nxt.tid)){
|
||||
thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true);
|
||||
thisIdx = nxt.tid;
|
||||
dropTrack(nxt.tid, "end of VoD track reached", false);
|
||||
return true;
|
||||
}
|
||||
//Check if there exists a different page for the next key
|
||||
size_t 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);
|
||||
}else{
|
||||
if (M.getVod() && nxt.time >= M.getLastms(nxt.tid)){break;}
|
||||
if (M.getPageNumberForKey(nxt.tid, thisKey + 1) != currentPage[nxt.tid]){
|
||||
// Check if its on a different page
|
||||
nextTime = keys.getTime(thisKey + 1);
|
||||
}
|
||||
//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.
|
||||
// after ~25 seconds, give up and drop the track.
|
||||
if (++emptyCount >= 1000){
|
||||
// after ~25 seconds, give up and drop the track.
|
||||
dropTrack(nxt.tid, "EOP: data wait timeout");
|
||||
return false;
|
||||
}
|
||||
Util::sleep(25);
|
||||
// we're waiting for new data to show up
|
||||
if (emptyCount % 640 == 0){
|
||||
reconnect(); // reconnect every 16 seconds
|
||||
// if we don't have a connection to the metadata here, this means the stream has gone offline in the meanwhile.
|
||||
if (!meta){return false;}
|
||||
//every ~1 second, check if the stream is not offline
|
||||
if (emptyCount % 40 == 0 && M.getLive() && Util::getStreamStatus(streamName) == STRMSTAT_OFF){
|
||||
Util::logExitReason("Stream source shut down");
|
||||
thisPacket.null();
|
||||
return true;
|
||||
}
|
||||
//every ~16 seconds, reconnect to metadata
|
||||
if (emptyCount % 640 == 0){
|
||||
reconnect();
|
||||
if (!meta){
|
||||
onFail("Could not connect to stream data", true);
|
||||
thisPacket.null();
|
||||
return true;
|
||||
}
|
||||
// if we don't have a connection to the metadata here, this means the stream has gone offline in the meanwhile.
|
||||
if (!meta){
|
||||
Util::logExitReason("Attempted reconnect to source failed");
|
||||
thisPacket.null();
|
||||
return true;
|
||||
}
|
||||
return false;//no sleep after reconnect
|
||||
}
|
||||
//Fine! We didn't want a packet, anyway. Let's try again later.
|
||||
Util::sleep(25);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (M.getVod() && nxt.time >= M.getLastms(nxt.tid)){
|
||||
thisPacket.reInit(curPage[nxt.tid].mapped + nxt.offset, 0, true);
|
||||
thisIdx = nxt.tid;
|
||||
dropTrack(nxt.tid, "end of VoD track reached", false);
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't have a timestamp at all, this is due to a different cause.
|
||||
if (!nextTime && (emptyCount < 20)){return false;}
|
||||
//If the next packet should've been before the current packet, something is wrong. Abort, abort!
|
||||
if (nextTime < nxt.time){
|
||||
dropTrack(nxt.tid, "time going backwards");
|
||||
return false;
|
||||
|
@ -1513,7 +1544,7 @@ namespace Mist{
|
|||
emptyCount = 0; // valid packet - reset empty counter
|
||||
|
||||
if (!userSelect[nxt.tid].isAlive()){
|
||||
INFO_MSG("Track %zu is not alive!", nxt.tid);
|
||||
INFO_MSG("Track %zu is not alive!", nxt.tid);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -1526,12 +1557,6 @@ namespace Mist{
|
|||
|
||||
// exchange the current packet in the buffer for the next one
|
||||
buffer.erase(buffer.begin());
|
||||
|
||||
if (M.getVod() && nxt.time > M.getLastms(nxt.tid)){
|
||||
dropTrack(nxt.tid, "detected last VoD packet");
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer.insert(nxt);
|
||||
|
||||
return true;
|
||||
|
@ -1541,8 +1566,10 @@ namespace Mist{
|
|||
/// 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";}
|
||||
if (config->hasOption("target") && config->getString("target").size()){return "OUTPUT";}
|
||||
if (isPushing()){return "INPUT:" + capa["name"].asStringRef();}
|
||||
if (config->hasOption("target") && config->getString("target").size()){
|
||||
return "OUTPUT:" + capa["name"].asStringRef();
|
||||
}
|
||||
return capa["name"].asStringRef();
|
||||
}
|
||||
|
||||
|
@ -1554,6 +1581,25 @@ namespace Mist{
|
|||
uint64_t now = Util::bootSecs();
|
||||
if (now == lastStats && !force){return;}
|
||||
|
||||
if (isRecording()){
|
||||
static uint64_t 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(it->first);
|
||||
}
|
||||
pData["bytes"] = myConn.dataUp();
|
||||
pData["active_seconds"] = (now - myConn.connTime());
|
||||
Socket::UDPConnection uSock;
|
||||
uSock.SetDestination("localhost", 4242);
|
||||
uSock.SendNow(pStat.toString());
|
||||
lastPushUpdate = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (!statComm){statComm.reload();}
|
||||
if (!statComm){return;}
|
||||
|
||||
|
@ -1562,16 +1608,13 @@ namespace Mist{
|
|||
HIGH_MSG("Writing stats: %s, %s, %u, %lu, %lu", getConnectedHost().c_str(), streamName.c_str(),
|
||||
crc & 0xFFFFFFFFu, myConn.dataUp(), myConn.dataDown());
|
||||
/*LTS-START*/
|
||||
if (statComm.getStatus() == COMM_STATUS_DISCONNECT){
|
||||
if (statComm.getStatus() == COMM_STATUS_REQDISCONNECT){
|
||||
onFail("Shutting down on controller request");
|
||||
return;
|
||||
}
|
||||
/*LTS-END*/
|
||||
statComm.setNow(now);
|
||||
if (statComm.getHost() ==
|
||||
std::string("\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000", 16)){
|
||||
statComm.setHost(getConnectedBinHost());
|
||||
}
|
||||
statComm.setHost(getConnectedBinHost());
|
||||
statComm.setCRC(crc);
|
||||
statComm.setStream(streamName);
|
||||
statComm.setConnector(getStatsName());
|
||||
|
@ -1597,16 +1640,38 @@ namespace Mist{
|
|||
|
||||
doSync();
|
||||
|
||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||
it->second.keepAlive();
|
||||
if (it->second.isAlive() && M.getLive() && it->second.getStatus() & COMM_STATUS_SOURCE){
|
||||
if (Util::bootSecs() - M.getLastUpdated(it->first) > 3){
|
||||
INFO_MSG("Not updating data for track %zu?", it->first);
|
||||
if (isPushing()){
|
||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||
it->second.keepAlive();
|
||||
if (it->second.getStatus() == COMM_STATUS_REQDISCONNECT){
|
||||
if (dropPushTrack(it->second.getTrack(), "disconnect request from buffer")){break;}
|
||||
}
|
||||
if (!it->second.isAlive()){
|
||||
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;}
|
||||
//}
|
||||
}
|
||||
}else{
|
||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||
it->second.keepAlive();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -1651,7 +1716,7 @@ namespace Mist{
|
|||
// Initialize the stream source if needed, connect to it
|
||||
waitForStreamPushReady();
|
||||
// pull the source setting from metadata
|
||||
strmSource = meta.getSource();
|
||||
if (meta){strmSource = meta.getSource();}
|
||||
|
||||
if (!strmSource.size()){
|
||||
FAIL_MSG("Push rejected - stream %s not configured or unavailable", streamName.c_str());
|
||||
|
@ -1683,13 +1748,10 @@ namespace Mist{
|
|||
}
|
||||
}
|
||||
|
||||
std::string smp = streamName.substr(0, streamName.find_first_of("+ "));
|
||||
if (Triggers::shouldTrigger("STREAM_PUSH", smp)){
|
||||
std::string payload =
|
||||
streamName + "\n" + getConnectedHost() + "\n" + capa["name"].asStringRef() + "\n" + reqUrl;
|
||||
if (!Triggers::doTrigger("STREAM_PUSH", payload, smp)){
|
||||
FAIL_MSG("Push from %s to %s rejected - STREAM_PUSH trigger denied the push",
|
||||
getConnectedHost().c_str(), streamName.c_str());
|
||||
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;
|
||||
}
|
||||
|
@ -1698,8 +1760,7 @@ namespace Mist{
|
|||
|
||||
if (IP != ""){
|
||||
if (!myConn.isAddress(IP)){
|
||||
FAIL_MSG("Push from %s to %s rejected - source host not whitelisted",
|
||||
getConnectedHost().c_str(), streamName.c_str());
|
||||
WARN_MSG("Push from %s rejected; not whitelisted", getConnectedHost().c_str());
|
||||
pushing = false;
|
||||
return false;
|
||||
}
|
||||
|
@ -1712,7 +1773,31 @@ namespace Mist{
|
|||
void Output::waitForStreamPushReady(){
|
||||
uint8_t streamStatus = Util::getStreamStatus(streamName);
|
||||
MEDIUM_MSG("Current status for %s buffer is %u", streamName.c_str(), streamStatus);
|
||||
while (streamStatus != STRMSTAT_WAIT && streamStatus != STRMSTAT_READY && keepGoing()){
|
||||
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.getLastms(*it) > oneTime){oneTime = M.getLastms(*it);}
|
||||
}
|
||||
Util::wait(2000);
|
||||
for (std::set<size_t>::iterator it = vTracks.begin(); it != vTracks.end(); ++it){
|
||||
if (M.getLastms(*it) > twoTime){twoTime = M.getLastms(*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(1000);
|
||||
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();
|
||||
userSelect.clear();
|
||||
|
@ -1720,11 +1805,15 @@ namespace Mist{
|
|||
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);
|
||||
Util::wait(500);
|
||||
reconnect();
|
||||
streamStatus = Util::getStreamStatus(streamName);
|
||||
}
|
||||
}
|
||||
if (streamStatus == STRMSTAT_READY || streamStatus == STRMSTAT_WAIT){reconnect();}
|
||||
if (!meta){
|
||||
onFail("Could not connect to stream data", true);
|
||||
}
|
||||
}
|
||||
|
||||
void Output::selectAllTracks(){
|
||||
|
|
|
@ -65,6 +65,7 @@ namespace Mist{
|
|||
/// This function is called whenever a packet is ready for sending.
|
||||
/// Inside it, thisPacket is guaranteed to contain a valid packet.
|
||||
virtual void sendNext(){}// REQUIRED! Others are optional.
|
||||
virtual bool dropPushTrack(uint32_t trackId, const std::string & dropReason);
|
||||
bool getKeyFrame();
|
||||
bool prepareNext();
|
||||
virtual void dropTrack(size_t trackId, const std::string &reason, bool probablyBad = true);
|
||||
|
@ -105,6 +106,7 @@ namespace Mist{
|
|||
std::set<sortedPageInfo> buffer; ///< A sorted list of next-to-be-loaded packets.
|
||||
bool sought; ///< If a seek has been done, this is set to true. Used for seeking on
|
||||
///< prepareNext().
|
||||
std::string prevHost; ///< Old value for getConnectedBinHost, for caching
|
||||
protected: // these are to be messed with by child classes
|
||||
virtual bool inlineRestartCapable() const{
|
||||
return false;
|
||||
|
@ -128,6 +130,7 @@ namespace Mist{
|
|||
Comms::Statistics statComm;
|
||||
bool isBlocking; ///< If true, indicates that myConn is blocking.
|
||||
uint32_t crc; ///< Checksum, if any, for usage in the stats.
|
||||
uint64_t nextKeyTime();
|
||||
|
||||
// stream delaying variables
|
||||
uint64_t maxSkipAhead; ///< Maximum ms that we will go ahead of the intended timestamps.
|
||||
|
@ -148,7 +151,6 @@ namespace Mist{
|
|||
virtual bool isPushing(){return pushing;};
|
||||
bool allowPush(const std::string &passwd);
|
||||
void waitForStreamPushReady();
|
||||
bool pushIsOngoing;
|
||||
|
||||
uint64_t firstPacketTime;
|
||||
uint64_t lastPacketTime;
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
#include <mist/stream.h>
|
||||
#include <mist/timing.h>
|
||||
|
||||
uint64_t bootMsOffset;
|
||||
|
||||
namespace Mist{
|
||||
|
||||
OutCMAF::OutCMAF(Socket::Connection &conn) : HTTPOutput(conn){
|
||||
|
@ -68,6 +70,8 @@ namespace Mist{
|
|||
|
||||
void OutCMAF::onHTTP(){
|
||||
initialize();
|
||||
bootMsOffset = 0;
|
||||
if (M.getLive()){bootMsOffset = M.getBootMsOffset();}
|
||||
|
||||
if (H.url.size() < streamName.length() + 7){
|
||||
H.Clean();
|
||||
|
@ -440,6 +444,12 @@ namespace Mist{
|
|||
}
|
||||
|
||||
void hlsSegment(uint64_t start, uint64_t duration, std::stringstream &s, bool first){
|
||||
if (bootMsOffset){
|
||||
uint64_t unixMs = start + bootMsOffset + (Util::unixMS() - Util::bootMS());
|
||||
time_t uSecs = unixMs/1000;
|
||||
struct tm * tVal = gmtime(&uSecs);
|
||||
s << "#EXT-X-PROGRAM-DATE-TIME: " << (tVal->tm_year+1900) << "-" << std::setw(2) << std::setfill('0') << (tVal->tm_mon+1) << "-" << std::setw(2) << std::setfill('0') << tVal->tm_mday << "T" << std::setw(2) << std::setfill('0') << tVal->tm_hour << ":" << std::setw(2) << std::setfill('0') << tVal->tm_min << ":" << std::setw(2) << std::setfill('0') << tVal->tm_sec << "." << std::setw(3) << std::setfill('0') << (unixMs%1000) << "Z" << std::endl;
|
||||
}
|
||||
s << "#EXTINF:" << (((double)duration) / 1000) << ",\r\nchunk_" << start << ".m4s" << std::endl;
|
||||
}
|
||||
|
||||
|
@ -537,7 +547,6 @@ namespace Mist{
|
|||
|
||||
result << "#EXTM3U\r\n"
|
||||
"#EXT-X-VERSION:7\r\n"
|
||||
"#EXT-X-DISCONTINUITY\r\n"
|
||||
"#EXT-X-TARGETDURATION:"
|
||||
<< targetDuration << "\r\n";
|
||||
if (M.getLive()){result << "#EXT-X-MEDIA-SEQUENCE:" << firstFragment << "\r\n";}
|
||||
|
|
|
@ -63,7 +63,7 @@ namespace Mist{
|
|||
config = cfg;
|
||||
}
|
||||
|
||||
std::string OutDTSC::getStatsName(){return (pushing ? "INPUT" : "OUTPUT");}
|
||||
std::string OutDTSC::getStatsName(){return (pushing ? "INPUT:DTSC" : "OUTPUT:DTSC");}
|
||||
|
||||
/// Seeks to the first sync'ed keyframe of the main track.
|
||||
/// Aborts if there is no main track or it has no keyframes.
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
namespace Mist{
|
||||
bool OutHLS::isReadyForPlay(){
|
||||
if (!isInitialized){initialize();}
|
||||
meta.refresh();
|
||||
if (!M.getValidTracks().size()){return false;}
|
||||
uint32_t mainTrack = M.mainTrack();
|
||||
if (mainTrack == INVALID_TRACK_ID){return false;}
|
||||
|
@ -244,7 +246,11 @@ namespace Mist{
|
|||
bool isTS = (HTTP::URL(H.url).getExt().substr(0, 3) != "m3u");
|
||||
H.Clean();
|
||||
H.setCORSHeaders();
|
||||
H.SetHeader("Content-Type", "application/octet-stream");
|
||||
if (isTS){
|
||||
H.SetHeader("Content-Type", "video/mp2t");
|
||||
}else{
|
||||
H.SetHeader("Content-Type", "application/vnd.apple.mpegurl");
|
||||
}
|
||||
if (isTS && !hasSessionIDs()){
|
||||
H.SetHeader("Cache-Control", "public, max-age=600, immutable");
|
||||
H.SetHeader("Pragma", "");
|
||||
|
@ -343,6 +349,7 @@ namespace Mist{
|
|||
std::string request = H.url.substr(H.url.find("/", 5) + 1);
|
||||
H.Clean();
|
||||
H.setCORSHeaders();
|
||||
H.SetHeader("Content-Type", "application/vnd.apple.mpegurl");
|
||||
if (!M.getValidTracks().size()){
|
||||
H.SendResponse("404", "Not online or found", myConn);
|
||||
H.Clean();
|
||||
|
@ -378,16 +385,14 @@ namespace Mist{
|
|||
wantRequest = true;
|
||||
parseData = false;
|
||||
|
||||
// Ensure alignment of contCounters for selected tracks, to prevent discontinuities.
|
||||
for (std::map<size_t, Comms::Users>::iterator it = userSelect.begin(); it != userSelect.end(); it++){
|
||||
uint32_t pkgPid = 255 + it->first;
|
||||
uint16_t &contPkg = contCounters[pkgPid];
|
||||
if (contPkg % 16 != 0){
|
||||
// Ensure alignment of contCounters, to prevent discontinuities.
|
||||
for (std::map<size_t, uint16_t>::iterator it = contCounters.begin(); it != contCounters.end(); it++){
|
||||
if (it->second % 16 != 0){
|
||||
packData.clear();
|
||||
packData.setPID(pkgPid);
|
||||
packData.setPID(it->first);
|
||||
packData.addStuffing();
|
||||
while (contPkg % 16 != 0){
|
||||
packData.setContinuityCounter(++contPkg);
|
||||
while (it->second % 16 != 0){
|
||||
packData.setContinuityCounter(++it->second);
|
||||
sendTS(packData.checkAndGetBuffer());
|
||||
}
|
||||
packData.clear();
|
||||
|
|
|
@ -111,6 +111,7 @@ namespace Mist{
|
|||
capa["provides"] = "HTTP";
|
||||
capa["protocol"] = "http://";
|
||||
capa["url_rel"] = "/$.html";
|
||||
capa["codecs"][0u][0u].append("+*");
|
||||
capa["url_match"].append("/crossdomain.xml");
|
||||
capa["url_match"].append("/clientaccesspolicy.xml");
|
||||
capa["url_match"].append("/$.html");
|
||||
|
@ -1028,7 +1029,7 @@ namespace Mist{
|
|||
snprintf(pageName, NAME_BUFFER_SIZE, SHM_STREAM_STATE, streamName.c_str());
|
||||
IPC::sharedPage streamStatus(pageName, 1, false, false);
|
||||
uint8_t prevState, newState, pingCounter = 0;
|
||||
uint64_t prevTracks;
|
||||
std::set<size_t> prevTracks;
|
||||
prevState = newState = STRMSTAT_INVALID;
|
||||
while (keepGoing()){
|
||||
if (!streamStatus || !streamStatus.exists()){streamStatus.init(pageName, 1, false, false);}
|
||||
|
@ -1038,10 +1039,10 @@ namespace Mist{
|
|||
newState = streamStatus.mapped[0];
|
||||
}
|
||||
|
||||
if (newState != prevState || (newState == STRMSTAT_READY && M.getValidTracks().size() != prevTracks)){
|
||||
if (newState != prevState || (newState == STRMSTAT_READY && M.getValidTracks() != prevTracks)){
|
||||
if (newState == STRMSTAT_READY){
|
||||
reconnect();
|
||||
prevTracks = M.getValidTracks().size();
|
||||
prevTracks = M.getValidTracks();
|
||||
}else{
|
||||
disconnect();
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@ namespace Mist{
|
|||
char error_buf[200];
|
||||
mbedtls_strerror(ret, error_buf, 200);
|
||||
MEDIUM_MSG("Could not handshake, SSL error: %s (%d)", error_buf, ret);
|
||||
Util::logExitReason("Could not handshake, SSL error: %s (%d)", error_buf, ret);
|
||||
C.close();
|
||||
return;
|
||||
}else{
|
||||
|
@ -110,6 +111,7 @@ namespace Mist{
|
|||
int fd[2];
|
||||
if (socketpair(PF_LOCAL, SOCK_STREAM, 0, fd) != 0){
|
||||
FAIL_MSG("Could not open anonymous socket for SSL<->HTTP connection!");
|
||||
Util::logExitReason("Could not open anonymous socket for SSL<->HTTP connection!");
|
||||
return 1;
|
||||
}
|
||||
std::deque<std::string> args;
|
||||
|
@ -135,6 +137,7 @@ namespace Mist{
|
|||
close(fd[1]);
|
||||
if (http_proc < 2){
|
||||
FAIL_MSG("Could not spawn MistOutHTTP process for SSL connection!");
|
||||
Util::logExitReason("Could not spawn MistOutHTTP process for SSL connection!");
|
||||
return 1;
|
||||
}
|
||||
Socket::Connection http(fd[0]);
|
||||
|
@ -150,6 +153,7 @@ namespace Mist{
|
|||
if (ret != MBEDTLS_ERR_SSL_WANT_READ && ret != MBEDTLS_ERR_SSL_WANT_WRITE){
|
||||
if (ret <= 0){
|
||||
HIGH_MSG("SSL disconnect!");
|
||||
Util::logExitReason("SSL client disconnected");
|
||||
break;
|
||||
}
|
||||
// we received ret bytes of data to pass on. Do so.
|
||||
|
@ -168,6 +172,7 @@ namespace Mist{
|
|||
ret = mbedtls_ssl_write(&ssl, (const unsigned char *)http_buf.get().data() + done, toSend - done);
|
||||
if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_SSL_CLIENT_RECONNECT){
|
||||
HIGH_MSG("SSL disconnect!");
|
||||
Util::logExitReason("SSL client disconnected");
|
||||
http.close();
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -1229,7 +1229,7 @@ namespace Mist{
|
|||
static std::map<size_t, AMF::Object> pushMeta;
|
||||
static std::map<size_t, uint64_t> lastTagTime;
|
||||
static std::map<size_t, size_t> reTrackToID;
|
||||
if (!isInitialized){
|
||||
if (!isInitialized || !meta){
|
||||
MEDIUM_MSG("Received useless media data");
|
||||
onFinish();
|
||||
break;
|
||||
|
|
|
@ -91,6 +91,8 @@ namespace Mist{
|
|||
capa["codecs"][0u][0u].append("H264");
|
||||
capa["codecs"][0u][0u].append("HEVC");
|
||||
capa["codecs"][0u][0u].append("MPEG2");
|
||||
capa["codecs"][0u][0u].append("VP8");
|
||||
capa["codecs"][0u][0u].append("VP9");
|
||||
capa["codecs"][0u][1u].append("AAC");
|
||||
capa["codecs"][0u][1u].append("MP3");
|
||||
capa["codecs"][0u][1u].append("AC3");
|
||||
|
|
|
@ -189,7 +189,7 @@ namespace Mist{
|
|||
|
||||
std::string OutTS::getStatsName(){
|
||||
if (!parseData){
|
||||
return "INPUT";
|
||||
return "INPUT:" + capa["name"].asStringRef();
|
||||
}else{
|
||||
return Output::getStatsName();
|
||||
}
|
||||
|
|
|
@ -65,17 +65,12 @@ namespace Mist{
|
|||
std::string type = M.getType(thisIdx);
|
||||
std::string codec = M.getCodec(thisIdx);
|
||||
bool video = (type == "video");
|
||||
|
||||
size_t pkgPid = M.getID(thisIdx);
|
||||
if (pkgPid < 255){pkgPid += 255;}
|
||||
|
||||
size_t pkgPid = TS::getUniqTrackID(M, thisIdx);
|
||||
bool &firstPack = first[thisIdx];
|
||||
uint16_t &contPkg = contCounters[pkgPid];
|
||||
|
||||
uint64_t packTime = thisPacket.getTime();
|
||||
bool keyframe = thisPacket.getInt("keyframe");
|
||||
firstPack = true;
|
||||
|
||||
char *dataPointer = 0;
|
||||
size_t dataLen = 0;
|
||||
thisPacket.getString("data", dataPointer, dataLen); // data
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include <mist/procs.h>
|
||||
#include <mist/sdp.h>
|
||||
#include <mist/timing.h>
|
||||
#include <mist/url.h>
|
||||
#include <netdb.h> // ifaddr, listing ip addresses.
|
||||
|
||||
namespace Mist{
|
||||
|
@ -31,10 +32,12 @@ namespace Mist{
|
|||
/* ------------------------------------------------ */
|
||||
|
||||
OutWebRTC::OutWebRTC(Socket::Connection &myConn) : HTTPOutput(myConn){
|
||||
lastPackMs = 0;
|
||||
vidTrack = INVALID_TRACK_ID;
|
||||
prevVidTrack = INVALID_TRACK_ID;
|
||||
audTrack = INVALID_TRACK_ID;
|
||||
stayLive = true;
|
||||
target_rate = 0.0;
|
||||
firstKey = true;
|
||||
repeatInit = true;
|
||||
|
||||
|
@ -95,6 +98,7 @@ namespace Mist{
|
|||
capa["url_match"] = "/webrtc/$";
|
||||
capa["codecs"][0u][0u].append("H264");
|
||||
capa["codecs"][0u][0u].append("VP8");
|
||||
capa["codecs"][0u][0u].append("VP9");
|
||||
capa["codecs"][0u][1u].append("opus");
|
||||
capa["codecs"][0u][1u].append("ALAW");
|
||||
capa["codecs"][0u][1u].append("ULAW");
|
||||
|
@ -107,7 +111,7 @@ namespace Mist{
|
|||
capa["optional"]["preferredvideocodec"]["help"] =
|
||||
"Comma separated list of video codecs you want to support in preferred order. e.g. "
|
||||
"H264,VP8";
|
||||
capa["optional"]["preferredvideocodec"]["default"] = "H264,VP8";
|
||||
capa["optional"]["preferredvideocodec"]["default"] = "H264,VP9,VP8";
|
||||
capa["optional"]["preferredvideocodec"]["type"] = "string";
|
||||
capa["optional"]["preferredvideocodec"]["option"] = "--webrtc-video-codecs";
|
||||
capa["optional"]["preferredvideocodec"]["short"] = "V";
|
||||
|
@ -129,14 +133,26 @@ namespace Mist{
|
|||
capa["optional"]["bindhost"]["option"] = "--bindhost";
|
||||
capa["optional"]["bindhost"]["short"] = "B";
|
||||
|
||||
capa["optional"]["mergesessions"]["name"] = "Merge sessions";
|
||||
capa["optional"]["mergesessions"]["name"] = "merge sessions";
|
||||
capa["optional"]["mergesessions"]["help"] =
|
||||
"If enabled, merges together all views from a single user into a single combined session. "
|
||||
"If disabled, each view (reconnection of the signalling websocket) is a separate session.";
|
||||
"if enabled, merges together all views from a single user into a single combined session. "
|
||||
"if disabled, each view (reconnection of the signalling websocket) is a separate session.";
|
||||
capa["optional"]["mergesessions"]["option"] = "--mergesessions";
|
||||
capa["optional"]["mergesessions"]["short"] = "m";
|
||||
capa["optional"]["mergesessions"]["default"] = 0;
|
||||
|
||||
capa["optional"]["nackdisable"]["name"] = "Disallow NACKs for viewers";
|
||||
capa["optional"]["nackdisable"]["help"] = "Disallows viewers to send NACKs for lost packets";
|
||||
capa["optional"]["nackdisable"]["option"] = "--nackdisable";
|
||||
capa["optional"]["nackdisable"]["short"] = "n";
|
||||
capa["optional"]["nackdisable"]["default"] = 0;
|
||||
|
||||
capa["optional"]["jitterlog"]["name"] = "Write jitter log";
|
||||
capa["optional"]["jitterlog"]["help"] = "Writes log of frame transmit jitter to /tmp/ for each outgoing connection";
|
||||
capa["optional"]["jitterlog"]["option"] = "--jitterlog";
|
||||
capa["optional"]["jitterlog"]["short"] = "J";
|
||||
capa["optional"]["jitterlog"]["default"] = 0;
|
||||
|
||||
config->addOptionsFromCapabilities(capa);
|
||||
}
|
||||
|
||||
|
@ -280,13 +296,61 @@ namespace Mist{
|
|||
parseData = true;
|
||||
selectDefaultTracks();
|
||||
}
|
||||
stayLive = (endTime() < seek_time + 5000);
|
||||
stayLive = (target_rate == 0.0) && (endTime() < seek_time + 5000);
|
||||
if (command["seek_time"].asStringRef() == "live"){stayLive = true;}
|
||||
if (stayLive){seek_time = endTime();}
|
||||
seek(seek_time, true);
|
||||
JSON::Value commandResult;
|
||||
commandResult["type"] = "on_seek";
|
||||
commandResult["result"] = true;
|
||||
if (M.getLive()){commandResult["live_point"] = stayLive;}
|
||||
if (target_rate == 0.0){
|
||||
commandResult["play_rate_curr"] = "auto";
|
||||
}else{
|
||||
commandResult["play_rate_curr"] = target_rate;
|
||||
}
|
||||
webSock->sendFrame(commandResult.toString());
|
||||
onIdle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command["type"] == "set_speed"){
|
||||
if (!command.isMember("play_rate")){
|
||||
sendSignalingError("on_speed", "Received a playback speed setting request but no `play_rate` property.");
|
||||
return;
|
||||
}
|
||||
double set_rate = command["play_rate"].asDouble();
|
||||
if (!parseData){
|
||||
parseData = true;
|
||||
selectDefaultTracks();
|
||||
}
|
||||
JSON::Value commandResult;
|
||||
commandResult["type"] = "on_speed";
|
||||
if (target_rate == 0.0){
|
||||
commandResult["play_rate_prev"] = "auto";
|
||||
}else{
|
||||
commandResult["play_rate_prev"] = target_rate;
|
||||
}
|
||||
if (set_rate == 0.0){
|
||||
commandResult["play_rate_curr"] = "auto";
|
||||
}else{
|
||||
commandResult["play_rate_curr"] = set_rate;
|
||||
}
|
||||
if (target_rate != set_rate){
|
||||
target_rate = set_rate;
|
||||
if (target_rate == 0.0){
|
||||
realTime = 1000;//set playback speed to default
|
||||
firstTime = Util::bootMS() - currentTime();
|
||||
maxSkipAhead = 0;//enabled automatic rate control
|
||||
}else{
|
||||
stayLive = false;
|
||||
//Set new realTime speed
|
||||
realTime = 1000 / target_rate;
|
||||
firstTime = Util::bootMS() - (currentTime() / target_rate);
|
||||
maxSkipAhead = 1;//disable automatic rate control
|
||||
}
|
||||
}
|
||||
if (M.getLive()){commandResult["live_point"] = stayLive;}
|
||||
webSock->sendFrame(commandResult.toString());
|
||||
onIdle();
|
||||
return;
|
||||
|
@ -337,6 +401,15 @@ namespace Mist{
|
|||
sendSignalingError(command["type"].asString(), "Unhandled command type: " + command["type"].asString());
|
||||
}
|
||||
|
||||
bool OutWebRTC::dropPushTrack(uint32_t trackId, const std::string & dropReason){
|
||||
JSON::Value commandResult;
|
||||
commandResult["type"] = "on_track_drop";
|
||||
commandResult["track"] = trackId;
|
||||
commandResult["mediatype"] = M.getType(trackId);
|
||||
webSock->sendFrame(commandResult.toString());
|
||||
return Output::dropPushTrack(trackId, dropReason);
|
||||
}
|
||||
|
||||
void OutWebRTC::sendSignalingError(const std::string &commandType, const std::string &errorMessage){
|
||||
JSON::Value commandResult;
|
||||
commandResult["type"] = "on_error";
|
||||
|
@ -357,6 +430,12 @@ namespace Mist{
|
|||
initialize();
|
||||
selectDefaultTracks();
|
||||
|
||||
if (config && config->hasOption("jitterlog") && config->getBool("jitterlog")){
|
||||
std::string fileName = "/tmp/jitter_"+JSON::Value(getpid()).asString();
|
||||
jitterLog.open(fileName.c_str());
|
||||
lastPackMs = Util::bootMS();
|
||||
}
|
||||
|
||||
if (0 == udpPort){bindUDPSocketOnLocalCandidateAddress(0);}
|
||||
|
||||
std::string videoCodec;
|
||||
|
@ -389,8 +468,10 @@ namespace Mist{
|
|||
return false;
|
||||
}
|
||||
videoTrack.rtpPacketizer = RTP::Packet(videoTrack.payloadType, rand(), 0, videoTrack.SSRC, 0);
|
||||
// Enabled NACKs
|
||||
sdpAnswer.videoLossPrevention = SDP_LOSS_PREVENTION_NACK;
|
||||
if (!config || !config->hasOption("nackdisable") || !config->getBool("nackdisable")){
|
||||
// Enable NACKs
|
||||
sdpAnswer.videoLossPrevention = SDP_LOSS_PREVENTION_NACK;
|
||||
}
|
||||
videoTrack.sorter.tmpVideoLossPrevention = sdpAnswer.videoLossPrevention;
|
||||
}
|
||||
}
|
||||
|
@ -500,7 +581,7 @@ namespace Mist{
|
|||
|
||||
capa["codecs"].null();
|
||||
|
||||
const char *videoCodecPreference[] ={"H264", "VP8", NULL};
|
||||
const char *videoCodecPreference[] ={"H264", "VP9", "VP8", NULL};
|
||||
const char **videoCodec = videoCodecPreference;
|
||||
SDP::Media *videoMediaOffer = sdpSession.getMediaForType("video");
|
||||
if (videoMediaOffer){
|
||||
|
@ -535,12 +616,12 @@ namespace Mist{
|
|||
|
||||
if (0 == udpPort){bindUDPSocketOnLocalCandidateAddress(0);}
|
||||
|
||||
std::string prefVideoCodec = "VP8,H264";
|
||||
std::string prefVideoCodec = "VP9,VP8,H264";
|
||||
if (config && config->hasOption("preferredvideocodec")){
|
||||
prefVideoCodec = config->getString("preferredvideocodec");
|
||||
if (prefVideoCodec.empty()){
|
||||
WARN_MSG("No preferred video codec value set; resetting to default.");
|
||||
prefVideoCodec = "VP8,H264";
|
||||
prefVideoCodec = "VP9,VP8,H264";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -579,10 +660,11 @@ namespace Mist{
|
|||
|
||||
SDP::MediaFormat *fmtRED = sdpSession.getMediaFormatByEncodingName("video", "RED");
|
||||
SDP::MediaFormat *fmtULPFEC = sdpSession.getMediaFormatByEncodingName("video", "ULPFEC");
|
||||
if (fmtRED && fmtULPFEC){
|
||||
if (fmtRED || fmtULPFEC){
|
||||
videoTrack.ULPFECPayloadType = fmtULPFEC->payloadType;
|
||||
videoTrack.REDPayloadType = fmtRED->payloadType;
|
||||
payloadTypeToWebRTCTrack[fmtRED->payloadType] = videoTrack.payloadType;
|
||||
payloadTypeToWebRTCTrack[fmtULPFEC->payloadType] = videoTrack.payloadType;
|
||||
}
|
||||
sdpAnswer.videoLossPrevention = SDP_LOSS_PREVENTION_NACK;
|
||||
videoTrack.sorter.tmpVideoLossPrevention = sdpAnswer.videoLossPrevention;
|
||||
|
@ -594,7 +676,7 @@ namespace Mist{
|
|||
|
||||
videoTrack.rtpToDTSC.setProperties(meta, vIdx);
|
||||
videoTrack.rtpToDTSC.setCallbacks(onDTSCConverterHasPacketCallback, onDTSCConverterHasInitDataCallback);
|
||||
videoTrack.sorter.setCallback(vIdx, onRTPSorterHasPacketCallback);
|
||||
videoTrack.sorter.setCallback(M.getID(vIdx), onRTPSorterHasPacketCallback);
|
||||
|
||||
userSelect[vIdx].reload(streamName, vIdx, COMM_STATUS_SOURCE);
|
||||
INFO_MSG("Video push received on track %zu", vIdx);
|
||||
|
@ -616,7 +698,7 @@ namespace Mist{
|
|||
|
||||
audioTrack.rtpToDTSC.setProperties(meta, aIdx);
|
||||
audioTrack.rtpToDTSC.setCallbacks(onDTSCConverterHasPacketCallback, onDTSCConverterHasInitDataCallback);
|
||||
audioTrack.sorter.setCallback(aIdx, onRTPSorterHasPacketCallback);
|
||||
audioTrack.sorter.setCallback(M.getID(aIdx), onRTPSorterHasPacketCallback);
|
||||
|
||||
userSelect[aIdx].reload(streamName, aIdx, COMM_STATUS_SOURCE);
|
||||
INFO_MSG("Audio push received on track %zu", aIdx);
|
||||
|
@ -639,13 +721,47 @@ namespace Mist{
|
|||
return false;
|
||||
}
|
||||
|
||||
udpPort =
|
||||
udp.bind(port, (config && config->hasOption("bindhost") && config->getString("bindhost").size())
|
||||
? config->getString("bindhost")
|
||||
: myConn.getBoundAddress());
|
||||
Util::Procs::socketList.insert(udp.getSock());
|
||||
sdpAnswer.setCandidate(externalAddr, udpPort);
|
||||
std::string bindAddr;
|
||||
//If a bind host has been put in as override, use it
|
||||
if (config && config->hasOption("bindhost") && config->getString("bindhost").size()){
|
||||
bindAddr = config->getString("bindhost");
|
||||
udpPort = udp.bind(port, bindAddr);
|
||||
if (!udpPort){
|
||||
WARN_MSG("UDP bind address not valid - ignoring setting and using best guess instead");
|
||||
bindAddr.clear();
|
||||
}else{
|
||||
INFO_MSG("Bound to pre-configured UDP bind address");
|
||||
}
|
||||
}
|
||||
//use the best IPv4 guess we have
|
||||
if (!bindAddr.size()){
|
||||
bindAddr = Socket::resolveHostToBestExternalAddrGuess(externalAddr, AF_INET, myConn.getBoundAddress());
|
||||
if (!bindAddr.size()){
|
||||
WARN_MSG("UDP bind to best guess failed - using same address as incoming connection as a last resort");
|
||||
bindAddr.clear();
|
||||
}else{
|
||||
udpPort = udp.bind(port, bindAddr);
|
||||
if (!udpPort){
|
||||
WARN_MSG("UDP bind to best guess failed - using same address as incoming connection as a last resort");
|
||||
bindAddr.clear();
|
||||
}else{
|
||||
INFO_MSG("Bound to public UDP bind address derived from hostname");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!bindAddr.size()){
|
||||
bindAddr = myConn.getBoundAddress();
|
||||
udpPort = udp.bind(port, bindAddr);
|
||||
if (!udpPort){
|
||||
FAIL_MSG("UDP bind to connected address failed - we're out of options here, I'm afraid...");
|
||||
bindAddr.clear();
|
||||
}else{
|
||||
INFO_MSG("Bound to same UDP address as TCP address - this is potentially wrong, but used as a last resort");
|
||||
}
|
||||
}
|
||||
|
||||
Util::Procs::socketList.insert(udp.getSock());
|
||||
sdpAnswer.setCandidate(bindAddr, udpPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -813,8 +929,8 @@ namespace Mist{
|
|||
|
||||
if (idx == INVALID_TRACK_ID || !webrtcTracks.count(idx)){
|
||||
FAIL_MSG("Received an RTP packet for a track that we didn't prepare for. PayloadType is "
|
||||
"%" PRIu32,
|
||||
rtp_pkt.getPayloadType());
|
||||
"%" PRIu32 ", idx %zu",
|
||||
rtp_pkt.getPayloadType(), idx);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -830,6 +946,8 @@ namespace Mist{
|
|||
FAIL_MSG("Failed to unprotect a RTP packet.");
|
||||
return;
|
||||
}
|
||||
RTP::Packet unprotPack(udp.data, len);
|
||||
DONTEVEN_MSG("%s", unprotPack.toString().c_str());
|
||||
|
||||
// Here follows a very rudimentary algo for requesting lost
|
||||
// packets; I guess after some experimentation a better
|
||||
|
@ -843,11 +961,11 @@ namespace Mist{
|
|||
|
||||
rtcTrack.prevReceivedSequenceNumber = currSeqNum;
|
||||
|
||||
if (rtp_pkt.getPayloadType() == rtcTrack.REDPayloadType){
|
||||
if (rtp_pkt.getPayloadType() == rtcTrack.REDPayloadType || rtp_pkt.getPayloadType() == rtcTrack.ULPFECPayloadType){
|
||||
rtcTrack.sorter.addREDPacket(udp.data, len, rtcTrack.payloadType, rtcTrack.REDPayloadType,
|
||||
rtcTrack.ULPFECPayloadType);
|
||||
}else{
|
||||
rtcTrack.sorter.addPacket(RTP::Packet(udp.data, len));
|
||||
rtcTrack.sorter.addPacket(unprotPack);
|
||||
}
|
||||
}else if ((pt >= 64) && (pt < 96)){
|
||||
|
||||
|
@ -909,7 +1027,7 @@ namespace Mist{
|
|||
void OutWebRTC::onDTSCConverterHasPacket(const DTSC::Packet &pkt){
|
||||
|
||||
// extract meta data (init data, width/height, etc);
|
||||
size_t idx = pkt.getTrackId();
|
||||
size_t idx = M.trackIDToIndex(pkt.getTrackId(), getpid());
|
||||
std::string codec = M.getCodec(idx);
|
||||
if (codec == "H264"){
|
||||
if (M.getInit(idx).empty()){
|
||||
|
@ -919,9 +1037,10 @@ namespace Mist{
|
|||
}
|
||||
|
||||
if (codec == "VP8" && pkt.getFlag("keyframe")){extractFrameSizeFromVP8KeyFrame(pkt);}
|
||||
if (codec == "VP9" && pkt.getFlag("keyframe")){extractFrameSizeFromVP8KeyFrame(pkt);}
|
||||
|
||||
// create rtcp packet (set bitrate and request keyframe).
|
||||
if (codec == "H264" || codec == "VP8"){
|
||||
if (codec == "H264" || codec == "VP8" || codec == "VP9"){
|
||||
uint64_t now = Util::bootMS();
|
||||
|
||||
if (now >= rtcpTimeoutInMillis){
|
||||
|
@ -942,13 +1061,15 @@ namespace Mist{
|
|||
INFO_MSG("Validated track %zu in meta", idx);
|
||||
meta.validateTrack(idx);
|
||||
}
|
||||
DONTEVEN_MSG("DTSC: %s", pkt.toSummary().c_str());
|
||||
bufferLivePacket(pkt);
|
||||
}
|
||||
|
||||
void OutWebRTC::onDTSCConverterHasInitData(size_t idx, const std::string &initData){
|
||||
void OutWebRTC::onDTSCConverterHasInitData(size_t trackId, const std::string &initData){
|
||||
size_t idx = M.trackIDToIndex(trackId, getpid());
|
||||
if (idx == INVALID_TRACK_ID || !webrtcTracks.count(idx)){
|
||||
ERROR_MSG(
|
||||
"Recieved init data for a track that we don't manager. TrackID %zu /PayloadType: %zu",
|
||||
"Recieved init data for a track that we don't manage. TrackID %zu /PayloadType: %zu",
|
||||
idx, M.getID(idx));
|
||||
return;
|
||||
}
|
||||
|
@ -972,9 +1093,10 @@ namespace Mist{
|
|||
meta.setInit(idx, avccbox.payload(), avccbox.payloadSize());
|
||||
}
|
||||
|
||||
void OutWebRTC::onRTPSorterHasPacket(size_t idx, const RTP::Packet &pkt){
|
||||
void OutWebRTC::onRTPSorterHasPacket(size_t trackId, const RTP::Packet &pkt){
|
||||
size_t idx = M.trackIDToIndex(trackId, getpid());
|
||||
if (idx == INVALID_TRACK_ID || !webrtcTracks.count(idx)){
|
||||
ERROR_MSG("Received a sorted RTP packet for track %zu but we don't manage this track.", idx);
|
||||
ERROR_MSG("Received a sorted RTP packet for payload %zu (idx %zu) but we don't manage this track.", trackId, idx);
|
||||
return;
|
||||
}
|
||||
webrtcTracks[idx].rtpToDTSC.addRTP(pkt);
|
||||
|
@ -989,7 +1111,7 @@ namespace Mist{
|
|||
|
||||
int protectedSize = nbytes;
|
||||
if (srtpWriter.protectRtp((uint8_t *)(void *)rtpOutBuffer, &protectedSize) != 0){
|
||||
ERROR_MSG("Failed to protect the RTCP message.");
|
||||
ERROR_MSG("Failed to protect the RTP message.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1076,6 +1198,14 @@ namespace Mist{
|
|||
// If we see this is audio or video, use the webrtc track we negotiated
|
||||
if (M.getType(tid) == "video" && webrtcTracks.count(vidTrack)){
|
||||
trackPointer = &webrtcTracks[vidTrack];
|
||||
|
||||
if (lastPackMs){
|
||||
uint64_t newMs = Util::bootMS();
|
||||
jitterLog << (newMs - lastPackMs) << std::endl;
|
||||
lastPackMs = newMs;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
if (M.getType(tid) == "audio" && webrtcTracks.count(audTrack)){
|
||||
trackPointer = &webrtcTracks[audTrack];
|
||||
|
@ -1093,19 +1223,17 @@ namespace Mist{
|
|||
WebRTCTrack &rtcTrack = *trackPointer;
|
||||
|
||||
uint64_t timestamp = thisPacket.getTime();
|
||||
rtcTrack.rtpPacketizer.setTimestamp(timestamp * SDP::getMultiplier(&M, thisIdx));
|
||||
uint64_t newTime = timestamp * SDP::getMultiplier(&M, thisIdx);
|
||||
rtcTrack.rtpPacketizer.setTimestamp(newTime);
|
||||
|
||||
bool isKeyFrame = thisPacket.getFlag("keyframe");
|
||||
didReceiveKeyFrame = isKeyFrame;
|
||||
if (M.getCodec(thisIdx) == "H264"){
|
||||
if (isKeyFrame && firstKey){
|
||||
char *data;
|
||||
size_t dataLen;
|
||||
thisPacket.getString("data", data, dataLen);
|
||||
size_t offset = 0;
|
||||
while (offset + 4 < dataLen){
|
||||
size_t nalLen = Bit::btohl(data + offset);
|
||||
uint8_t nalType = data[offset + 4] & 0x1F;
|
||||
size_t nalLen = Bit::btohl(dataPointer + offset);
|
||||
uint8_t nalType = dataPointer[offset + 4] & 0x1F;
|
||||
if (nalType == 7 || nalType == 8){// Init data already provided in-band, skip repeating
|
||||
// it.
|
||||
repeatInit = false;
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
#include <mist/stun.h>
|
||||
#include <mist/tinythread.h>
|
||||
#include <mist/websocket.h>
|
||||
#include <fstream>
|
||||
|
||||
#define NACK_BUFFER_SIZE 1024
|
||||
|
||||
|
@ -130,6 +131,7 @@ namespace Mist{
|
|||
virtual void sendNext();
|
||||
virtual void onWebsocketFrame();
|
||||
virtual void preWebsocketConnect();
|
||||
virtual bool dropPushTrack(uint32_t trackId, const std::string & dropReason);
|
||||
void onIdle();
|
||||
bool onFinish();
|
||||
bool doesWebsockets(){return true;}
|
||||
|
@ -142,6 +144,8 @@ namespace Mist{
|
|||
void onRTPPacketizerHasRTCPPacket(const char *data, uint32_t nbytes);
|
||||
|
||||
private:
|
||||
uint64_t lastPackMs;
|
||||
std::ofstream jitterLog;
|
||||
std::string externalAddr;
|
||||
void ackNACK(uint32_t SSRC, uint16_t seq);
|
||||
bool handleWebRTCInputOutput(); ///< Reads data from the UDP socket. Returns true when we read
|
||||
|
@ -198,6 +202,7 @@ namespace Mist{
|
|||
///< the signaling channel. Defaults to 6mbit.
|
||||
|
||||
size_t audTrack, vidTrack, prevVidTrack;
|
||||
double target_rate; ///< Target playback speed rate (1.0 = normal, 0 = auto)
|
||||
|
||||
bool didReceiveKeyFrame; /* TODO burst delay */
|
||||
int64_t packetOffset; ///< For timestamp rewrite with BMO
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue