Reworked existing subtitle support (sideloaded, MP4 in and srt out)
This commit is contained in:
parent
741c4755cc
commit
b9e261e1ef
7 changed files with 159 additions and 33 deletions
38
lib/subtitles.cpp
Normal file
38
lib/subtitles.cpp
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
#include "subtitles.h"
|
||||||
|
#include "bitfields.h"
|
||||||
|
|
||||||
|
#include "defines.h"
|
||||||
|
namespace Subtitle {
|
||||||
|
|
||||||
|
Packet getSubtitle(DTSC::Packet packet, DTSC::Meta meta) {
|
||||||
|
char * tmp = 0;
|
||||||
|
uint16_t length = 0;
|
||||||
|
unsigned int len;
|
||||||
|
|
||||||
|
Packet output;
|
||||||
|
long int trackId= packet.getTrackId();
|
||||||
|
if(meta.tracks[trackId].codec != "TTXT" && meta.tracks[trackId].codec != "SRT") {
|
||||||
|
//no subtitle track
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(packet.hasMember("duration")) {
|
||||||
|
output.duration = packet.getInt("duration");
|
||||||
|
} else {
|
||||||
|
//get parts from meta
|
||||||
|
//calculate duration
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
packet.getString("data", output.subtitle);
|
||||||
|
if(meta.tracks[trackId].codec == "TTXT") {
|
||||||
|
unsigned short size = Bit::btohs(output.subtitle.c_str());
|
||||||
|
output.subtitle = output.subtitle.substr(2,size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
15
lib/subtitles.h
Normal file
15
lib/subtitles.h
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include "dtsc.h"
|
||||||
|
|
||||||
|
namespace Subtitle {
|
||||||
|
|
||||||
|
struct Packet {
|
||||||
|
std::string subtitle;
|
||||||
|
uint64_t duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
Packet getSubtitle(DTSC::Packet packet, DTSC::Meta meta);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -131,8 +131,8 @@ namespace Mist {
|
||||||
srtTrack = myMeta.tracks.rbegin()->first + 1;
|
srtTrack = myMeta.tracks.rbegin()->first + 1;
|
||||||
|
|
||||||
myMeta.tracks[srtTrack].trackID = srtTrack;
|
myMeta.tracks[srtTrack].trackID = srtTrack;
|
||||||
myMeta.tracks[srtTrack].type = "subtitle";
|
myMeta.tracks[srtTrack].type = "meta";
|
||||||
myMeta.tracks[srtTrack].codec = "srt";
|
myMeta.tracks[srtTrack].codec = "subtitle";
|
||||||
|
|
||||||
getNextSrt();
|
getNextSrt();
|
||||||
while (srtPack){
|
while (srtPack){
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
#include "input_mp4.h"
|
#include "input_mp4.h"
|
||||||
|
|
||||||
namespace Mist {
|
namespace Mist{
|
||||||
|
|
||||||
mp4TrackHeader::mp4TrackHeader(){
|
mp4TrackHeader::mp4TrackHeader(){
|
||||||
initialised = false;
|
initialised = false;
|
||||||
|
@ -136,7 +136,7 @@ namespace Mist {
|
||||||
size = stszBox.getEntrySize(index);
|
size = stszBox.getEntrySize(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
inputMP4::inputMP4(Util::Config * cfg) : Input(cfg) {
|
inputMP4::inputMP4(Util::Config * cfg) : Input(cfg){
|
||||||
malSize = 4;//initialise data read buffer to 0;
|
malSize = 4;//initialise data read buffer to 0;
|
||||||
data = (char*)malloc(malSize);
|
data = (char*)malloc(malSize);
|
||||||
capa["name"] = "MP4";
|
capa["name"] = "MP4";
|
||||||
|
@ -156,18 +156,18 @@ namespace Mist {
|
||||||
free(data);
|
free(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inputMP4::checkArguments() {
|
bool inputMP4::checkArguments(){
|
||||||
if (config->getString("input") == "-") {
|
if (config->getString("input") == "-"){
|
||||||
std::cerr << "Input from stdin not yet supported" << std::endl;
|
std::cerr << "Input from stdin not yet supported" << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!config->getString("streamname").size()){
|
if (!config->getString("streamname").size()){
|
||||||
if (config->getString("output") == "-") {
|
if (config->getString("output") == "-"){
|
||||||
std::cerr << "Output to stdout not yet supported" << std::endl;
|
std::cerr << "Output to stdout not yet supported" << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
if (config->getString("output") != "-") {
|
if (config->getString("output") != "-"){
|
||||||
std::cerr << "File output in player mode not supported" << std::endl;
|
std::cerr << "File output in player mode not supported" << std::endl;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -176,18 +176,18 @@ namespace Mist {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inputMP4::preRun() {
|
bool inputMP4::preRun(){
|
||||||
//open File
|
//open File
|
||||||
inFile = fopen(config->getString("input").c_str(), "r");
|
inFile = fopen(config->getString("input").c_str(), "r");
|
||||||
if (!inFile) {
|
if (!inFile){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool inputMP4::readHeader() {
|
bool inputMP4::readHeader(){
|
||||||
if (!inFile) {
|
if (!inFile){
|
||||||
INFO_MSG("inFile failed!");
|
INFO_MSG("inFile failed!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -306,7 +306,7 @@ namespace Mist {
|
||||||
initBox = vEntryBox.getPASP();
|
initBox = vEntryBox.getPASP();
|
||||||
if (initBox.isType("hvcC")){
|
if (initBox.isType("hvcC")){
|
||||||
myMeta.tracks[trackNo].init.assign(initBox.payload(), initBox.payloadSize());
|
myMeta.tracks[trackNo].init.assign(initBox.payload(), initBox.payloadSize());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sType == "mp4a" || sType == "aac " || sType == "ac-3"){
|
if (sType == "mp4a" || sType == "aac " || sType == "ac-3"){
|
||||||
MP4::AudioSampleEntry & aEntryBox = (MP4::AudioSampleEntry&)sEntryBox;
|
MP4::AudioSampleEntry & aEntryBox = (MP4::AudioSampleEntry&)sEntryBox;
|
||||||
|
@ -325,8 +325,8 @@ namespace Mist {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sType == "tx3g"){//plain text subtitles
|
if (sType == "tx3g"){//plain text subtitles
|
||||||
myMeta.tracks[trackNo].type = "subtitle";
|
myMeta.tracks[trackNo].type = "meta";
|
||||||
myMeta.tracks[trackNo].codec = "TTXT";
|
myMeta.tracks[trackNo].codec = "subtitle";
|
||||||
}
|
}
|
||||||
|
|
||||||
MP4::STSS stssBox = stblBox.getChild<MP4::STSS>();
|
MP4::STSS stssBox = stblBox.getChild<MP4::STSS>();
|
||||||
|
@ -407,7 +407,21 @@ namespace Mist {
|
||||||
}else{
|
}else{
|
||||||
BsetPart.timeOffset = 0;
|
BsetPart.timeOffset = 0;
|
||||||
}
|
}
|
||||||
myMeta.update(BsetPart.time, BsetPart.timeOffset, trackNo, stszBox.getEntrySize(stszIndex), BsetPart.bpos, BsetPart.keyframe);
|
|
||||||
|
if(sType == "tx3g"){
|
||||||
|
if(stszBox.getEntrySize(stszIndex) <=2 && false){
|
||||||
|
FAIL_MSG("size <=2");
|
||||||
|
}else{
|
||||||
|
long long packSendSize = 0;
|
||||||
|
packSendSize = 24 + (BsetPart.timeOffset ? 17 : 0) + (BsetPart.bpos ? 15 : 0) + 19 +
|
||||||
|
stszBox.getEntrySize(stszIndex) + 11-2 + 19;
|
||||||
|
myMeta.update(BsetPart.time, BsetPart.timeOffset, trackNo,
|
||||||
|
stszBox.getEntrySize(stszIndex) -2 , BsetPart.bpos, true,
|
||||||
|
packSendSize);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
myMeta.update(BsetPart.time, BsetPart.timeOffset, trackNo, stszBox.getEntrySize(stszIndex), BsetPart.bpos, BsetPart.keyframe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
@ -424,7 +438,7 @@ namespace Mist {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputMP4::getNext(bool smart) {//get next part from track in stream
|
void inputMP4::getNext(bool smart){//get next part from track in stream
|
||||||
if (curPositions.empty()){
|
if (curPositions.empty()){
|
||||||
thisPacket.null();
|
thisPacket.null();
|
||||||
return;
|
return;
|
||||||
|
@ -459,13 +473,31 @@ namespace Mist {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (myMeta.tracks[curPart.trackID].codec == "subtitle"){
|
||||||
if (myMeta.tracks[curPart.trackID].codec == "TTXT"){
|
|
||||||
unsigned int txtLen = Bit::btohs(data);
|
unsigned int txtLen = Bit::btohs(data);
|
||||||
if (!txtLen){
|
if (!txtLen && false ){
|
||||||
thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, " ", 1, 0/*Note: no bpos*/, isKeyframe);
|
curPart.index ++;
|
||||||
|
return getNext(smart);
|
||||||
|
//thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, " ", 1, 0/*Note: no bpos*/, isKeyframe);
|
||||||
}else{
|
}else{
|
||||||
thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data+2, txtLen, 0/*Note: no bpos*/, isKeyframe);
|
|
||||||
|
static JSON::Value thisPack;
|
||||||
|
thisPack.null();
|
||||||
|
thisPack["trackid"] = (long long)curPart.trackID;
|
||||||
|
thisPack["bpos"] = (long long)curPart.bpos; //(long long)fileSource.tellg();
|
||||||
|
thisPack["data"] = std::string(data+2,txtLen);
|
||||||
|
// thisPack["index"] = index;
|
||||||
|
thisPack["time"] = (long long)curPart.time;
|
||||||
|
thisPack["duration"] = 1000;
|
||||||
|
|
||||||
|
// thisPack["time"] = (long long)timestamp;
|
||||||
|
thisPack["keyframe"] = true;
|
||||||
|
// Write the json value to lastpack
|
||||||
|
std::string tmpStr = thisPack.toNetPacked();
|
||||||
|
thisPacket.reInit(tmpStr.data(), tmpStr.size());
|
||||||
|
//return;
|
||||||
|
|
||||||
|
//thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data+2, txtLen, 0/*Note: no bpos*/, isKeyframe);
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data, curPart.size, 0/*Note: no bpos*/, isKeyframe);
|
thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data, curPart.size, 0/*Note: no bpos*/, isKeyframe);
|
||||||
|
@ -479,7 +511,7 @@ namespace Mist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputMP4::seek(int seekTime) {//seek to a point
|
void inputMP4::seek(int seekTime){//seek to a point
|
||||||
nextKeyframe.clear();
|
nextKeyframe.clear();
|
||||||
//for all tracks
|
//for all tracks
|
||||||
curPositions.clear();
|
curPositions.clear();
|
||||||
|
@ -509,16 +541,16 @@ namespace Mist {
|
||||||
}//rof all tracks
|
}//rof all tracks
|
||||||
}
|
}
|
||||||
|
|
||||||
void inputMP4::trackSelect(std::string trackSpec) {
|
void inputMP4::trackSelect(std::string trackSpec){
|
||||||
selectedTracks.clear();
|
selectedTracks.clear();
|
||||||
long long int index;
|
long long int index;
|
||||||
while (trackSpec != "") {
|
while (trackSpec != ""){
|
||||||
index = trackSpec.find(' ');
|
index = trackSpec.find(' ');
|
||||||
selectedTracks.insert(atoi(trackSpec.substr(0, index).c_str()));
|
selectedTracks.insert(atoi(trackSpec.substr(0, index).c_str()));
|
||||||
VERYHIGH_MSG("Added track %d, index = %lld, (index == npos) = %d", atoi(trackSpec.substr(0, index).c_str()), index, index == std::string::npos);
|
VERYHIGH_MSG("Added track %d, index = %lld, (index == npos) = %d", atoi(trackSpec.substr(0, index).c_str()), index, index == std::string::npos);
|
||||||
if (index != std::string::npos) {
|
if (index != std::string::npos){
|
||||||
trackSpec.erase(0, index + 1);
|
trackSpec.erase(0, index + 1);
|
||||||
} else {
|
}else{
|
||||||
trackSpec = "";
|
trackSpec = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,12 +35,19 @@ namespace Mist {
|
||||||
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) << "\r\n";
|
result << "#EXT-X-STREAM-INF:PROGRAM-ID=1,SUBTITLES=\"sub1\",BANDWIDTH=" << (bWidth * 8) << "\r\n";
|
||||||
result << it->first;
|
result << it->first;
|
||||||
if (audioId != -1) {
|
if (audioId != -1) {
|
||||||
result << "_" << audioId;
|
result << "_" << audioId;
|
||||||
}
|
}
|
||||||
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
|
result << "/index.m3u8?sessId=" << getpid() << "\r\n";
|
||||||
|
}else if(it->second.codec == "subtitle"){
|
||||||
|
|
||||||
|
if(it->second.lang.empty()){
|
||||||
|
it->second.lang = "und";
|
||||||
|
}
|
||||||
|
|
||||||
|
result << "#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID=\"sub1\",LANGUAGE=\"" << it->second.lang << "\",NAME=\"" << Encodings::ISO639::decode(it->second.lang) << "\",AUTOSELECT=NO,DEFAULT=NO,FORCED=NO,URI=\"" << it->first << "/index.m3u8\"" << "\r\n";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!vidTracks && audioId) {
|
if (!vidTracks && audioId) {
|
||||||
|
@ -145,10 +152,15 @@ namespace Mist {
|
||||||
duration = myMeta.tracks[tid].lastms - starttime;
|
duration = myMeta.tracks[tid].lastms - starttime;
|
||||||
}
|
}
|
||||||
char lineBuf[400];
|
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());
|
if(myMeta.tracks[tid].codec == "subtitle"){
|
||||||
|
snprintf(lineBuf, 400, "#EXTINF:%f,\r\n../../../%s.vtt?track=%d&from=%lld&to=%lld\r\n", streamName.c_str(),(double)duration/1000,tid, starttime, starttime + duration);
|
||||||
}else{
|
}else{
|
||||||
snprintf(lineBuf, 400, "#EXTINF:%f,\r\n%lld_%lld.ts\r\n", (double)duration/1000, starttime, starttime + duration);
|
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);
|
durs.push_back(duration);
|
||||||
total_dur += duration;
|
total_dur += duration;
|
||||||
|
|
|
@ -14,8 +14,7 @@ namespace Mist {
|
||||||
capa["desc"] = "Enables HTTP protocol subtitle streaming in subrip and WebVTT formats.";
|
capa["desc"] = "Enables HTTP protocol subtitle streaming in subrip and WebVTT formats.";
|
||||||
capa["url_match"].append("/$.srt");
|
capa["url_match"].append("/$.srt");
|
||||||
capa["url_match"].append("/$.vtt");
|
capa["url_match"].append("/$.vtt");
|
||||||
capa["codecs"][0u][0u].append("srt");
|
capa["codecs"][0u][0u].append("subtitle");
|
||||||
capa["codecs"][0u][0u].append("TTXT");
|
|
||||||
capa["methods"][0u]["handler"] = "http";
|
capa["methods"][0u]["handler"] = "http";
|
||||||
capa["methods"][0u]["type"] = "html5/text/plain";
|
capa["methods"][0u]["type"] = "html5/text/plain";
|
||||||
capa["methods"][0u]["priority"] = 8ll;
|
capa["methods"][0u]["priority"] = 8ll;
|
||||||
|
@ -30,6 +29,7 @@ namespace Mist {
|
||||||
char * dataPointer = 0;
|
char * dataPointer = 0;
|
||||||
unsigned int len = 0;
|
unsigned int len = 0;
|
||||||
thisPacket.getString("data", dataPointer, len);
|
thisPacket.getString("data", dataPointer, len);
|
||||||
|
// INFO_MSG("getting sub: %s", dataPointer);
|
||||||
//ignore empty subs
|
//ignore empty subs
|
||||||
if (len == 0 || (len == 1 && dataPointer[0] == ' ')){
|
if (len == 0 || (len == 1 && dataPointer[0] == ' ')){
|
||||||
return;
|
return;
|
||||||
|
@ -39,6 +39,20 @@ namespace Mist {
|
||||||
tmp << lastNum++ << std::endl;
|
tmp << lastNum++ << std::endl;
|
||||||
}
|
}
|
||||||
long long unsigned int time = thisPacket.getTime();
|
long long unsigned int time = thisPacket.getTime();
|
||||||
|
|
||||||
|
|
||||||
|
//filter subtitle in specific timespan
|
||||||
|
if(filter_from > 0 && time < filter_from){
|
||||||
|
index++; //when using seek, the index is lost.
|
||||||
|
seek(filter_from);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(filter_to > 0 && time > filter_to && filter_to > filter_from){
|
||||||
|
config->is_active = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
char tmpBuf[50];
|
char tmpBuf[50];
|
||||||
int tmpLen = sprintf(tmpBuf, "%.2llu:%.2llu:%.2llu.%.3llu", (time / 3600000), ((time % 3600000) / 60000), (((time % 3600000) % 60000) / 1000), time % 1000);
|
int tmpLen = sprintf(tmpBuf, "%.2llu:%.2llu:%.2llu.%.3llu", (time / 3600000), ((time % 3600000) / 60000), (((time % 3600000) % 60000) / 1000), time % 1000);
|
||||||
tmp.write(tmpBuf, tmpLen);
|
tmp.write(tmpBuf, tmpLen);
|
||||||
|
@ -79,6 +93,18 @@ namespace Mist {
|
||||||
selectedTracks.clear();
|
selectedTracks.clear();
|
||||||
selectedTracks.insert(JSON::Value(H.GetVar("track")).asInt());
|
selectedTracks.insert(JSON::Value(H.GetVar("track")).asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filter_from = 0;
|
||||||
|
filter_to = 0;
|
||||||
|
index = 0;
|
||||||
|
|
||||||
|
if (H.GetVar("from") != ""){
|
||||||
|
filter_from = JSON::Value(H.GetVar("from")).asInt();
|
||||||
|
}
|
||||||
|
if (H.GetVar("to") != ""){
|
||||||
|
filter_to = JSON::Value(H.GetVar("to")).asInt();
|
||||||
|
}
|
||||||
|
|
||||||
H.Clean();
|
H.Clean();
|
||||||
H.setCORSHeaders();
|
H.setCORSHeaders();
|
||||||
if(method == "OPTIONS" || method == "HEAD"){
|
if(method == "OPTIONS" || method == "HEAD"){
|
||||||
|
|
|
@ -13,6 +13,9 @@ namespace Mist {
|
||||||
protected:
|
protected:
|
||||||
bool webVTT;
|
bool webVTT;
|
||||||
int lastNum;
|
int lastNum;
|
||||||
|
uint32_t filter_from;
|
||||||
|
uint32_t filter_to;
|
||||||
|
uint32_t index;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue