From 095a60e0ed00c98d99b350da07431423a8057039 Mon Sep 17 00:00:00 2001
From: Thulinma <jaron@vietors.com>
Date: Fri, 26 Apr 2019 16:55:11 +0200
Subject: [PATCH] HLS stream track selector support in index URLs, fixed source
 matching when multi-select or type-select is used, handle user agent
 exceptions in Output::selectDefaultTracks(), added Util::codecString to
 stream.h library, removed duplicate/wrong code from DASH/HLS outputs

---
 lib/stream.cpp                      | 41 ++++++++++++++++
 lib/stream.h                        |  2 +
 src/output/output.cpp               | 33 +++++++++++++
 src/output/output_hls.cpp           | 75 +++++++++++------------------
 src/output/output_http_internal.cpp | 37 ++++----------
 5 files changed, 114 insertions(+), 74 deletions(-)

diff --git a/lib/stream.cpp b/lib/stream.cpp
index 8c7ce702..5e6360b9 100644
--- a/lib/stream.cpp
+++ b/lib/stream.cpp
@@ -9,12 +9,30 @@
 #include "procs.h"
 #include "shared_memory.h"
 #include "socket.h"
+#include "mp4_generic.h"
 #include <semaphore.h>
 #include <stdlib.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
 
+std::string Util::codecString(const std::string & codec, const std::string & initData){
+  if (codec == "H264"){ 
+    std::stringstream r;
+    MP4::AVCC avccBox;
+    avccBox.setPayload(initData);
+    r << "avc1.";
+    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[1] << std::dec;
+    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[2] << std::dec;
+    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[3] << std::dec;
+    return r.str();
+  }
+  if (codec == "AAC"){return "mp4a.40.2";}
+  if (codec == "MP3"){return "mp4a.40.34";}
+  if (codec == "AC3"){return "ec-3";}
+  return "";
+}
+
 std::string Util::getTmpFolder(){
   std::string dir;
   char *tmp_char = 0;
@@ -351,6 +369,29 @@ uint8_t Util::getStreamStatus(const std::string &streamname){
   return streamStatus.mapped[0];
 }
 
+/// Checks if a given user agent is allowed according to the given exception.
+bool Util::checkException(const JSON::Value & ex, const std::string & useragent){
+  //No user agent? Always allow everything.
+  if (!useragent.size()){return true;}
+  if (!ex.isArray() || !ex.size()){return true;}
+  bool ret = true;
+  jsonForEachConst(ex, e){
+    if (!e->isArray() || !e->size()){continue;}
+    bool setTo = ((*e)[0u].asStringRef() == "whitelist");
+    if (e->size() == 1){
+      ret = setTo;
+      continue;
+    }
+    if (!(*e)[1].isArray()){continue;}
+    jsonForEachConst((*e)[1u], i){
+      if (useragent.find(i->asStringRef()) != std::string::npos){
+        ret = setTo;
+      }
+    }
+  }
+  return ret;
+}
+
 Util::DTSCShmReader::DTSCShmReader(const std::string &pageName){
   rPage.init(pageName, 0, false, false);
   if (rPage){rAcc = Util::RelAccX(rPage.mapped);}
diff --git a/lib/stream.h b/lib/stream.h
index 81c8bd5e..a928a5b6 100644
--- a/lib/stream.h
+++ b/lib/stream.h
@@ -18,6 +18,8 @@ namespace Util {
   JSON::Value getInputBySource(const std::string & filename, bool isProvider = false);
   DTSC::Meta getStreamMeta(const std::string & streamname);
   uint8_t getStreamStatus(const std::string & streamname);
+  bool checkException(const JSON::Value & ex, const std::string & useragent);
+  std::string codecString(const std::string & codec, const std::string & initData = "");
 
   class DTSCShmReader{
     public:
diff --git a/src/output/output.cpp b/src/output/output.cpp
index 15e1d0e9..f82b915f 100644
--- a/src/output/output.cpp
+++ b/src/output/output.cpp
@@ -317,6 +317,17 @@ namespace Mist{
               if (strRef[shift] == '+'){multiSel = true; ++shift;}
               for (std::set<unsigned long>::iterator itd = selectedTracks.begin(); itd != selectedTracks.end(); itd++){
                 if ((!byType && myMeta.tracks[*itd].codec == strRef.substr(shift)) || (byType && myMeta.tracks[*itd].type == strRef.substr(shift)) || strRef.substr(shift) == "*"){
+                  //user-agent-check
+                  bool problems = false;
+                  if (capa.isMember("exceptions") && capa["exceptions"].isObject() && capa["exceptions"].size()){
+                    jsonForEach(capa["exceptions"], ex){
+                      if (ex.key() == "codec:"+strRef.substr(shift)){
+                        problems = !Util::checkException(*ex, UA);
+                        break;
+                      }
+                    }
+                  }
+                  if (problems){break;}
                   selCounter++;
                   if (!multiSel){
                     break;
@@ -372,6 +383,17 @@ namespace Mist{
                 for (std::map<unsigned int, DTSC::Track>::reverse_iterator trit = myMeta.tracks.rbegin(); trit != myMeta.tracks.rend(); trit++){
                   if ((!byType && trit->second.codec == strRef.substr(shift)) || (byType && trit->second.type == strRef.substr(shift)) || strRef.substr(shift) == "*"){
                     if (autoSeek && trit->second.lastms < std::max(seekTarget, (uint64_t)6000lu) - 6000){continue;}
+                    //user-agent-check
+                    bool problems = false;
+                    if (capa.isMember("exceptions") && capa["exceptions"].isObject() && capa["exceptions"].size()){
+                      jsonForEach(capa["exceptions"], ex){
+                        if (ex.key() == "codec:"+strRef.substr(shift)){
+                          problems = !Util::checkException(*ex, UA);
+                          break;
+                        }
+                      }
+                    }
+                    if (problems){continue;}
                     selectedTracks.insert(trit->first);
                     found = true;
                     if (!multiSel){break;}
@@ -381,6 +403,17 @@ namespace Mist{
                 for (std::map<unsigned int, DTSC::Track>::iterator trit = myMeta.tracks.begin(); trit != myMeta.tracks.end(); trit++){
                   if ((!byType && trit->second.codec == strRef.substr(shift)) || (byType && trit->second.type == strRef.substr(shift)) || strRef.substr(shift) == "*"){
                     if (autoSeek && trit->second.lastms < std::max(seekTarget, (uint64_t)6000lu) - 6000){continue;}
+                    //user-agent-check
+                    bool problems = false;
+                    if (capa.isMember("exceptions") && capa["exceptions"].isObject() && capa["exceptions"].size()){
+                      jsonForEach(capa["exceptions"], ex){
+                        if (ex.key() == "codec:"+strRef.substr(shift)){
+                          problems = !Util::checkException(*ex, UA);
+                          break;
+                        }
+                      }
+                    }
+                    if (problems){continue;}
                     selectedTracks.insert(trit->first);
                     found = true;
                     if (!multiSel){break;}
diff --git a/src/output/output_hls.cpp b/src/output/output_hls.cpp
index 8ba7c76a..3ce1fbf9 100644
--- a/src/output/output_hls.cpp
+++ b/src/output/output_hls.cpp
@@ -12,61 +12,46 @@ namespace Mist {
     return false;
   }
 
-  std::string OutHLS::h264init(const std::string & initData){
-    std::stringstream r;
-    MP4::AVCC avccBox;
-    avccBox.setPayload(initData);
-    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[1] << std::dec;
-    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[2] << std::dec;
-    r << std::hex << std::setw(2) << std::setfill('0') << (int)initData[3] << std::dec;
-    return r.str();
-  }
-
   ///\brief Builds an index file for HTTP Live streaming.
   ///\return The index file for HTTP Live Streaming.
   std::string OutHLS::liveIndex(){
     std::stringstream result;
+    selectDefaultTracks();
     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"){
-        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"){
+    bool hasSubs = false;
+    for (std::set<unsigned long>::iterator it = selectedTracks.begin(); it != selectedTracks.end(); ++it){
+      if (audioId == -1 && myMeta.tracks[*it].type == "audio"){audioId = *it;}
+      if (!hasSubs && myMeta.tracks[*it].codec == "subtitle"){hasSubs = true;}
+    }
+    for (std::set<unsigned long>::iterator it = selectedTracks.begin(); it != selectedTracks.end(); ++it){
+      if (myMeta.tracks[*it].type == "video") {
         vidTracks++;
-        int bWidth = it->second.bps;
-        if (bWidth < 5){
+        int bWidth = myMeta.tracks[*it].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);
-        result << ",RESOLUTION=" << it->second.width << "x" << it->second.height;
-        if (it->second.fpks){
-          result << ",FRAME-RATE=" << (float)it->second.fpks / 1000; 
+        result << ",RESOLUTION=" << myMeta.tracks[*it].width << "x" << myMeta.tracks[*it].height;
+        if (myMeta.tracks[*it].fpks){
+          result << ",FRAME-RATE=" << (float)myMeta.tracks[*it].fpks / 1000; 
         }
-        if (it->second.codec == "H264"){
-          result << ",CODECS=\"";
-          if (it->second.codec == "H264"){
-            result << "avc1." << h264init(it->second.init);
-          }
-          if (audioId != -1){
-            if (myMeta.tracks[audioId].codec == "AAC"){
-              result << ",mp4a.40.2";
-            }else if (myMeta.tracks[audioId].codec == "MP3" ){
-              result << ",mp4a.40.34";
-            }
-          }
-          result << "\"";
+        if (hasSubs){
+          result << ",SUBTITLES=\"sub1\"";
         }
-        result <<"\r\n";
-        result << it->first;
+        result << ",CODECS=\"";
+        result << Util::codecString(myMeta.tracks[*it].codec, myMeta.tracks[*it].init);
         if (audioId != -1){
+          result << "," << Util::codecString(myMeta.tracks[audioId].codec, myMeta.tracks[audioId].init);
+        }
+        result << "\"";
+        result <<"\r\n";
+        result << *it;
+        if (audioId != -1) {
           result << "_" << audioId;
         }
         result << "/index.m3u8?sessId=" << getpid() << "\r\n";
@@ -74,11 +59,7 @@ namespace Mist {
     }
     if (!vidTracks && audioId) {
       result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (myMeta.tracks[audioId].bps * 8);
-      if (myMeta.tracks[audioId].codec == "AAC"){
-        result << ",CODECS=\"mp4a.40.2\"";
-      }else if (myMeta.tracks[audioId].codec == "MP3" ){
-        result << ",CODECS=\"mp4a.40.34\"";
-      }
+      result << ",CODECS=\"" << Util::codecString(myMeta.tracks[audioId].codec, myMeta.tracks[audioId].init) << "\"";
       result << "\r\n";
       result << audioId << "/index.m3u8\r\n";
     }
@@ -156,14 +137,14 @@ namespace Mist {
     capa["desc"] = "Segmented streaming in Apple (TS-based) format over HTTP ( = HTTP Live Streaming)";
     capa["url_rel"] = "/hls/$/index.m3u8";
     capa["url_prefix"] = "/hls/$/";
-    capa["codecs"][0u][0u].append("H264");
-    capa["codecs"][0u][1u].append("AAC");
-    capa["codecs"][0u][1u].append("MP3");
+    capa["codecs"][0u][0u].append("+H264");
+    capa["codecs"][0u][1u].append("+AAC");
+    capa["codecs"][0u][2u].append("+MP3");
     capa["methods"][0u]["handler"] = "http";
     capa["methods"][0u]["type"] = "html5/application/vnd.apple.mpegurl";
     capa["methods"][0u]["priority"] = 9;
     //MP3 only works on Edge/Apple
-    capa["exceptions"]["codec:MP3"] = JSON::fromString("[[\"blacklist\"],[\"whitelist\",[\"iPad\",\"iPhone\",\"iPod\",\"MacIntel\",\"Edge\"]]]");
+    capa["exceptions"]["codec:MP3"] = JSON::fromString("[[\"blacklist\",[\"Mozilla/\"]],[\"whitelist\",[\"iPad\",\"iPhone\",\"iPod\",\"MacIntel\",\"Edge\"]]]");
   }
 
   void OutHLS::onHTTP() {
diff --git a/src/output/output_http_internal.cpp b/src/output/output_http_internal.cpp
index 9aa77b16..ebd01cc2 100644
--- a/src/output/output_http_internal.cpp
+++ b/src/output/output_http_internal.cpp
@@ -188,34 +188,11 @@ namespace Mist {
     sources.insert(tmp);
   }
  
-  /// Checks if a given user agent is allowed according to the given exception.
-  bool checkException(const JSON::Value & ex, const std::string & useragent){
-    //No user agent? Always allow everything.
-    if (!useragent.size()){return true;}
-    if (!ex.isArray() || !ex.size()){return true;}
-    bool ret = true;
-    jsonForEachConst(ex, e){
-      if (!e->isArray() || !e->size()){continue;}
-      bool setTo = ((*e)[0u].asStringRef() == "whitelist");
-      if (e->size() == 1){
-        ret = setTo;
-        continue;
-      }
-      if (!(*e)[1].isArray()){continue;}
-      jsonForEachConst((*e)[1u], i){
-        if (useragent.find(i->asStringRef()) != std::string::npos){
-          ret = setTo;
-        }
-      }
-    }
-    return ret;
-  }
-
   void addSources(std::string & streamname, std::set<JSON::Value, sourceCompare> & sources, HTTP::URL url, JSON::Value & conncapa, JSON::Value & strmMeta, const std::string & useragent){
     if (strmMeta.isMember("live") && conncapa.isMember("exceptions") && conncapa["exceptions"].isObject() && conncapa["exceptions"].size()){
       jsonForEach(conncapa["exceptions"], ex){
         if (ex.key() == "live"){
-          if (!checkException(*ex, useragent)){
+          if (!Util::checkException(*ex, useragent)){
             return;
           }
         }
@@ -232,14 +209,20 @@ namespace Mist {
             unsigned int matches = 0;
             if ((*itb).size() > 0){
               jsonForEach((*itb), itc) {
+                const std::string & strRef = (*itc).asStringRef();
+                bool byType = false;
+                bool multiSel = false;
+                uint8_t shift = 0;
+                if (strRef[shift] == '@'){byType = true; ++shift;}
+                if (strRef[shift] == '+'){multiSel = true; ++shift;}
                 jsonForEach(strmMeta["tracks"], trit) {
-                  if ((*trit)["codec"].asStringRef() == (*itc).asStringRef()){
+                  if ((!byType && (*trit)["codec"].asStringRef() == strRef.substr(shift)) || (byType && (*trit)["type"].asStringRef() == strRef.substr(shift)) || strRef.substr(shift) == "*"){
                     matches++;
                     total_matches++;
                     if (conncapa.isMember("exceptions") && conncapa["exceptions"].isObject() && conncapa["exceptions"].size()){
                       jsonForEach(conncapa["exceptions"], ex){
-                        if (ex.key() == "codec:"+(*trit)["codec"].asStringRef()){
-                          if (!checkException(*ex, useragent)){
+                        if (ex.key() == "codec:"+strRef.substr(shift)){
+                          if (!Util::checkException(*ex, useragent)){
                             matches--;
                             total_matches--;
                           }