From ecbbaa463776c624875412c7971c128505264dcc Mon Sep 17 00:00:00 2001 From: Thulinma Date: Thu, 3 Sep 2015 20:18:42 +0200 Subject: [PATCH] MP4 live streaming support by Oswald de Bruin. --- src/output/output_progressive_mp4.cpp | 1068 ++++++++++++++++--------- src/output/output_progressive_mp4.h | 37 +- 2 files changed, 719 insertions(+), 386 deletions(-) diff --git a/src/output/output_progressive_mp4.cpp b/src/output/output_progressive_mp4.cpp index d741686c..da1ea1e4 100644 --- a/src/output/output_progressive_mp4.cpp +++ b/src/output/output_progressive_mp4.cpp @@ -1,19 +1,23 @@ -#include "output_progressive_mp4.h" #include #include #include #include +#include +#include "output_progressive_mp4.h" namespace Mist { - OutProgressiveMP4::OutProgressiveMP4(Socket::Connection & conn) : HTTPOutput(conn){} + OutProgressiveMP4::OutProgressiveMP4(Socket::Connection & conn) : HTTPOutput(conn) { + completeKeysOnly = true; + } OutProgressiveMP4::~OutProgressiveMP4() {} - - void OutProgressiveMP4::init(Util::Config * cfg){ + + void OutProgressiveMP4::init(Util::Config * cfg) { HTTPOutput::init(cfg); capa["name"] = "MP4"; capa["desc"] = "Enables HTTP protocol progressive streaming."; capa["url_rel"] = "/$.mp4"; - capa["url_match"] = "/$.mp4"; + capa["url_match"][0u] = "/$.mp4"; + capa["url_match"][1u] = "/$.3gp"; capa["codecs"][0u][0u].append("H264"); capa["codecs"][0u][0u].append("HEVC"); capa["codecs"][0u][1u].append("AAC"); @@ -22,362 +26,394 @@ namespace Mist { capa["methods"][0u]["handler"] = "http"; capa["methods"][0u]["type"] = "html5/video/mp4"; capa["methods"][0u]["priority"] = 8ll; - capa["methods"][0u]["nolive"] = 1; } - - long long unsigned OutProgressiveMP4::estimateFileSize(){ + + long long unsigned OutProgressiveMP4::estimateFileSize() { long long unsigned retVal = 0; - for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++){ - for (std::deque::iterator keyIt = myMeta.tracks[*it].keySizes.begin(); keyIt != myMeta.tracks[*it].keySizes.end(); keyIt++){ + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + for (std::deque::iterator keyIt = myMeta.tracks[*it].keySizes.begin(); keyIt != myMeta.tracks[*it].keySizes.end(); keyIt++) { retVal += *keyIt; } } return retVal * 1.1; } - - std::string OutProgressiveMP4::DTSCMeta2MP4Header(long long & size){ + + ///\todo This function does not indicate errors anywhere... maybe fix this... + std::string OutProgressiveMP4::DTSCMeta2MP4Header(long long & size, int fragmented) { + //Make sure we have a proper being value for the size... + size = 0; + //Stores the result of the function std::stringstream header; - //ftyp box - MP4::FTYP ftypBox; - header.write(ftypBox.asBox(),ftypBox.boxedSize()); - bool biggerThan4G = (estimateFileSize() > 0xFFFFFFFFull); + //Determines whether the outputfile is larger than 4GB, in which case we need to use 64-bit boxes for offsets + bool useLargeBoxes = !fragmented && (estimateFileSize() > 0xFFFFFFFFull); + //Keeps track of the total size of the mdat box uint64_t mdatSize = 0; - //moov box - MP4::MOOV moovBox; - unsigned int moovOffset = 0; - { - //calculating longest duration - long long int firstms = -1; - long long int lastms = -1; - for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { - if (lastms == -1 || lastms < (long long)myMeta.tracks[*it].lastms){ - lastms = myMeta.tracks[*it].lastms; - } - if (firstms == -1 || firstms > (long long)myMeta.tracks[*it].firstms){ - firstms = myMeta.tracks[*it].firstms; - } - } - MP4::MVHD mvhdBox(lastms - firstms); - moovBox.setContent(mvhdBox, moovOffset++); + + + //Start actually creating the header + + //MP4 Files always start with an FTYP box. Constructor sets default values + MP4::FTYP ftypBox; + if (sending3GP) { + ftypBox.setMajorBrand("3gp6"); + ftypBox.setCompatibleBrands("3gp6", 3); } - for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + header.write(ftypBox.asBox(), ftypBox.boxedSize()); + + //Start building the moov box. This is the metadata box for an mp4 file, and will contain all metadata. + MP4::MOOV moovBox; + //Keep track of the current index within the moovBox + unsigned int moovOffset = 0; + + + //Construct with duration of -1, as this is the default for fragmented + MP4::MVHD mvhdBox(-1); + //Then override it only when we are not sending a fragmented file + if (!fragmented){ + //calculating longest duration + long long unsigned firstms = 0xFFFFFFFFFFFFFFull; + long long unsigned lastms = 0; + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + lastms = std::max(lastms, myMeta.tracks[*it].lastms); + firstms = std::min(firstms, myMeta.tracks[*it].firstms); + } + mvhdBox.setDuration(lastms - firstms); + } + //Set the trackid for the first "empty" track within the file. + mvhdBox.setTrackID(selectedTracks.size() + 1); + moovBox.setContent(mvhdBox, moovOffset++); + + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { DTSC::Track & thisTrack = myMeta.tracks[*it]; MP4::TRAK trakBox; - { - { - MP4::TKHD tkhdBox(*it, thisTrack.lastms - thisTrack.firstms, thisTrack.width, thisTrack.height); - trakBox.setContent(tkhdBox, 0); - }{ - MP4::MDIA mdiaBox; - unsigned int mdiaOffset = 0; - { - MP4::MDHD mdhdBox(thisTrack.lastms - thisTrack.firstms); - mdiaBox.setContent(mdhdBox, mdiaOffset++); - }//MDHD box - { - MP4::HDLR hdlrBox(thisTrack.type, thisTrack.getIdentifier()); - mdiaBox.setContent(hdlrBox, mdiaOffset++); - }//hdlr box - { - MP4::MINF minfBox; - unsigned int minfOffset = 0; - if (thisTrack.type== "video"){ - MP4::VMHD vmhdBox; - vmhdBox.setFlags(1); - minfBox.setContent(vmhdBox,minfOffset++); - }else if (thisTrack.type == "audio"){ - MP4::SMHD smhdBox; - minfBox.setContent(smhdBox,minfOffset++); - }//type box - { - MP4::DINF dinfBox; - MP4::DREF drefBox; - dinfBox.setContent(drefBox,0); - minfBox.setContent(dinfBox,minfOffset++); - }//dinf box - { - MP4::STBL stblBox; - unsigned int offset = 0; - { - MP4::STSD stsdBox; - stsdBox.setVersion(0); - if (thisTrack.type == "video"){//boxname = codec - MP4::VisualSampleEntry vse; - if (thisTrack.codec == "H264"){ - vse.setCodec("avc1"); - } - if (thisTrack.codec == "HEVC"){ - vse.setCodec("hev1"); - } - vse.setDataReferenceIndex(1); - vse.setWidth(thisTrack.width); - vse.setHeight(thisTrack.height); - if (thisTrack.codec == "H264"){ - MP4::AVCC avccBox; - avccBox.setPayload(thisTrack.init); - vse.setCLAP(avccBox); - } - /*LTS-START*/ - if (thisTrack.codec == "HEVC"){ - MP4::HVCC hvccBox; - hvccBox.setPayload(thisTrack.init); - vse.setCLAP(hvccBox); - } - /*LTS-END*/ - stsdBox.setEntry(vse,0); - }else if(thisTrack.type == "audio"){//boxname = codec - MP4::AudioSampleEntry ase; - if (thisTrack.codec == "AAC"){ - ase.setCodec("mp4a"); - ase.setDataReferenceIndex(1); - }else if (thisTrack.codec == "MP3"){ - ase.setCodec("mp4a"); - ase.setDataReferenceIndex(1); - }else if (thisTrack.codec == "AC3"){ - ase.setCodec("ac-3"); - ase.setDataReferenceIndex(1); - } - ase.setSampleRate(thisTrack.rate); - ase.setChannelCount(thisTrack.channels); - ase.setSampleSize(thisTrack.size); - if (myMeta.tracks[*it].codec == "AC3"){ - MP4::DAC3 dac3Box(thisTrack.rate, thisTrack.channels); - ase.setCodecBox(dac3Box); - }else{//other codecs use the ESDS box - MP4::ESDS esdsBox(thisTrack.init); - ase.setCodecBox(esdsBox); - } - stsdBox.setEntry(ase,0); - } - stblBox.setContent(stsdBox,offset++); - }//stsd box - { - MP4::STTS sttsBox; - sttsBox.setVersion(0); - if (thisTrack.parts.size()){ - /// \todo Optimize for speed. We're currently parsing backwards, to prevent massive reallocs. Better would be to not set sampleCount to 1 for every single entry, calculate in advance, *then* set backwards. Volunteers? - for (unsigned int part = thisTrack.parts.size(); part > 0; --part){ - MP4::STTSEntry newEntry; - newEntry.sampleCount = 1; - newEntry.sampleDelta = thisTrack.parts[part-1].getDuration(); - sttsBox.setSTTSEntry(newEntry, part-1); - } - } - stblBox.setContent(sttsBox,offset++); - }//stts box - if (thisTrack.type == "video"){ - //STSS Box here - MP4::STSS stssBox; - stssBox.setVersion(0); - int tmpCount = 0; - int tmpItCount = 0; - for ( std::deque< DTSC::Key>::iterator tmpIt = thisTrack.keys.begin(); tmpIt != thisTrack.keys.end(); tmpIt ++) { - stssBox.setSampleNumber(tmpCount,tmpItCount); - tmpCount += tmpIt->getParts(); - tmpItCount ++; - } - stblBox.setContent(stssBox,offset++); - }//stss box - { - MP4::STSC stscBox; - stscBox.setVersion(0); - MP4::STSCEntry stscEntry; - stscEntry.firstChunk = 1; - stscEntry.samplesPerChunk = 1; - stscEntry.sampleDescriptionIndex = 1; - stscBox.setSTSCEntry(stscEntry, 0); - stblBox.setContent(stscBox,offset++); - }//stsc box - { - bool makeCTTS = false; - MP4::STSZ stszBox; - stszBox.setVersion(0); - if (thisTrack.parts.size()){ - std::deque::reverse_iterator tmpIt = thisTrack.parts.rbegin(); - for (unsigned int part = thisTrack.parts.size(); part > 0; --part){ - unsigned int partSize = tmpIt->getSize(); - stszBox.setEntrySize(partSize, part-1);//in bytes in file - size += partSize; - makeCTTS |= tmpIt->getOffset(); - tmpIt++; - } - } - if (makeCTTS){ - MP4::CTTS cttsBox; - cttsBox.setVersion(0); - if (thisTrack.parts.size()){ - std::deque::iterator tmpIt = thisTrack.parts.begin(); - MP4::CTTSEntry tmpEntry; - tmpEntry.sampleCount = 1; - tmpEntry.sampleOffset = tmpIt->getOffset(); - unsigned int totalEntries = 0; - tmpIt++; - while (tmpIt != thisTrack.parts.end()){ - unsigned int timeOffset = tmpIt->getOffset(); - if (timeOffset == tmpEntry.sampleOffset){ - tmpEntry.sampleCount++; - }else{ - cttsBox.setCTTSEntry(tmpEntry, totalEntries++); - tmpEntry.sampleCount = 1; - tmpEntry.sampleOffset = timeOffset; - } - tmpIt++; - } - cttsBox.setCTTSEntry(tmpEntry, totalEntries++); - //cttsBox.setEntryCount(totalEntries); - } - stblBox.setContent(cttsBox,offset++); - }//ctts - stblBox.setContent(stszBox,offset++); - }//stsz box - { - if (biggerThan4G){ - MP4::CO64 CO64Box; - //Inserting empty values on purpose here, will be fixed later. - if (thisTrack.parts.size() != 0){ - CO64Box.setChunkOffset(0, thisTrack.parts.size() - 1);//this inserts all empty entries at once - } - stblBox.setContent(CO64Box,offset++); - }else{ - MP4::STCO stcoBox; - //Inserting empty values on purpose here, will be fixed later. - if (thisTrack.parts.size() != 0){ - stcoBox.setChunkOffset(0, thisTrack.parts.size() - 1);//this inserts all empty entries at once - } - stblBox.setContent(stcoBox,offset++); - } - }//stco box - minfBox.setContent(stblBox,minfOffset++); - }//stbl box - mdiaBox.setContent(minfBox, mdiaOffset++); - }//minf box - trakBox.setContent(mdiaBox, 1); + //Keep track of the current index within the moovBox + unsigned int trakOffset = 0; + + MP4::TKHD tkhdBox(thisTrack, fragmented); + trakBox.setContent(tkhdBox, trakOffset++); + + //Create an EDTS box, containing an ELST box with default values; + ///\todo Figure out if this box is really needed for anything. + MP4::EDTS edtsBox; + MP4::ELST elstBox; + elstBox.setVersion(0); + elstBox.setFlags(0); + elstBox.setCount(1); + elstBox.setSegmentDuration(thisTrack.lastms - thisTrack.firstms); + elstBox.setMediaTime(0); + elstBox.setMediaRateInteger(1); + elstBox.setMediaRateFraction(0); + edtsBox.setContent(elstBox, 0); + trakBox.setContent(edtsBox, trakOffset++); + + MP4::MDIA mdiaBox; + unsigned int mdiaOffset = 0; + + //Add the mandatory MDHD and HDLR boxes to the MDIA + MP4::MDHD mdhdBox(thisTrack.lastms - thisTrack.firstms); + mdiaBox.setContent(mdhdBox, mdiaOffset++); + MP4::HDLR hdlrBox(thisTrack.type, thisTrack.getIdentifier()); + mdiaBox.setContent(hdlrBox, mdiaOffset++); + + MP4::MINF minfBox; + unsigned int minfOffset = 0; + + //Add a track-type specific box to the MINF box + if (thisTrack.type == "video") { + MP4::VMHD vmhdBox; + vmhdBox.setFlags(1); + minfBox.setContent(vmhdBox, minfOffset++); + } else if (thisTrack.type == "audio") { + MP4::SMHD smhdBox; + minfBox.setContent(smhdBox, minfOffset++); + } + + //Add the mandatory DREF (dataReference) box + MP4::DINF dinfBox; + MP4::DREF drefBox; + dinfBox.setContent(drefBox, 0); + minfBox.setContent(dinfBox, minfOffset++); + + + MP4::STBL stblBox; + unsigned int stblOffset = 0; + + //Add STSD box + MP4::STSD stsdBox(0); + if (thisTrack.type == "video") { + MP4::VisualSampleEntry sampleEntry(thisTrack); + stsdBox.setEntry(sampleEntry, 0); + } else if (thisTrack.type == "audio") { + MP4::AudioSampleEntry sampleEntry(thisTrack); + stsdBox.setEntry(sampleEntry, 0); + } + stblBox.setContent(stsdBox, stblOffset++); + + //Add STTS Box + //note: STTS is empty when fragmented + MP4::STTS sttsBox(0); + if (!fragmented) { + std::deque > sttsCounter; + for (unsigned int part = 0; part < thisTrack.parts.size(); ++part) { + //Create a new entry with current duration if EITHER there is no entry yet, or this parts duration differs from the previous + if (!sttsCounter.size() || sttsCounter.rbegin()->second != thisTrack.parts[part].getDuration()){ + //Set the counter to 0, so we don't have to handle this situation diffent when updating + sttsCounter.push_back(std::pair(0, thisTrack.parts[part].getDuration())); + } + //Then update the counter + sttsCounter.rbegin()->first++; } - }//trak Box + + //Write all entries in reverse + for (unsigned int entry = sttsCounter.size(); entry > 0; --entry){ + MP4::STTSEntry newEntry; + newEntry.sampleCount = sttsCounter[entry - 1].first;; + newEntry.sampleDelta = sttsCounter[entry - 1].second; + sttsBox.setSTTSEntry(newEntry, entry - 1);///\todo rewrite for sanity + } + } + stblBox.setContent(sttsBox, stblOffset++); + + //Add STSS Box IF type is video and we are not fragmented + if (thisTrack.type == "video" && !fragmented) { + MP4::STSS stssBox(0); + int tmpCount = 0; + for (int i = 0; i < thisTrack.keys.size(); i++){ + stssBox.setSampleNumber(tmpCount + 1, i);///\todo PLEASE rewrite this for sanity.... SHOULD be: index FIRST, value SECOND + tmpCount += thisTrack.keys[i].getParts(); + } + stblBox.setContent(stssBox, stblOffset++); + } + + //Add STSC Box + //note: STSC is empty when fragmented + MP4::STSC stscBox(0); + if (!fragmented) { + MP4::STSCEntry stscEntry(1,1,1); + stscBox.setSTSCEntry(stscEntry, 0); + } + stblBox.setContent(stscBox, stblOffset++); + + bool containsOffsets = false; + + //Add STSZ Box + //note: STSZ is empty when fragmented + MP4::STSZ stszBox(0); + if (!fragmented) { + if (thisTrack.parts.size()) { + std::deque::reverse_iterator tmpIt = thisTrack.parts.rbegin(); + for (unsigned int part = thisTrack.parts.size(); part > 0; --part) { + ///\todo rewrite for sanity + stszBox.setEntrySize(tmpIt->getSize(), part - 1); //in bytes in file + size += tmpIt->getSize(); + containsOffsets |= tmpIt->getOffset(); + tmpIt++; + } + } + } + stblBox.setContent(stszBox, stblOffset++); + + //Add CTTS Box only if the track contains time offsets + //note: CTTS will never exist in fragmented, since containsOffsets is set while generating the STSZ box + if (containsOffsets) { + MP4::CTTS cttsBox; + cttsBox.setVersion(0); + + MP4::CTTSEntry tmpEntry; + tmpEntry.sampleCount = 0; + tmpEntry.sampleOffset = thisTrack.parts[0].getOffset(); + unsigned int totalEntries = 0; + for (std::deque::iterator tmpIt = thisTrack.parts.begin(); tmpIt != thisTrack.parts.end(); tmpIt++){ + if (tmpIt->getOffset() != tmpEntry.sampleOffset) { + //If the offset of this and previous part differ, write current values and reset + cttsBox.setCTTSEntry(tmpEntry, totalEntries++);///\todo Again, rewrite for sanity. index FIRST, value SECOND + tmpEntry.sampleCount = 0; + tmpEntry.sampleOffset = tmpIt->getOffset(); + } + tmpEntry.sampleCount++; + } + //set the last entry + cttsBox.setCTTSEntry(tmpEntry, totalEntries++); + stblBox.setContent(cttsBox, stblOffset++); + } + + + //Create STCO Box (either stco or co64) + //note: 64bit boxes will never be used in fragmented + //note: Inserting empty values on purpose here, will be fixed later. + if (useLargeBoxes) { + MP4::CO64 CO64Box; + CO64Box.setChunkOffset(0, thisTrack.parts.size() - 1); + stblBox.setContent(CO64Box, stblOffset++); + } else { + MP4::STCO stcoBox(0); + if (fragmented) { + stcoBox.setEntryCount(0); + } else { + stcoBox.setChunkOffset(0, thisTrack.parts.size() - 1); + } + stblBox.setContent(stcoBox, stblOffset++); + } + + minfBox.setContent(stblBox, minfOffset++); + + mdiaBox.setContent(minfBox, mdiaOffset++); + + trakBox.setContent(mdiaBox, 2); + moovBox.setContent(trakBox, moovOffset++); - }//for each selected track - //initial offset length ftyp, length moov + 8 - unsigned long long int byteOffset = ftypBox.boxedSize() + moovBox.boxedSize() + 8; - //update all STCO or CO64 from the following maps; - std::map checkStcoBoxes; - std::map checkCO64Boxes; - //for all tracks - for (unsigned int i = 1; i < moovBox.getContentCount(); i++){ - //10 lines to get the STCO box. - MP4::TRAK checkTrakBox; - MP4::Box checkMdiaBox; - MP4::Box checkTkhdBox; - MP4::MINF checkMinfBox; - MP4::STBL checkStblBox; - //MP4::STCO checkStcoBox; - checkTrakBox = ((MP4::TRAK&)moovBox.getContent(i)); - for (unsigned int j = 0; j < checkTrakBox.getContentCount(); j++){ - if (checkTrakBox.getContent(j).isType("mdia")){ - checkMdiaBox = checkTrakBox.getContent(j); - break; - } - if (checkTrakBox.getContent(j).isType("tkhd")){ - checkTkhdBox = checkTrakBox.getContent(j); - } - } - for (unsigned int j = 0; j < ((MP4::MDIA&)checkMdiaBox).getContentCount(); j++){ - if (((MP4::MDIA&)checkMdiaBox).getContent(j).isType("minf")){ - checkMinfBox = ((MP4::MINF&)((MP4::MDIA&)checkMdiaBox).getContent(j)); - break; - } - } - for (unsigned int j = 0; j < checkMinfBox.getContentCount(); j++){ - if (checkMinfBox.getContent(j).isType("stbl")){ - checkStblBox = ((MP4::STBL&)checkMinfBox.getContent(j)); - break; - } - } - for (unsigned int j = 0; j < checkStblBox.getContentCount(); j++){ - if (checkStblBox.getContent(j).isType("stco")){ - checkStcoBoxes.insert( std::pair(((MP4::TKHD&)checkTkhdBox).getTrackID(), ((MP4::STCO&)checkStblBox.getContent(j)) )); - break; - } - if (checkStblBox.getContent(j).isType("co64")){ - checkCO64Boxes.insert( std::pair(((MP4::TKHD&)checkTkhdBox).getTrackID(), ((MP4::CO64&)checkStblBox.getContent(j)) )); - break; - } - } - } - //inserting right values in the STCO box header - //total = 0; - long long unsigned int totalByteOffset = 0; - //Current values are actual byte offset without header-sized offset - std::set sortSet;//filling sortset for interleaving parts - for (std::set::iterator subIt = selectedTracks.begin(); subIt != selectedTracks.end(); subIt++) { - keyPart temp; - temp.trackID = *subIt; - temp.time = myMeta.tracks[*subIt].firstms;//timeplace of frame - temp.endTime = myMeta.tracks[*subIt].firstms + myMeta.tracks[*subIt].parts[0].getDuration(); - temp.size = myMeta.tracks[*subIt].parts[0].getSize();//bytesize of frame (alle parts all together) - temp.index = 0; - sortSet.insert(temp); - } - while (!sortSet.empty()){ - std::set::iterator keyBegin = sortSet.begin(); - //setting the right STCO size in the STCO box - if (checkCO64Boxes.count(keyBegin->trackID)){ - checkCO64Boxes[keyBegin->trackID].setChunkOffset(totalByteOffset + byteOffset, keyBegin->index); - }else{ - checkStcoBoxes[keyBegin->trackID].setChunkOffset(totalByteOffset + byteOffset, keyBegin->index); - } - totalByteOffset += keyBegin->size; - //add keyPart to sortSet - keyPart temp; - temp.index = keyBegin->index + 1; - temp.trackID = keyBegin->trackID; - DTSC::Track & thisTrack = myMeta.tracks[temp.trackID]; - if(temp.index < thisTrack.parts.size() ){//only insert when there are parts left - temp.time = keyBegin->endTime;//timeplace of frame - temp.endTime = keyBegin->endTime + thisTrack.parts[temp.index].getDuration(); - temp.size = thisTrack.parts[temp.index].getSize();//bytesize of frame - sortSet.insert(temp); - } - //remove highest keyPart - sortSet.erase(keyBegin); } - mdatSize = totalByteOffset+8; - - header.write(moovBox.asBox(),moovBox.boxedSize()); - - header << (char)((mdatSize>>24) & 0xFF) << (char)((mdatSize>>16) & 0xFF) << (char)((mdatSize>>8) & 0xFF) << (char)(mdatSize & 0xFF) << "mdat"; - //end of header - + if (fragmented) { + MP4::MVEX mvexBox; + unsigned int curBox = 0; + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + MP4::TREX trexBox(*it); + mvexBox.setContent(trexBox, curBox++); + } + moovBox.setContent(mvexBox, moovOffset++); + }else{ //if we are making a non fragmented MP4 and there are parts + //initial offset length ftyp, length moov + 8 + unsigned long long int dataOffset = ftypBox.boxedSize() + moovBox.boxedSize() + 8; + //update all STCO or CO64 from the following maps; + std::map checkStcoBoxes; + std::map checkCO64Boxes; + //for all tracks + for (unsigned int i = 1; i < moovBox.getContentCount(); i++) { + //10 lines to get the STCO box. + MP4::TRAK checkTrakBox; + MP4::Box checkMdiaBox; + MP4::Box checkTkhdBox; + MP4::MINF checkMinfBox; + MP4::STBL checkStblBox; + //MP4::STCO checkStcoBox; + checkTrakBox = ((MP4::TRAK &)moovBox.getContent(i)); + for (unsigned int j = 0; j < checkTrakBox.getContentCount(); j++) { + if (checkTrakBox.getContent(j).isType("mdia")) { + checkMdiaBox = checkTrakBox.getContent(j); + break; + } + if (checkTrakBox.getContent(j).isType("tkhd")) { + checkTkhdBox = checkTrakBox.getContent(j); + } + } + for (unsigned int j = 0; j < ((MP4::MDIA &)checkMdiaBox).getContentCount(); j++) { + if (((MP4::MDIA &)checkMdiaBox).getContent(j).isType("minf")) { + checkMinfBox = ((MP4::MINF &)((MP4::MDIA &)checkMdiaBox).getContent(j)); + break; + } + } + for (unsigned int j = 0; j < checkMinfBox.getContentCount(); j++) { + if (checkMinfBox.getContent(j).isType("stbl")) { + checkStblBox = ((MP4::STBL &)checkMinfBox.getContent(j)); + break; + } + } + for (unsigned int j = 0; j < checkStblBox.getContentCount(); j++) { + if (checkStblBox.getContent(j).isType("stco")) { + checkStcoBoxes.insert(std::pair(((MP4::TKHD &)checkTkhdBox).getTrackID(), ((MP4::STCO &)checkStblBox.getContent(j)))); + break; + } + if (checkStblBox.getContent(j).isType("co64")) { + checkCO64Boxes.insert(std::pair(((MP4::TKHD &)checkTkhdBox).getTrackID(), ((MP4::CO64 &)checkStblBox.getContent(j)))); + break; + } + } + } + //inserting right values in the STCO box header + //total = 0; + //Keep track of the current size of the data within the mdat + long long unsigned int dataSize = 0; + //Current values are actual byte offset without header-sized offset + std::set sortSet;//filling sortset for interleaving parts + for (std::set::iterator subIt = selectedTracks.begin(); subIt != selectedTracks.end(); subIt++) { + keyPart temp; + temp.trackID = *subIt; + temp.time = myMeta.tracks[*subIt].firstms;//timeplace of frame + temp.endTime = myMeta.tracks[*subIt].firstms + myMeta.tracks[*subIt].parts[0].getDuration(); + temp.size = myMeta.tracks[*subIt].parts[0].getSize();//bytesize of frame (alle parts all together) + temp.index = 0; + INFO_MSG("adding to sortSet: tid %lu time %llu", temp.trackID, temp.time); + sortSet.insert(temp); + } + while (!sortSet.empty()) { + std::set::iterator keyBegin = sortSet.begin(); + //setting the right STCO size in the STCO box + if (useLargeBoxes){//Re-using the previously defined boolean for speedup + checkCO64Boxes[keyBegin->trackID].setChunkOffset(dataOffset + dataSize, keyBegin->index); + } else { + checkStcoBoxes[keyBegin->trackID].setChunkOffset(dataOffset + dataSize, keyBegin->index); + } + dataSize += keyBegin->size; + + //add next keyPart to sortSet + DTSC::Track & thisTrack = myMeta.tracks[keyBegin->trackID]; + if (keyBegin->index < thisTrack.parts.size() - 1) {//Only create new element, when there are new elements to be added + keyPart temp = *keyBegin; + temp.index ++; + temp.time = temp.endTime; + temp.endTime += thisTrack.parts[temp.index].getDuration(); + temp.size = thisTrack.parts[temp.index].getSize();//bytesize of frame + sortSet.insert(temp); + } + //remove highest keyPart + sortSet.erase(keyBegin); + } + + ///\todo Update this thing for boxes >4G? + mdatSize = dataSize + 8;//+8 for mp4 header + + } + header << std::string(moovBox.asBox(), moovBox.boxedSize()); + + + if (!fragmented) { //if we are making a non fragmented MP4 and there are parts + char mdatHeader[8] = {0x00,0x00,0x00,0x00,'m','d','a','t'}; + Bit::htobl(mdatHeader, mdatSize); + header.write(mdatHeader, 8); + } else { + //this is a dirty fix to prevent the code from adding 0xDE to the end of the header + header << (char)(0); + } + size += header.str().size(); + if (fragmented) { + realBaseOffset = header.str().size(); + } return header.str(); } - + /// Calculate a seekPoint, based on byteStart, metadata, tracks and headerSize. /// The seekPoint will be set to the timestamp of the first packet to send. - void OutProgressiveMP4::findSeekPoint(long long byteStart, long long & seekPoint, unsigned int headerSize){ + void OutProgressiveMP4::findSeekPoint(long long byteStart, long long & seekPoint, unsigned int headerSize) { seekPoint = 0; //if we're starting in the header, seekPoint is always zero. - if (byteStart <= headerSize){return;} + if (byteStart <= headerSize) { + return; + } //okay, we're past the header. Substract the headersize from the starting postion. byteStart -= headerSize; //forward through the file by headers, until we reach the point where we need to be - while (!sortSet.empty()){ + while (!sortSet.empty()) { //record where we are seekPoint = sortSet.begin()->time; //substract the size of this fragment from byteStart byteStart -= sortSet.begin()->size; //if that put us past the point where we wanted to be, return right now - if (byteStart < 0){return;} + if (byteStart < 0) { + INFO_MSG("Seeked to tid: %lu t: %llu ", sortSet.begin()->trackID, sortSet.begin()->time); + return; + } //otherwise, set currPos to where we are now and continue currPos += sortSet.begin()->size; //find the next part keyPart temp; temp.index = sortSet.begin()->index + 1; temp.trackID = sortSet.begin()->trackID; - if(temp.index < myMeta.tracks[temp.trackID].parts.size() ){//only insert when there are parts left + if (temp.index < myMeta.tracks[temp.trackID].parts.size()) { //only insert when there are parts left temp.time = sortSet.begin()->endTime;//timeplace of frame temp.endTime = sortSet.begin()->endTime + myMeta.tracks[temp.trackID].parts[temp.index].getDuration(); - temp.size = myMeta.tracks[temp.trackID].parts[temp.index].getSize();//bytesize of frame + temp.size = myMeta.tracks[temp.trackID].parts[temp.index].getSize();//bytesize of frame sortSet.insert(temp); } //remove highest keyPart @@ -387,69 +423,71 @@ namespace Mist { //If we're here, we're in the last fragment. //That's technically legal, of course. } - - /// Parses a "Range: " header, setting byteStart, byteEnd and seekPoint using data from metadata and tracks to do - /// the calculations. - /// On error, byteEnd is set to zero. - void OutProgressiveMP4::parseRange(std::string header, long long & byteStart, long long & byteEnd, long long & seekPoint, unsigned int headerSize){ - if (header.size() < 6 || header.substr(0, 6) != "bytes="){ + +/// Parses a "Range: " header, setting byteStart, byteEnd and seekPoint using data from metadata and tracks to do +/// the calculations. +/// On error, byteEnd is set to zero. + void OutProgressiveMP4::parseRange(std::string header, long long & byteStart, long long & byteEnd, long long & seekPoint, unsigned int headerSize) { + if (header.size() < 6 || header.substr(0, 6) != "bytes=") { byteEnd = 0; DEBUG_MSG(DLVL_WARN, "Invalid range header: %s", header.c_str()); return; } header.erase(0, 6); - if (header.size() && header[0] == '-'){ + if (header.size() && header[0] == '-') { //negative range = count from end byteStart = 0; - for (unsigned int i = 1; i < header.size(); ++i){ - if (header[i] >= '0' && header[i] <= '9'){ + for (unsigned int i = 1; i < header.size(); ++i) { + if (header[i] >= '0' && header[i] <= '9') { byteStart *= 10; byteStart += header[i] - '0'; continue; } break; } - if (byteStart > byteEnd){ + if (byteStart > byteEnd) { //entire file if starting before byte zero byteStart = 0; findSeekPoint(byteStart, seekPoint, headerSize); return; - }else{ + } else { //start byteStart bytes before byteEnd byteStart = byteEnd - byteStart; findSeekPoint(byteStart, seekPoint, headerSize); return; } - }else{ + } else { long long size = byteEnd; byteEnd = 0; byteStart = 0; unsigned int i = 0; - for ( ; i < header.size(); ++i){ - if (header[i] >= '0' && header[i] <= '9'){ + for (; i < header.size(); ++i) { + if (header[i] >= '0' && header[i] <= '9') { byteStart *= 10; byteStart += header[i] - '0'; continue; } break; } - if (header[i] != '-'){ + if (header[i] != '-') { DEBUG_MSG(DLVL_WARN, "Invalid range header: %s", header.c_str()); byteEnd = 0; return; } ++i; - if (i < header.size()){ - for ( ; i < header.size(); ++i){ - if (header[i] >= '0' && header[i] <= '9'){ + if (i < header.size()) { + for (; i < header.size(); ++i) { + if (header[i] >= '0' && header[i] <= '9') { byteEnd *= 10; byteEnd += header[i] - '0'; continue; } break; } - if (byteEnd > size - 1){byteEnd = size - 1;} - }else{ + if (byteEnd > size - 1) { + byteEnd = size - 1; + } + } else { byteEnd = size; } DEBUG_MSG(DLVL_MEDIUM, "Range request: %lli-%lli (%s)", byteStart, byteEnd, header.c_str()); @@ -457,12 +495,119 @@ namespace Mist { return; } } - - void OutProgressiveMP4::onHTTP(){ + + void OutProgressiveMP4::sendFragmentHeader(int fragNum) { + long unsigned int dataOffset = 0; + uint64_t mdatSize = 8; + MP4::MOOF moofBox; + MP4::MFHD mfhdBox; + mfhdBox.setSequenceNumber(fragNum); + moofBox.setContent(mfhdBox, 0); + unsigned int moofIndex = 1; + std::vector trunOrderWithOffset; + + //sort all parts here + std::set trunOrder; + //set with trackID, relative data offset, time and size + for (std::map::iterator it = currentPartSet.begin(); it != currentPartSet.end(); it++) { + long unsigned int timeStamp = it->second.firstTime; + DTSC::Track & thisTrack = myMeta.tracks[it->first]; + for (long unsigned int i = it->second.firstPart; i <= it->second.lastPart; i++) { + keyPart temp; + temp.trackID = it->first; + temp.size = thisTrack.parts[i].getSize(); + temp.duration = thisTrack.parts[i].getDuration(); + temp.time = timeStamp; + timeStamp += temp.duration; + temp.endTime = timeStamp; + temp.timeOffset = thisTrack.parts[i].getOffset();//this will be changed soon, so now it is used for B-frame offset + trunOrder.insert(temp); + } + } + + //now all the parts have been sorted, we make a relative ByteOffset + long unsigned int relativeOffset = 0; + for (std::set::iterator it = trunOrder.begin(); it != trunOrder.end(); it++) { + //We have to make a copy, because altering the element inside the set would invalidate the iterators + keyPart temp = *it; + temp.byteOffset = relativeOffset; + relativeOffset += it->size; + DONTEVEN_MSG("Anticipating tid: %lu size: %lu", it->trackID, it->size); + trunOrderWithOffset.push_back(temp); + } + trunOrder.clear();//erase the trunOrder set, to keep memory usage down + + + bool firstSample = true; + for (std::map::iterator it = currentPartSet.begin(); it != currentPartSet.end(); it++) { + unsigned int tid = it->first; + DTSC::Track & thisTrack = myMeta.tracks[tid]; + MP4::TRAF trafBox; + MP4::TFHD tfhdBox; + tfhdBox.setFlags(MP4::tfhdBaseOffset | MP4::tfhdSampleDura | MP4::tfhdSampleSize | MP4::tfhdSampleFlag); + tfhdBox.setTrackID(tid); + tfhdBox.setBaseDataOffset(realBaseOffset - 1); //Offset of current moof box, we use currPos for this. Not sure why we need the -1, but this gives the right offset + tfhdBox.setDefaultSampleDuration(thisTrack.parts[it->second.firstPart].getDuration()); + tfhdBox.setDefaultSampleSize(thisTrack.parts[it->second.firstPart].getSize()); + if (tid == vidTrack) { + tfhdBox.setDefaultSampleFlags(MP4::noIPicture | MP4::noKeySample); + } else { + tfhdBox.setDefaultSampleFlags(MP4::isIPicture | MP4::isKeySample); + } + trafBox.setContent(tfhdBox, 0); + + unsigned int trafOffset = 1; + for (std::vector::iterator trunIt = trunOrderWithOffset.begin(); trunIt != trunOrderWithOffset.end(); trunIt++) { + if (trunIt->trackID == tid) { + MP4::TRUN trunBox; + trunBox.setFlags(MP4::trundataOffset | MP4::trunfirstSampleFlags | MP4::trunsampleSize | MP4::trunsampleDuration | (trunIt->timeOffset ? MP4::trunsampleOffsets : 0)); + //The value set here, will be updated afterwards to the correct value + trunBox.setDataOffset(trunIt->byteOffset); + trunBox.setFirstSampleFlags(MP4::isIPicture | (firstSample ? MP4::isKeySample : MP4::noKeySample)); + firstSample = false; + + mdatSize += trunIt->size; + + MP4::trunSampleInformation sampleInfo; + sampleInfo.sampleSize = trunIt->size; + sampleInfo.sampleDuration = trunIt->duration; + if (trunIt->timeOffset) { + sampleInfo.sampleOffset = trunIt->timeOffset; + } + trunBox.setSampleInformation(sampleInfo, 0); + trafBox.setContent(trunBox, trafOffset++); + } + } + moofBox.setContent(trafBox, moofIndex); + moofIndex++; + } + + //Update the trun data offsets with their correct values + MP4::TRAF loopTrafBox; + MP4::TRUN fixTrunBox; + for (unsigned int i = 0; i < moofBox.getContentCount(); i++) { + if (moofBox.getContent(i).isType("traf")) { + loopTrafBox = ((MP4::TRAF &)moofBox.getContent(i)); + for (unsigned int j = 0; j < loopTrafBox.getContentCount(); j++) { + if (loopTrafBox.getContent(j).isType("trun")) { + fixTrunBox = ((MP4::TRUN &)loopTrafBox.getContent(j)); + fixTrunBox.setDataOffset(fixTrunBox.getDataOffset() + moofBox.boxedSize() + 8); + } + } + } + } + realBaseOffset += (moofBox.boxedSize() + mdatSize); + myConn.SendNow(moofBox.asBox(), moofBox.boxedSize()); + char mdatHeader[8] = {0x00,0x00,0x00,0x00,'m','d','a','t'}; + Bit::htobl(mdatHeader, mdatSize); + myConn.SendNow(mdatHeader, 8); + } + + void OutProgressiveMP4::onHTTP() { /*LTS-START*/ //allow setting of max lead time through buffer variable. //max lead time is set in MS, but the variable is in integer seconds for simplicity. - if (H.GetVar("buffer") != ""){ + if (H.GetVar("buffer") != "") { maxSkipAhead = JSON::Value(H.GetVar("buffer")).asInt() * 1000; minSkipAhead = maxSkipAhead - std::min(2500u, maxSkipAhead / 2); } @@ -477,15 +622,48 @@ namespace Mist { } } /*LTS-END*/ + //Always initialize before anything else initialize(); + + //Make sure we start receiving data after this function + ///\todo Should this happen here? parseData = true; wantRequest = false; sentHeader = false; - fileSize = 0; - std::string headerData = DTSCMeta2MP4Header(fileSize); + + //Check if the url contains .3gp --> if yes, we will send a 3gp header + sending3GP = (H.url.find(".3gp") != std::string::npos); + + //For storing the header. + ///\todo Do we really need this though? + std::string headerData = DTSCMeta2MP4Header(fileSize, myMeta.live); + + seekPoint = 0; + if (myMeta.live) { + realTime = 0; + //for live we use fragmented mode + fragSeqNum = 0; + partListSent = 0; + partListLength = 0; + //seek to first video keyframe here + setvidTrack(); + //making sure we have a first keyframe + if (!vidTrack || !myMeta.tracks[vidTrack].keys.size() || !myMeta.tracks[vidTrack].keys.begin()->getLength()) { + WARN_MSG("Stream not ready yet"); + myConn.close(); + parseData = false; + return; + } + std::deque::reverse_iterator fromHere = myMeta.tracks[vidTrack].keys.rbegin(); + if (myMeta.tracks[vidTrack].keys.size() > 1) { + fromHere++; + } + ///\todo Note: Not necessary, but we might want to think of a method that does not use seeking + seekPoint = fromHere->getTime(); + fragKeyNumberShift = fromHere->getNumber() - 1; + } byteStart = 0; byteEnd = fileSize - 1; - seekPoint = 0; char rangeType = ' '; currPos = 0; sortSet.clear(); @@ -498,30 +676,35 @@ namespace Mist { temp.index = 0; sortSet.insert(temp); } - if (H.GetHeader("Range") != ""){ - parseRange(H.GetHeader("Range"), byteStart, byteEnd, seekPoint, headerData.size()); - rangeType = H.GetHeader("Range")[0]; + if (!myMeta.live) { + INFO_MSG("notlive get range %s", H.GetHeader("Range").c_str()); + if (H.GetHeader("Range") != "") { + parseRange(H.GetHeader("Range"), byteStart, byteEnd, seekPoint, headerData.size()); + rangeType = H.GetHeader("Range")[0]; + } } H.Clean(); //make sure no parts of old requests are left in any buffers H.SetHeader("Content-Type", "video/MP4"); //Send the correct content-type for MP4 files - H.SetHeader("Accept-Ranges", "bytes, parsec"); - if (rangeType != ' '){ - if (!byteEnd){ - if (rangeType == 'p'){ + if (!myMeta.live) { + H.SetHeader("Accept-Ranges", "bytes, parsec"); + } + if (rangeType != ' ') { + if (!byteEnd) { + if (rangeType == 'p') { H.SetBody("Starsystem not in communications range"); H.SendResponse("416", "Starsystem not in communications range", myConn); return; - }else{ + } else { H.SetBody("Requested Range Not Satisfiable"); H.SendResponse("416", "Requested Range Not Satisfiable", myConn); return; } - }else{ + } else { std::stringstream rangeReply; rangeReply << "bytes " << byteStart << "-" << byteEnd << "/" << fileSize; H.SetHeader("Content-Length", byteEnd - byteStart + 1); //do not multiplex requests that are > 1MiB - if (byteEnd - byteStart + 1 > 1024*1024){ + if (byteEnd - byteStart + 1 > 1024 * 1024) { H.SetHeader("MistMultiplex", "No"); } H.SetHeader("Content-Range", rangeReply.str()); @@ -529,8 +712,10 @@ namespace Mist { H.SendResponse("206", "Partial content", myConn); //H.StartResponse("206", "Partial content", HTTP_R, conn); } - }else{ - H.SetHeader("Content-Length", byteEnd - byteStart + 1); + } else { + if (!myMeta.live) { + H.SetHeader("Content-Length", byteEnd - byteStart + 1); + } //do not multiplex requests that aren't ranged H.SetHeader("MistMultiplex", "No"); /// \todo Switch to chunked? @@ -538,71 +723,186 @@ namespace Mist { //HTTP_S.StartResponse(HTTP_R, conn); } leftOver = byteEnd - byteStart + 1;//add one byte, because range "0-0" = 1 byte of data - if (byteStart < (long long)headerData.size()){ + if (byteStart < (long long)headerData.size()) { /// \todo Switch to chunked? - //H.Chunkify(headerData.data()+byteStart, std::min((long long)headerData.size(), byteEnd) - byteStart, conn);//send MP4 header - myConn.SendNow(headerData.data()+byteStart, std::min((long long)headerData.size(), byteEnd) - byteStart);//send MP4 header + myConn.SendNow(headerData.data() + byteStart, std::min((long long)headerData.size(), byteEnd) - byteStart); //send MP4 header leftOver -= std::min((long long)headerData.size(), byteEnd) - byteStart; } currPos += headerData.size();//we're now guaranteed to be past the header point, no matter what } - - void OutProgressiveMP4::sendNext(){ + + void OutProgressiveMP4::setvidTrack() { + vidTrack = 0; + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + //Find video track + if (myMeta.tracks[*it].type == "video") { + vidTrack = *it; + break; + } + } + } + +///Builds up a datastructure that allows for access in the fragment send header function +///using the fragment number **FOR THIS USER, NOT ACTUAL FRAGMENT NUMBER, HAS NOTHING TO DO WITH ACTUAL FRAGMENTS EVEN** +///We take the corresponding keyframe and interframes of the main video track and take concurrent frames from its secondary (audio) tracks +///\todo See if we can use something more elegant than a member variable... + void OutProgressiveMP4::buildFragment(int fragNum) { + currentPartSet.clear(); + DTSC::Track & mainTrack = myMeta.tracks[vidTrack]; + + long int keyIndex = fragNum + fragKeyNumberShift - (mainTrack.keys.begin()->getNumber() - 1); //here we set the index of the video keyframe we are going to make a fragment of + + if (keyIndex < 0 || keyIndex >= mainTrack.keys.size()) {//if the fragnum is not in the keys + FAIL_MSG("Fragment Number %d not available. KeyShift: %ld FirstKeyNumber: %lu, Calculated KeyIndex: %ld, KeysInMeta: %lu", fragNum, fragKeyNumberShift, mainTrack.keys.begin()->getNumber(), keyIndex, mainTrack.keys.size()); + INSANE_MSG("Current Time: %llu, Current TrackID: %ld", thisPacket.getTime(), thisPacket.getTrackId()); + INSANE_MSG("Rbegin Number: %lu, Rbegin Time %llu, rBegin Length %lu", mainTrack.keys.rbegin()->getNumber(), mainTrack.keys.rbegin()->getTime(), mainTrack.keys.rbegin()->getLength()); + } else { + INSANE_MSG("Fragment Number %d OK. KeyShift: %ld FirstKeyNumber: %lu, Calculated KeyIndex: %ld, KeysInMeta: %lu", fragNum, fragKeyNumberShift, mainTrack.keys.begin()->getNumber(), keyIndex, mainTrack.keys.size()); + } + + long long int startms = mainTrack.keys[keyIndex].getTime(); + long long int endms;// = startms; + if (mainTrack.keys.size() > keyIndex + 1) { + endms = mainTrack.keys[keyIndex + 1].getTime(); + } else { + endms = mainTrack.lastms; + } + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++) { + DTSC::Track & thisTrack = myMeta.tracks[*it]; + fragSet thisRange; + thisRange.firstPart = 0; + thisRange.firstTime = thisTrack.keys.begin()->getTime(); + unsigned long long int prevParts = 0; + for (std::deque::iterator it2 = thisTrack.keys.begin(); it2 != thisTrack.keys.end(); it2++) { + if (it2->getTime() > startms) { + break; + } + thisRange.firstPart += prevParts; + prevParts = it2->getParts(); + thisRange.firstTime = it2->getTime(); + } + thisRange.lastPart = thisRange.firstPart; + thisRange.lastTime = thisRange.firstTime; + unsigned int curMS = thisRange.firstTime; + unsigned int nextMS = thisRange.firstTime; + bool first = true; + for (int i = thisRange.firstPart; i < thisTrack.parts.size(); i++) { + if (first && curMS >= startms) { + thisRange.firstPart = i; + thisRange.firstTime = curMS; + first = false; + } + nextMS = curMS + thisTrack.parts[i].getDuration(); + if (nextMS >= endms) { + thisRange.lastPart = i; + thisRange.lastTime = curMS; + break; + } + curMS = nextMS; + } + currentPartSet[*it] = thisRange; + } + } + + void OutProgressiveMP4::buildTrafPart() { + updateMeta();//we need to update meta + //building set first + buildFragment(fragSeqNum);//map with metadata for keyframe + if (currentPartSet.size()) { + sendFragmentHeader(fragSeqNum++); + partListSent = 0; + //convert map to list here, apologies for inefficiency, but this works best + //partList = x1 * track y1 + x2 * track y2 * etc. + partListLength = 0; + //std::stringstream temp; + for (std::map::iterator it = currentPartSet.begin(); it != currentPartSet.end(); it++) { + partListLength += it->second.lastPart - it->second.firstPart + 1; + } + } else { + WARN_MSG("Warning: partMap should not be empty, but it is! Possibly source stopped streaming"); + myConn.close(); + } + } + + void OutProgressiveMP4::sendNext() { static bool perfect = true; + + //Obtain a pointer to the data of this packet char * dataPointer = 0; unsigned int len = 0; thisPacket.getString("data", dataPointer, len); - if ((unsigned long)thisPacket.getTrackId() != sortSet.begin()->trackID || thisPacket.getTime() != sortSet.begin()->time){ - if (thisPacket.getTime() >= sortSet.begin()->time || (unsigned long)thisPacket.getTrackId() >= sortSet.begin()->trackID){ - if (perfect){ + + if (myMeta.live) { + //if header needed + if (!partListLength || partListSent >= partListLength) { + buildTrafPart(); + } + //generate content in mdat, meaning: send right parts + DONTEVEN_MSG("Sending tid: %ld size: %u", thisPacket.getTrackId() , len); + myConn.SendNow(dataPointer, len); + partListSent++; + return; + } + + + //The remainder of this function handles non-live situations + if ((unsigned long)thisPacket.getTrackId() != sortSet.begin()->trackID || thisPacket.getTime() != sortSet.begin()->time) { + if (thisPacket.getTime() > sortSet.begin()->time || (unsigned long)thisPacket.getTrackId() > sortSet.begin()->trackID) { + if (perfect) { DEBUG_MSG(DLVL_WARN, "Warning: input is inconsistent. Expected %lu:%llu but got %ld:%llu - cancelling playback", sortSet.begin()->trackID, sortSet.begin()->time, thisPacket.getTrackId(), thisPacket.getTime()); + INFO_MSG("myMeta live: %d", myMeta.live); perfect = false; myConn.close(); } - }else{ + } else { DEBUG_MSG(DLVL_HIGH, "Did not receive expected %lu:%llu but got %ld:%llu - throwing it away", sortSet.begin()->trackID, sortSet.begin()->time, thisPacket.getTrackId(), thisPacket.getTime()); } return; } + //keep track of where we are - if (!sortSet.empty()){ + if (!sortSet.empty()) { keyPart temp; temp.index = sortSet.begin()->index + 1; temp.trackID = sortSet.begin()->trackID; - if(temp.index < myMeta.tracks[temp.trackID].parts.size() ){//only insert when there are parts left + if (temp.index < myMeta.tracks[temp.trackID].parts.size()) { //only insert when there are parts left temp.time = sortSet.begin()->endTime;//timeplace of frame temp.endTime = sortSet.begin()->endTime + myMeta.tracks[temp.trackID].parts[temp.index].getDuration(); - temp.size = myMeta.tracks[temp.trackID].parts[temp.index].getSize();//bytesize of frame + temp.size = myMeta.tracks[temp.trackID].parts[temp.index].getSize();//bytesize of frame sortSet.insert(temp); } currPos += sortSet.begin()->size; //remove highest keyPart sortSet.erase(sortSet.begin()); } - - - if (currPos >= byteStart){ + + + if (currPos >= byteStart) { myConn.SendNow(dataPointer, std::min(leftOver, (long long)len)); - //H.Chunkify(Strm.lastData().data(), Strm.lastData().size(), conn); + leftOver -= len; - }else{ - if (currPos + (long long)len > byteStart){ - myConn.SendNow(dataPointer+(byteStart-currPos), len-(byteStart-currPos)); - leftOver -= len-(byteStart-currPos); + } else { + if (currPos + (long long)len > byteStart) { + myConn.SendNow(dataPointer + (byteStart - currPos), len - (byteStart - currPos)); + leftOver -= len - (byteStart - currPos); currPos = byteStart; } } - //sortSet.clear();//we don't need you anymore! - if (leftOver < 1){ + + if (leftOver < 1) { + //stop playback, wait for new request stop(); wantRequest = true; } } - void OutProgressiveMP4::sendHeader(){ + void OutProgressiveMP4::sendHeader() { seek(seekPoint); + if (myMeta.live) { + setvidTrack(); + } sentHeader = true; } - + } diff --git a/src/output/output_progressive_mp4.h b/src/output/output_progressive_mp4.h index ed9e15e8..dbc1c593 100644 --- a/src/output/output_progressive_mp4.h +++ b/src/output/output_progressive_mp4.h @@ -1,5 +1,6 @@ #include "output_http.h" #include +#include namespace Mist { struct keyPart{ @@ -22,16 +23,28 @@ namespace Mist { long unsigned int size; long long unsigned int time; long long unsigned int endTime; + long long unsigned int byteOffset;//added for MP4 fragmented + long int timeOffset;//added for MP4 fragmented + long unsigned int duration;//added for MP4 fragmented long unsigned int index; }; + struct fragSet{ + long unsigned int firstPart; + long unsigned int lastPart; + long long unsigned int firstTime; + long long unsigned int lastTime; + }; class OutProgressiveMP4 : public HTTPOutput { public: OutProgressiveMP4(Socket::Connection & conn); ~OutProgressiveMP4(); static void init(Util::Config * cfg); void parseRange(std::string header, long long & byteStart, long long & byteEnd, long long & seekPoint, unsigned int headerSize); - std::string DTSCMeta2MP4Header(long long & size); + std::string DTSCMeta2MP4Header(long long & size, int fragmented = 0); + //int fragmented values: 0 = non fragmented stream, 1 = frag stream main header + void buildFragment(int fragNum);//this builds the structure of the fragment header for fragment number fragNum, and stores it in a member variable + void sendFragmentHeader(int fragNum);//this builds the moof box for fragmented MP4 void findSeekPoint(long long byteStart, long long & seekPoint, unsigned int headerSize); void onHTTP(); void sendNext(); @@ -43,9 +56,29 @@ namespace Mist { long long leftOver; long long currPos; long long seekPoint; - std::set sortSet;//filling sortset for interleaving parts + //variables for standard MP4 + std::set sortSet;//needed for unfragmented MP4, remembers the order of keyparts + + //functions for fragmented MP4 + void buildTrafPart(); + void setvidTrack();//searching for the current vid track + + //variables for fragmented + int fragSeqNum;//the sequence number of the next keyframe/fragment when producing fragmented MP4's + long unsigned int vidTrack;//the video track we use as fragmenting base + long long unsigned int realBaseOffset;//base offset for every moof packet + //from sendnext + long unsigned int partListSent;//parts of current fragSet sent + long unsigned int partListLength;//amount of packets in current fragment + long int fragKeyNumberShift;//the difference between the first fragment Number and the first keyframe number + + + bool sending3GP; long long unsigned estimateFileSize(); + + //This is a dirty solution... but it prevents copying and copying and copying again + std::map currentPartSet; }; }