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
This commit is contained in:
parent
7eb4f6634a
commit
095a60e0ed
5 changed files with 114 additions and 74 deletions
|
@ -9,12 +9,30 @@
|
||||||
#include "procs.h"
|
#include "procs.h"
|
||||||
#include "shared_memory.h"
|
#include "shared_memory.h"
|
||||||
#include "socket.h"
|
#include "socket.h"
|
||||||
|
#include "mp4_generic.h"
|
||||||
#include <semaphore.h>
|
#include <semaphore.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.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 Util::getTmpFolder(){
|
||||||
std::string dir;
|
std::string dir;
|
||||||
char *tmp_char = 0;
|
char *tmp_char = 0;
|
||||||
|
@ -351,6 +369,29 @@ uint8_t Util::getStreamStatus(const std::string &streamname){
|
||||||
return streamStatus.mapped[0];
|
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){
|
Util::DTSCShmReader::DTSCShmReader(const std::string &pageName){
|
||||||
rPage.init(pageName, 0, false, false);
|
rPage.init(pageName, 0, false, false);
|
||||||
if (rPage){rAcc = Util::RelAccX(rPage.mapped);}
|
if (rPage){rAcc = Util::RelAccX(rPage.mapped);}
|
||||||
|
|
|
@ -18,6 +18,8 @@ namespace Util {
|
||||||
JSON::Value getInputBySource(const std::string & filename, bool isProvider = false);
|
JSON::Value getInputBySource(const std::string & filename, bool isProvider = false);
|
||||||
DTSC::Meta getStreamMeta(const std::string & streamname);
|
DTSC::Meta getStreamMeta(const std::string & streamname);
|
||||||
uint8_t getStreamStatus(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{
|
class DTSCShmReader{
|
||||||
public:
|
public:
|
||||||
|
|
|
@ -317,6 +317,17 @@ namespace Mist{
|
||||||
if (strRef[shift] == '+'){multiSel = true; ++shift;}
|
if (strRef[shift] == '+'){multiSel = true; ++shift;}
|
||||||
for (std::set<unsigned long>::iterator itd = selectedTracks.begin(); itd != selectedTracks.end(); itd++){
|
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) == "*"){
|
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++;
|
selCounter++;
|
||||||
if (!multiSel){
|
if (!multiSel){
|
||||||
break;
|
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++){
|
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 ((!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;}
|
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);
|
selectedTracks.insert(trit->first);
|
||||||
found = true;
|
found = true;
|
||||||
if (!multiSel){break;}
|
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++){
|
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 ((!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;}
|
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);
|
selectedTracks.insert(trit->first);
|
||||||
found = true;
|
found = true;
|
||||||
if (!multiSel){break;}
|
if (!multiSel){break;}
|
||||||
|
|
|
@ -12,61 +12,46 @@ namespace Mist {
|
||||||
return false;
|
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.
|
///\brief Builds an index file for HTTP Live streaming.
|
||||||
///\return The index file for HTTP Live Streaming.
|
///\return The index file for HTTP Live Streaming.
|
||||||
std::string OutHLS::liveIndex(){
|
std::string OutHLS::liveIndex(){
|
||||||
std::stringstream result;
|
std::stringstream result;
|
||||||
|
selectDefaultTracks();
|
||||||
result << "#EXTM3U\r\n";
|
result << "#EXTM3U\r\n";
|
||||||
int audioId = -1;
|
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;
|
unsigned int vidTracks = 0;
|
||||||
for (std::map<unsigned int,DTSC::Track>::iterator it = myMeta.tracks.begin(); it != myMeta.tracks.end(); it++){
|
bool hasSubs = false;
|
||||||
if (it->second.codec == "H264"){
|
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++;
|
vidTracks++;
|
||||||
int bWidth = it->second.bps;
|
int bWidth = myMeta.tracks[*it].bps;
|
||||||
if (bWidth < 5){
|
if (bWidth < 5) {
|
||||||
bWidth = 5;
|
bWidth = 5;
|
||||||
}
|
}
|
||||||
if (audioId != -1){
|
if (audioId != -1){
|
||||||
bWidth += myMeta.tracks[audioId].bps;
|
bWidth += myMeta.tracks[audioId].bps;
|
||||||
}
|
}
|
||||||
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (bWidth * 8);
|
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (bWidth * 8);
|
||||||
result << ",RESOLUTION=" << it->second.width << "x" << it->second.height;
|
result << ",RESOLUTION=" << myMeta.tracks[*it].width << "x" << myMeta.tracks[*it].height;
|
||||||
if (it->second.fpks){
|
if (myMeta.tracks[*it].fpks){
|
||||||
result << ",FRAME-RATE=" << (float)it->second.fpks / 1000;
|
result << ",FRAME-RATE=" << (float)myMeta.tracks[*it].fpks / 1000;
|
||||||
|
}
|
||||||
|
if (hasSubs){
|
||||||
|
result << ",SUBTITLES=\"sub1\"";
|
||||||
}
|
}
|
||||||
if (it->second.codec == "H264"){
|
|
||||||
result << ",CODECS=\"";
|
result << ",CODECS=\"";
|
||||||
if (it->second.codec == "H264"){
|
result << Util::codecString(myMeta.tracks[*it].codec, myMeta.tracks[*it].init);
|
||||||
result << "avc1." << h264init(it->second.init);
|
|
||||||
}
|
|
||||||
if (audioId != -1){
|
if (audioId != -1){
|
||||||
if (myMeta.tracks[audioId].codec == "AAC"){
|
result << "," << Util::codecString(myMeta.tracks[audioId].codec, myMeta.tracks[audioId].init);
|
||||||
result << ",mp4a.40.2";
|
|
||||||
}else if (myMeta.tracks[audioId].codec == "MP3" ){
|
|
||||||
result << ",mp4a.40.34";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result << "\"";
|
result << "\"";
|
||||||
}
|
|
||||||
result <<"\r\n";
|
result <<"\r\n";
|
||||||
result << it->first;
|
result << *it;
|
||||||
if (audioId != -1){
|
if (audioId != -1) {
|
||||||
result << "_" << audioId;
|
result << "_" << audioId;
|
||||||
}
|
}
|
||||||
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
|
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
|
||||||
|
@ -74,11 +59,7 @@ namespace Mist {
|
||||||
}
|
}
|
||||||
if (!vidTracks && audioId) {
|
if (!vidTracks && audioId) {
|
||||||
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (myMeta.tracks[audioId].bps * 8);
|
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" << (myMeta.tracks[audioId].bps * 8);
|
||||||
if (myMeta.tracks[audioId].codec == "AAC"){
|
result << ",CODECS=\"" << Util::codecString(myMeta.tracks[audioId].codec, myMeta.tracks[audioId].init) << "\"";
|
||||||
result << ",CODECS=\"mp4a.40.2\"";
|
|
||||||
}else if (myMeta.tracks[audioId].codec == "MP3" ){
|
|
||||||
result << ",CODECS=\"mp4a.40.34\"";
|
|
||||||
}
|
|
||||||
result << "\r\n";
|
result << "\r\n";
|
||||||
result << audioId << "/index.m3u8\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["desc"] = "Segmented streaming in Apple (TS-based) format over HTTP ( = HTTP Live Streaming)";
|
||||||
capa["url_rel"] = "/hls/$/index.m3u8";
|
capa["url_rel"] = "/hls/$/index.m3u8";
|
||||||
capa["url_prefix"] = "/hls/$/";
|
capa["url_prefix"] = "/hls/$/";
|
||||||
capa["codecs"][0u][0u].append("H264");
|
capa["codecs"][0u][0u].append("+H264");
|
||||||
capa["codecs"][0u][1u].append("AAC");
|
capa["codecs"][0u][1u].append("+AAC");
|
||||||
capa["codecs"][0u][1u].append("MP3");
|
capa["codecs"][0u][2u].append("+MP3");
|
||||||
capa["methods"][0u]["handler"] = "http";
|
capa["methods"][0u]["handler"] = "http";
|
||||||
capa["methods"][0u]["type"] = "html5/application/vnd.apple.mpegurl";
|
capa["methods"][0u]["type"] = "html5/application/vnd.apple.mpegurl";
|
||||||
capa["methods"][0u]["priority"] = 9;
|
capa["methods"][0u]["priority"] = 9;
|
||||||
//MP3 only works on Edge/Apple
|
//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() {
|
void OutHLS::onHTTP() {
|
||||||
|
|
|
@ -188,34 +188,11 @@ namespace Mist {
|
||||||
sources.insert(tmp);
|
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){
|
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()){
|
if (strmMeta.isMember("live") && conncapa.isMember("exceptions") && conncapa["exceptions"].isObject() && conncapa["exceptions"].size()){
|
||||||
jsonForEach(conncapa["exceptions"], ex){
|
jsonForEach(conncapa["exceptions"], ex){
|
||||||
if (ex.key() == "live"){
|
if (ex.key() == "live"){
|
||||||
if (!checkException(*ex, useragent)){
|
if (!Util::checkException(*ex, useragent)){
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,14 +209,20 @@ namespace Mist {
|
||||||
unsigned int matches = 0;
|
unsigned int matches = 0;
|
||||||
if ((*itb).size() > 0){
|
if ((*itb).size() > 0){
|
||||||
jsonForEach((*itb), itc) {
|
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) {
|
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++;
|
matches++;
|
||||||
total_matches++;
|
total_matches++;
|
||||||
if (conncapa.isMember("exceptions") && conncapa["exceptions"].isObject() && conncapa["exceptions"].size()){
|
if (conncapa.isMember("exceptions") && conncapa["exceptions"].isObject() && conncapa["exceptions"].size()){
|
||||||
jsonForEach(conncapa["exceptions"], ex){
|
jsonForEach(conncapa["exceptions"], ex){
|
||||||
if (ex.key() == "codec:"+(*trit)["codec"].asStringRef()){
|
if (ex.key() == "codec:"+strRef.substr(shift)){
|
||||||
if (!checkException(*ex, useragent)){
|
if (!Util::checkException(*ex, useragent)){
|
||||||
matches--;
|
matches--;
|
||||||
total_matches--;
|
total_matches--;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue