mistserver/src/output/output_hls.cpp
2017-07-28 18:40:33 +02:00

515 lines
19 KiB
C++

#include "output_hls.h"
#include <mist/stream.h>
#include <unistd.h>
namespace Mist {
bool OutHLS::isReadyForPlay() {
if (myMeta.tracks.size()){
if (myMeta.mainTrack().fragments.size() > 4){
return true;
}
}
return false;
}
///\brief Builds an index file for HTTP Live streaming.
///\return The index file for HTTP Live Streaming.
std::string OutHLS::liveIndex() {
std::stringstream result;
result << "#EXTM3U\r\n";
int audioId = -1;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
if (it->second.codec == "AAC" || it->second.codec == "MP3" || it->second.codec == "AC3" || it->second.codec == "MP2") {
audioId = it->first;
break;
}
}
unsigned int vidTracks = 0;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
if (it->second.codec == "H264" || it->second.codec == "HEVC" || it->second.codec == "MPEG2") {
vidTracks++;
int bWidth = it->second.bps;
if (bWidth < 5) {
bWidth = 5;
}
if (audioId != -1) {
bWidth += myMeta.tracks[audioId].bps;
}
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (bWidth * 8) << "\r\n";
result << it->first;
if (audioId != -1) {
result << "_" << audioId;
}
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
}
}
if (!vidTracks && audioId) {
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (myMeta.tracks[audioId].bps * 8) << "\r\n";
result << audioId << "/index.m3u8\r\n";
}
DEBUG_MSG(DLVL_HIGH, "Sending this index: %s", result.str().c_str());
return result.str();
}
std::string OutHLS::pushLiveIndex(){
std::stringstream result;
result << "#EXTM3U\r\n";
std::set<unsigned int> audioTracks;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
if (it->second.codec == "AAC" || it->second.codec == "MP3" || it->second.codec == "AC3" || it->second.codec == "MP2") {
audioTracks.insert(it->first);
}
}
if (!audioTracks.size()){
audioTracks.insert(-1);
}
unsigned int vidTracks = 0;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
if (it->second.codec == "H264" || it->second.codec == "HEVC" || it->second.codec == "MPEG2") {
for (std::set<unsigned int>::iterator audIt = audioTracks.begin(); audIt != audioTracks.end(); audIt++){
vidTracks++;
int bWidth = it->second.bps;
if (bWidth < 5) {
bWidth = 5;
}
if (*audIt != -1) {
bWidth += myMeta.tracks[*audIt].bps;
}
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (bWidth * 8) << "\r\n";
result << it->first;
if (*audIt != -1) {
result << "_" << *audIt;
}
result << "/index.m3u8\r\n";
}
}
}
if (!vidTracks && audioTracks.size()) {
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (myMeta.tracks[*audioTracks.begin()].bps * 8) << "\r\n";
result << *audioTracks.begin() << "/index.m3u8\r\n";
}
return result.str();
}
std::string OutHLS::pushLiveIndex(int tid, unsigned long bTime, unsigned long eTime){
updateMeta();
std::stringstream result;
//parse single track
result << "#EXTM3U\r\n#EXT-X-TARGETDURATION:" << (myMeta.tracks[tid].biggestFragment() / 1000) + 1 << "\r\n";
std::deque<std::string> lines;
unsigned int skippedLines = 0;
for (std::deque<DTSC::Fragment>::iterator it = myMeta.tracks[tid].fragments.begin(); it != myMeta.tracks[tid].fragments.end(); it++) {
long long int starttime = myMeta.tracks[tid].getKey(it->getNumber()).getTime();
long long duration = it->getDuration();
if (duration <= 0) {
duration = myMeta.tracks[tid].lastms - starttime;
}
if (starttime < bTime){
skippedLines++;
}
if (starttime >= bTime && (starttime + duration) <= eTime){
char lineBuf[400];
snprintf(lineBuf, 400, "#EXTINF:%lld, no desc\r\n%lld_%lld.ts\r\n", ((duration + 500) / 1000), starttime, starttime + duration);
lines.push_back(lineBuf);
}
}
result << "#EXT-X-MEDIA-SEQUENCE:" << myMeta.tracks[tid].missedFrags + skippedLines << "\r\n";
while (lines.size()) {
result << lines.front();
lines.pop_front();
}
if (!myMeta.live && eTime >= myMeta.tracks[tid].lastms) {
result << "#EXT-X-ENDLIST\r\n";
}
return result.str();
}
std::string OutHLS::liveIndex(int tid, std::string & sessId) {
updateMeta();
std::stringstream result;
//parse single track
uint32_t target_dur = (myMeta.tracks[tid].biggestFragment() / 1000) + 1;
result << "#EXTM3U\r\n#EXT-X-VERSION:3\r\n#EXT-X-TARGETDURATION:" << target_dur << "\r\n";
std::deque<std::string> lines;
std::deque<uint16_t> durs;
uint32_t total_dur = 0;
for (std::deque<DTSC::Fragment>::iterator it = myMeta.tracks[tid].fragments.begin(); it != myMeta.tracks[tid].fragments.end(); it++) {
long long int starttime = myMeta.tracks[tid].getKey(it->getNumber()).getTime();
long long duration = it->getDuration();
if (duration <= 0) {
duration = myMeta.tracks[tid].lastms - starttime;
}
char lineBuf[400];
if (sessId.size()){
snprintf(lineBuf, 400, "#EXTINF:%f,\r\n%lld_%lld.ts?sessId=%s\r\n", (double)duration/1000, starttime, starttime + duration, sessId.c_str());
}else{
snprintf(lineBuf, 400, "#EXTINF:%f,\r\n%lld_%lld.ts\r\n", (double)duration/1000, starttime, starttime + duration);
}
durs.push_back(duration);
total_dur += duration;
lines.push_back(lineBuf);
}
unsigned int skippedLines = 0;
if (myMeta.live && lines.size()) {
//only print the last segment when VoD
lines.pop_back();
total_dur -= durs.back();
durs.pop_back();
//skip the first two segments when live, unless that brings us under 4 target durations
while ((total_dur-durs.front()) > (target_dur * 4000) && skippedLines < 2) {
lines.pop_front();
total_dur -= durs.front();
durs.pop_front();
++skippedLines;
}
/*LTS-START*/
//remove lines to reduce size towards listlimit setting - but keep at least 4X target duration available
if (config->getInteger("listlimit")) {
unsigned long listlimit = config->getInteger("listlimit");
while (lines.size() > listlimit && (total_dur-durs.front()) > (target_dur * 4000)) {
lines.pop_front();
total_dur -= durs.front();
durs.pop_front();
++skippedLines;
}
}
/*LTS-END*/
}
result << "#EXT-X-MEDIA-SEQUENCE:" << myMeta.tracks[tid].missedFrags + skippedLines << "\r\n";
while (lines.size()) {
result << lines.front();
lines.pop_front();
}
if (!myMeta.live || total_dur == 0) {
result << "#EXT-X-ENDLIST\r\n";
}
DEBUG_MSG(DLVL_HIGH, "Sending this index: %s", result.str().c_str());
return result.str();
} //liveIndex
std::string OutHLS::generatePushList() {
updateMeta();
std::set<unsigned int> videoTracks;
std::set<unsigned int> audioTracks;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
if (it->second.codec == "AAC" || it->second.codec == "MP3" || it->second.codec == "AC3") {
audioTracks.insert(it->first);
}
if (it->second.codec == "H264" || it->second.codec == "HEVC"){
videoTracks.insert(it->first);
}
}
JSON::Value result;
for (std::map<unsigned int, DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++) {
std::stringstream tid;
tid << it->second.trackID;
result["tracks"][tid.str()] = it->second.toJSON(true);
}
for(std::set<unsigned int>::iterator it = videoTracks.begin(); it != videoTracks.end(); it++){
for(std::set<unsigned int>::iterator it2 = audioTracks.begin(); it2 != audioTracks.end(); it2++){
JSON::Value quality;
std::stringstream identifier;
identifier << "/" << *it << "_" << *it2;
quality["index"] = "/push" + identifier.str() + "/index_\%llu_\%llu.m3u8";
quality["segment"] = identifier.str() + "/\%llu_\%llu.ts";
quality["video"] = *it;
quality["audio"] = *it2;
quality["id"] = identifier.str();
std::deque<DTSC::Fragment>::iterator it3 = myMeta.tracks[*it].fragments.begin();
for (int i = 0; i < 2; i++){
if (it3 != myMeta.tracks[*it].fragments.end()){
++it3;
}
}
for (; it3 != myMeta.tracks[*it].fragments.end(); it3++) {
if (myMeta.live && it3 == (myMeta.tracks[*it].fragments.end() - 1)){
//Skip the current last fragment if we are live
continue;
}
long long int starttime = myMeta.tracks[*it].getKey(it3->getNumber()).getTime();
std::stringstream line;
long long duration = it3->getDuration();
if (duration <= 0) {
duration = myMeta.tracks[*it].lastms - starttime;
}
std::stringstream segmenturl;
segmenturl << identifier.str() << "/" << starttime << "_" << duration + starttime << ".ts";
JSON::Value segment;
//segment["url"] = segmenturl.str();
segment["time"] = starttime;
segment["duration"] = duration;
segment["number"] = (unsigned int)it3->getNumber();
quality["segments"].append(segment);
}
result["qualities"].append(quality);
}
}
return result.toString();;
}
OutHLS::OutHLS(Socket::Connection & conn) : TSOutput(conn) {
uaDelay = 0;
realTime = 0;
until=0xFFFFFFFFFFFFFFFFull;
}
OutHLS::~OutHLS() {}
void OutHLS::init(Util::Config * cfg) {
HTTPOutput::init(cfg);
capa["name"] = "HLS";
capa["desc"] = "Enables HTTP protocol Apple-specific streaming (also known as HLS).";
capa["url_rel"] = "/hls/$/index.m3u8";
capa["url_prefix"] = "/hls/$/";
capa["url_pushlist"] = "/hls/$/push/list";
capa["codecs"][0u][0u].append("HEVC");
capa["codecs"][0u][0u].append("H264");
capa["codecs"][0u][0u].append("MPEG2");
capa["codecs"][0u][1u].append("AAC");
capa["codecs"][0u][1u].append("MP3");
capa["codecs"][0u][1u].append("AC3");
capa["codecs"][0u][1u].append("MP2");
capa["methods"][0u]["handler"] = "http";
capa["methods"][0u]["type"] = "html5/application/vnd.apple.mpegurl";
capa["methods"][0u]["priority"] = 9ll;
/*LTS-START*/
cfg->addOption("listlimit", JSON::fromString("{\"arg\":\"integer\",\"default\":0,\"short\":\"y\",\"long\":\"list-limit\",\"help\":\"Maximum number of parts in live playlists (0 = infinite).\"}"));
capa["optional"]["listlimit"]["name"] = "Live playlist limit";
capa["optional"]["listlimit"]["help"] = "Maximum number of parts in live playlists. (0 = infinite)";
capa["optional"]["listlimit"]["default"] = 0ll;
capa["optional"]["listlimit"]["type"] = "uint";
capa["optional"]["listlimit"]["option"] = "--list-limit";
/*LTS-END*/
}
void OutHLS::onHTTP() {
std::string method = H.method;
std::string sessId = H.GetVar("sessId");
if (H.url == "/crossdomain.xml") {
H.Clean();
H.SetHeader("Content-Type", "text/xml");
H.SetHeader("Server", "MistServer/" PACKAGE_VERSION);
H.setCORSHeaders();
if(method == "OPTIONS" || method == "HEAD"){
H.SendResponse("200", "OK", myConn);
H.Clean();
return;
}
H.SetBody("<?xml version=\"1.0\"?><!DOCTYPE cross-domain-policy SYSTEM \"http://www.adobe.com/xml/dtds/cross-domain-policy.dtd\"><cross-domain-policy><allow-access-from domain=\"*\" /><site-control permitted-cross-domain-policies=\"all\"/></cross-domain-policy>");
H.SendResponse("200", "OK", myConn);
H.Clean(); //clean for any possible next requests
return;
} //crossdomain.xml
if (H.method == "OPTIONS") {
H.Clean();
H.SetHeader("Content-Type", "application/octet-stream");
H.SetHeader("Cache-Control", "no-cache");
H.setCORSHeaders();
H.SetBody("");
H.SendResponse("200", "OK", myConn);
H.Clean();
return;
}
if (H.url.find("hls") == std::string::npos) {
myConn.close();
return;
}
appleCompat = (H.GetHeader("User-Agent").find("iPad") != std::string::npos) || (H.GetHeader("User-Agent").find("iPod") != std::string::npos) || (H.GetHeader("User-Agent").find("iPhone") != std::string::npos);
bool VLCworkaround = false;
if (H.GetHeader("User-Agent").substr(0, 3) == "VLC") {
std::string vlcver = H.GetHeader("User-Agent").substr(4);
if (vlcver[0] == '0' || vlcver[0] == '1' || (vlcver[0] == '2' && vlcver[2] < '2')) {
DEBUG_MSG(DLVL_INFO, "Enabling VLC version < 2.2.0 bug workaround.");
VLCworkaround = true;
}
}
initialize();
if (H.url.substr(5 + streamName.size(), 5) == "/push"){
std::string relPushUrl = H.url.substr(10 + streamName.size());
H.Clean();
if (relPushUrl == "/list"){
H.SetBody(generatePushList());
H.SendResponse("200", "OK", myConn);
H.Clean();
return;
}
if (relPushUrl.find(".m3u8") != std::string::npos) {
H.SetHeader("Content-Type", "audio/x-mpegurl");
} else {
H.SetHeader("Content-Type", "audio/mpegurl");
}
if (relPushUrl == "/index.m3u8"){
H.SetHeader("Cache-Control", "no-cache");
H.setCORSHeaders();
H.SetBody(pushLiveIndex());
H.SendResponse("200", "OK", myConn);
H.Clean(); //clean for any possible next requests
return;
}else {
unsigned int vTrack;
unsigned int aTrack;
unsigned long long bTime;
unsigned long long eTime;
if (sscanf(relPushUrl.c_str(), "/%u_%u/index_%llu_%llu.m3u", &vTrack, &aTrack, &bTime, &eTime) == 4) {
if (eTime < bTime){
eTime = bTime;
}
H.SetHeader("Cache-Control", "no-cache");
H.setCORSHeaders();
H.SetBody(pushLiveIndex(vTrack, bTime, eTime));
H.SendResponse("200", "OK", myConn);
H.Clean(); //clean for any possible next requests
return;
}
}
H.SetBody("The HLS URL wasn't understood - what did you want, exactly?\n");
myConn.SendNow(H.BuildResponse("404", "URL mismatch"));
H.Clean(); //clean for any possible next requests
return;
}else if (H.url.find(".m3u") == std::string::npos) {
std::string tmpStr = H.getUrl().substr(5 + streamName.size());
long long unsigned int from;
if (sscanf(tmpStr.c_str(), "/%u_%u/%llu_%llu.ts", &vidTrack, &audTrack, &from, &until) != 4) {
if (sscanf(tmpStr.c_str(), "/%u/%llu_%llu.ts", &vidTrack, &from, &until) != 3) {
DEBUG_MSG(DLVL_MEDIUM, "Could not parse URL: %s", H.getUrl().c_str());
H.Clean();
H.setCORSHeaders();
H.SetBody("The HLS URL wasn't understood - what did you want, exactly?\n");
myConn.SendNow(H.BuildResponse("404", "URL mismatch"));
H.Clean(); //clean for any possible next requests
return;
} else {
selectedTracks.clear();
selectedTracks.insert(vidTrack);
}
} else {
selectedTracks.clear();
selectedTracks.insert(vidTrack);
selectedTracks.insert(audTrack);
}
for (std::map<unsigned int,DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++){
if (it->second.codec == "ID3"){
selectedTracks.insert(it->first);
}
}
//Keep a reference to the main track
//This is called vidTrack, even for audio-only streams
DTSC::Track & Trk = myMeta.tracks[vidTrack];
if (myMeta.live) {
if (from < Trk.firstms){
H.Clean();
H.setCORSHeaders();
H.SetBody("The requested fragment is no longer kept in memory on the server and cannot be served.\n");
myConn.SendNow(H.BuildResponse("404", "Fragment out of range"));
H.Clean(); //clean for any possible next requests
WARN_MSG("Fragment @ %llu too old", from);
return;
}
}
H.SetHeader("Content-Type", "video/mp2t");
H.setCORSHeaders();
if(method == "OPTIONS" || method == "HEAD"){
H.SendResponse("200", "OK", myConn);
H.Clean();
return;
}
H.StartResponse(H, myConn, VLCworkaround);
//we assume whole fragments - but timestamps may be altered at will
uint32_t fragIndice = Trk.timeToFragnum(from);
contPAT = Trk.missedFrags + fragIndice; //PAT continuity counter
contPMT = Trk.missedFrags + fragIndice; //PMT continuity counter
contSDT = Trk.missedFrags + fragIndice; //SDT continuity counter
packCounter = 0;
parseData = true;
wantRequest = false;
seek(from);
ts_from = from;
} else {
initialize();
std::string request = H.url.substr(H.url.find("/", 5) + 1);
H.Clean();
if (H.url.find(".m3u8") != std::string::npos) {
H.SetHeader("Content-Type", "audio/x-mpegurl");
} else {
H.SetHeader("Content-Type", "audio/mpegurl");
}
H.SetHeader("Cache-Control", "no-cache");
H.setCORSHeaders();
if (!myMeta.tracks.size()){
H.SendResponse("404", "Not online or found", myConn);
H.Clean();
return;
}
if(method == "OPTIONS" || method == "HEAD"){
H.SendResponse("200", "OK", myConn);
H.Clean();
return;
}
std::string manifest;
if (request.find("/") == std::string::npos) {
manifest = liveIndex();
} else {
int selectId = atoi(request.substr(0, request.find("/")).c_str());
manifest = liveIndex(selectId, sessId);
}
H.SetBody(manifest);
H.SendResponse("200", "OK", myConn);
}
}
void OutHLS::sendNext(){
//First check if we need to stop.
if (thisPacket.getTime() >= until){
stop();
wantRequest = true;
parseData = false;
//Ensure alignment of contCounters for selected tracks, to prevent discontinuities.
for (std::set<unsigned long>::iterator it = selectedTracks.begin(); it != selectedTracks.end(); ++it){
DTSC::Track & Trk = myMeta.tracks[*it];
uint32_t pkgPid = 255 + *it;
int & contPkg = contCounters[pkgPid];
if (contPkg % 16 != 0){
packData.clear();
packData.setPID(pkgPid);
packData.addStuffing();
while (contPkg % 16 != 0){
packData.setContinuityCounter(++contPkg);
sendTS(packData.checkAndGetBuffer());
}
packData.clear();
}
}
//Signal end of data
H.Chunkify("", 0, myConn);
return;
}
//Invoke the generic TS output sendNext handler
TSOutput::sendNext();
}
void OutHLS::sendTS(const char * tsData, unsigned int len) {
H.Chunkify(tsData, len, myConn);
}
}