From 2432bbdfc362e0e29c24d9e427c0864227122c27 Mon Sep 17 00:00:00 2001 From: Thulinma Date: Tue, 2 Jul 2019 14:17:01 +0200 Subject: [PATCH] Backported MP4 input from Pro edition --- CMakeLists.txt | 1 + src/input/input_mp4.cpp | 553 ++++++++++++++++++++++++++++++++++++++++ src/input/input_mp4.h | 110 ++++++++ 3 files changed, 664 insertions(+) create mode 100644 src/input/input_mp4.cpp create mode 100644 src/input/input_mp4.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ca0eb25..81b40adf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -312,6 +312,7 @@ makeInput(OGG ogg) makeInput(Buffer buffer) makeInput(H264 h264) makeInput(EBML ebml) +makeInput(MP4 mp4) ######################################## # MistServer - Outputs # diff --git a/src/input/input_mp4.cpp b/src/input/input_mp4.cpp new file mode 100644 index 00000000..c44714b3 --- /dev/null +++ b/src/input/input_mp4.cpp @@ -0,0 +1,553 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "input_mp4.h" + +namespace Mist{ + + mp4TrackHeader::mp4TrackHeader(){ + initialised = false; + stscStart = 0; + sampleIndex = 0; + deltaIndex = 0; + deltaPos = 0; + deltaTotal = 0; + offsetIndex = 0; + offsetPos = 0; + sttsBox.clear(); + hasCTTS = false; + cttsBox.clear(); + stszBox.clear(); + stcoBox.clear(); + co64Box.clear(); + stco64 = false; + } + + uint64_t mp4TrackHeader::size(){ + return (stszBox.asBox() ? stszBox.getSampleCount() : 0); + } + + void mp4TrackHeader::read(MP4::TRAK & trakBox){ + initialised = false; + std::string tmp;//temporary string for copying box data + MP4::Box trakLoopPeek; + timeScale = 1; + + MP4::MDIA mdiaBox = trakBox.getChild(); + + timeScale = mdiaBox.getChild().getTimeScale(); + + MP4::STBL stblBox = mdiaBox.getChild().getChild(); + + sttsBox.copyFrom(stblBox.getChild()); + cttsBox.copyFrom(stblBox.getChild()); + stszBox.copyFrom(stblBox.getChild()); + stcoBox.copyFrom(stblBox.getChild()); + co64Box.copyFrom(stblBox.getChild()); + stscBox.copyFrom(stblBox.getChild()); + stco64 = co64Box.isType("co64"); + hasCTTS = cttsBox.isType("ctts"); + } + + void mp4TrackHeader::getPart(uint64_t index, uint64_t & offset, uint32_t & size, uint64_t & timestamp, int32_t & timeOffset, uint64_t & duration){ + if (index < sampleIndex){ + sampleIndex = 0; + stscStart = 0; + } + + uint64_t stscCount = stscBox.getEntryCount(); + MP4::STSCEntry stscEntry; + while (stscStart < stscCount){ + stscEntry = stscBox.getSTSCEntry(stscStart); + //check where the next index starts + uint64_t nextSampleIndex; + if (stscStart + 1 < stscCount){ + nextSampleIndex = sampleIndex + (stscBox.getSTSCEntry(stscStart+1).firstChunk - stscEntry.firstChunk) * stscEntry.samplesPerChunk; + }else{ + nextSampleIndex = stszBox.getSampleCount(); + } + if (nextSampleIndex > index){ + break; + } + sampleIndex = nextSampleIndex; + ++stscStart; + } + + if (sampleIndex > index){ + FAIL_MSG("Could not complete seek - not in file (%" PRIu64 " > %" PRIu64 ")", sampleIndex, index); + } + + uint64_t stcoPlace = (stscEntry.firstChunk - 1 ) + ((index - sampleIndex) / stscEntry.samplesPerChunk); + uint64_t stszStart = sampleIndex + (stcoPlace - (stscEntry.firstChunk - 1)) * stscEntry.samplesPerChunk; + + offset = (stco64 ? co64Box.getChunkOffset(stcoPlace) : stcoBox.getChunkOffset(stcoPlace)); + for (int j = stszStart; j < index; j++){ + offset += stszBox.getEntrySize(j); + } + + if (index < deltaPos){ + deltaIndex = 0; + deltaPos = 0; + deltaTotal = 0; + } + + MP4::STTSEntry tmpSTTS; + uint64_t sttsCount = sttsBox.getEntryCount(); + while (deltaIndex < sttsCount){ + tmpSTTS = sttsBox.getSTTSEntry(deltaIndex); + if ((index - deltaPos) < tmpSTTS.sampleCount){ + break; + } + deltaTotal += tmpSTTS.sampleCount * tmpSTTS.sampleDelta; + deltaPos += tmpSTTS.sampleCount; + ++deltaIndex; + } + timestamp = ((deltaTotal + ((index-deltaPos) * tmpSTTS.sampleDelta))*1000) / timeScale; + duration = 0; + + { + uint64_t tmpIndex = deltaIndex; + uint64_t tmpPos = deltaPos; + uint64_t tmpTotal = deltaTotal; + while (tmpIndex < sttsCount){ + tmpSTTS = sttsBox.getSTTSEntry(tmpIndex); + if ((index+1 - tmpPos) < tmpSTTS.sampleCount){ + duration = (((tmpTotal + ((index+1-tmpPos) * tmpSTTS.sampleDelta))*1000) / timeScale) - timestamp; + break; + } + tmpTotal += tmpSTTS.sampleCount * tmpSTTS.sampleDelta; + tmpPos += tmpSTTS.sampleCount; + ++tmpIndex; + } + } + + initialised = true; + + if (index < offsetPos){ + offsetIndex = 0; + offsetPos = 0; + } + if (hasCTTS){ + MP4::CTTSEntry tmpCTTS; + uint32_t cttsCount = cttsBox.getEntryCount(); + while (offsetIndex < cttsCount){ + tmpCTTS = cttsBox.getCTTSEntry(offsetIndex); + if ((index - offsetPos) < tmpCTTS.sampleCount){ + timeOffset = (tmpCTTS.sampleOffset*1000)/timeScale; + break; + } + offsetPos += tmpCTTS.sampleCount; + ++offsetIndex; + } + } + size = stszBox.getEntrySize(index); + } + + inputMP4::inputMP4(Util::Config * cfg) : Input(cfg){ + malSize = 4;//initialise data read buffer to 0; + data = (char*)malloc(malSize); + capa["name"] = "MP4"; + capa["desc"] = "This input allows streaming of MP4 files as Video on Demand."; + capa["source_match"] = "/*.mp4"; + capa["source_file"] = "$source"; + capa["priority"] = 9; + capa["codecs"][0u][0u].append("H264"); + capa["codecs"][0u][0u].append("H263"); + capa["codecs"][0u][0u].append("VP6"); + capa["codecs"][0u][1u].append("AAC"); + capa["codecs"][0u][1u].append("MP3"); + } + + inputMP4::~inputMP4(){ + free(data); + } + + bool inputMP4::checkArguments(){ + if (config->getString("input") == "-"){ + std::cerr << "Input from stdin not yet supported" << std::endl; + return false; + } + if (!config->getString("streamname").size()){ + if (config->getString("output") == "-"){ + std::cerr << "Output to stdout not yet supported" << std::endl; + return false; + } + }else{ + if (config->getString("output") != "-"){ + std::cerr << "File output in player mode not supported" << std::endl; + return false; + } + streamName = config->getString("streamname"); + } + return true; + } + + bool inputMP4::preRun(){ + //open File + inFile = fopen(config->getString("input").c_str(), "r"); + if (!inFile){ + return false; + } + return true; + + } + + bool inputMP4::readHeader(){ + if (!inFile){ + INFO_MSG("inFile failed!"); + return false; + } + + uint32_t trackNo = 0; + + //first we get the necessary header parts + while(!feof(inFile)){ + std::string boxType = MP4::readBoxType(inFile); + if (boxType == "erro"){ + break; + } + if (boxType=="moov"){ + MP4::MOOV moovBox; + moovBox.read(inFile); + //for all box in moov + + std::deque trak = moovBox.getChildren(); + for (std::deque::iterator trakIt = trak.begin(); trakIt != trak.end(); trakIt++){ + headerData[++trackNo].read(*trakIt); + } + continue; + } + if (!MP4::skipBox(inFile)){//moving on to next box + FAIL_MSG("Error in skipping box, exiting"); + return false; + } + } + fseeko(inFile,0,SEEK_SET); + + //See whether a separate header file exists. + if (readExistingHeader()){return true;} + HIGH_MSG("Not read existing header"); + + trackNo = 0; + //Create header file from MP4 data + while(!feof(inFile)){ + std::string boxType = MP4::readBoxType(inFile); + if (boxType=="erro"){ + break; + } + if (boxType=="moov"){ + MP4::MOOV moovBox; + moovBox.read(inFile); + + std::deque trak = moovBox.getChildren(); + HIGH_MSG("Obtained %zu trak Boxes", trak.size()); + + for (std::deque::iterator trakIt = trak.begin(); trakIt != trak.end(); trakIt++){ + uint64_t trackNo = myMeta.tracks.size()+1; + myMeta.tracks[trackNo].trackID = trackNo; + + MP4::TKHD tkhdBox = trakIt->getChild(); + if (tkhdBox.getWidth() > 0){ + myMeta.tracks[trackNo].width = tkhdBox.getWidth(); + myMeta.tracks[trackNo].height = tkhdBox.getHeight(); + } + + MP4::MDIA mdiaBox = trakIt->getChild(); + + MP4::MDHD mdhdBox = mdiaBox.getChild(); + uint64_t timescale = mdhdBox.getTimeScale(); + myMeta.tracks[trackNo].lang = mdhdBox.getLanguage(); + + std::string hdlrType = mdiaBox.getChild().getHandlerType(); + if (hdlrType != "vide" && hdlrType != "soun" && hdlrType != "sbtl"){ + headerData.erase(trackNo); + myMeta.tracks.erase(trackNo); + break; + } + + MP4::STBL stblBox = mdiaBox.getChild().getChild(); + + MP4::STSD stsdBox = stblBox.getChild(); + MP4::Box sEntryBox = stsdBox.getEntry(0); + std::string sType = sEntryBox.getType(); + HIGH_MSG("Found track %zu of type %s", trackNo, sType.c_str()); + + if (sType == "avc1" || sType == "h264" || sType == "mp4v"){ + MP4::VisualSampleEntry & vEntryBox = (MP4::VisualSampleEntry&)sEntryBox; + myMeta.tracks[trackNo].type = "video"; + myMeta.tracks[trackNo].codec = "H264"; + if (!myMeta.tracks[trackNo].width){ + myMeta.tracks[trackNo].width = vEntryBox.getWidth(); + myMeta.tracks[trackNo].height = vEntryBox.getHeight(); + } + MP4::Box initBox = vEntryBox.getCLAP(); + if (initBox.isType("avcC")){ + myMeta.tracks[trackNo].init.assign(initBox.payload(), initBox.payloadSize()); + } + initBox = vEntryBox.getPASP(); + if (initBox.isType("avcC")){ + myMeta.tracks[trackNo].init.assign(initBox.payload(), initBox.payloadSize()); + } + ///this is a hacky way around invalid FLV data (since it gets ignored nearly everywhere, but we do need correct data... + if (!myMeta.tracks[trackNo].width){ + h264::sequenceParameterSet sps; + sps.fromDTSCInit(myMeta.tracks[trackNo].init); + h264::SPSMeta spsChar = sps.getCharacteristics(); + myMeta.tracks[trackNo].width = spsChar.width; + myMeta.tracks[trackNo].height = spsChar.height; + } + } + if (sType == "mp4a" || sType == "aac "){ + MP4::AudioSampleEntry & aEntryBox = (MP4::AudioSampleEntry&)sEntryBox; + myMeta.tracks[trackNo].type = "audio"; + myMeta.tracks[trackNo].channels = aEntryBox.getChannelCount(); + myMeta.tracks[trackNo].rate = aEntryBox.getSampleRate(); + MP4::ESDS esdsBox = (MP4::ESDS&)(aEntryBox.getCodecBox()); + myMeta.tracks[trackNo].codec = esdsBox.getCodec(); + myMeta.tracks[trackNo].init = esdsBox.getInitData(); + myMeta.tracks[trackNo].size = 16;///\todo this might be nice to calculate from mp4 file; + } + + if (sType == "tx3g"){//plain text subtitles + myMeta.tracks[trackNo].type = "meta"; + myMeta.tracks[trackNo].codec = "subtitle"; + } + + MP4::STSS stssBox = stblBox.getChild(); + MP4::STTS sttsBox = stblBox.getChild(); + MP4::STSZ stszBox = stblBox.getChild(); + MP4::STCO stcoBox = stblBox.getChild(); + MP4::CO64 co64Box = stblBox.getChild(); + MP4::STSC stscBox = stblBox.getChild(); + MP4::CTTS cttsBox = stblBox.getChild();//optional ctts box + + bool stco64 = co64Box.isType("co64"); + bool hasCTTS = cttsBox.isType("ctts"); + + uint64_t totaldur = 0;///\todo note: set this to begin time + mp4PartBpos BsetPart; + + uint64_t entryNo = 0; + uint64_t sampleNo = 0; + + uint64_t stssIndex = 0; + uint64_t stcoIndex = 0; + uint64_t stscIndex = 0; + uint64_t cttsIndex = 0;//current ctts Index we are reading + uint64_t cttsEntryRead = 0;//current part of ctts we are reading + + uint64_t stssCount = stssBox.getEntryCount(); + uint64_t stscCount = stscBox.getEntryCount(); + uint64_t stszCount = stszBox.getSampleCount(); + uint64_t stcoCount = (stco64 ? co64Box.getEntryCount() : stcoBox.getEntryCount()); + + MP4::STTSEntry sttsEntry = sttsBox.getSTTSEntry(0); + + uint32_t fromSTCOinSTSC = 0; + uint64_t tmpOffset = (stco64 ? co64Box.getChunkOffset(0) : stcoBox.getChunkOffset(0)); + + uint64_t nextFirstChunk = (stscCount > 1 ? stscBox.getSTSCEntry(1).firstChunk - 1 : stcoCount); + + for (uint64_t stszIndex = 0; stszIndex < stszCount; ++stszIndex){ + if (stcoIndex >= nextFirstChunk){ + ++stscIndex; + nextFirstChunk = (stscIndex + 1 < stscCount ? stscBox.getSTSCEntry(stscIndex + 1).firstChunk - 1 : stcoCount); + } + BsetPart.keyframe = (myMeta.tracks[trackNo].type == "video" && stssIndex < stssCount && stszIndex + 1 == stssBox.getSampleNumber(stssIndex)); + if (BsetPart.keyframe){ + ++stssIndex; + } + //in bpos set + BsetPart.stcoNr = stcoIndex; + //bpos = chunkoffset[samplenr] in stco + BsetPart.bpos = tmpOffset; + ++fromSTCOinSTSC; + if (fromSTCOinSTSC < stscBox.getSTSCEntry(stscIndex).samplesPerChunk){//as long as we are still in this chunk + tmpOffset += stszBox.getEntrySize(stszIndex); + }else{ + ++stcoIndex; + fromSTCOinSTSC = 0; + tmpOffset = (stco64 ? co64Box.getChunkOffset(stcoIndex) : stcoBox.getChunkOffset(stcoIndex)); + } + BsetPart.time = (totaldur*1000)/timescale; + totaldur += sttsEntry.sampleDelta; + sampleNo++; + if (sampleNo >= sttsEntry.sampleCount){ + ++entryNo; + sampleNo = 0; + if (entryNo < sttsBox.getEntryCount()){ + sttsEntry = sttsBox.getSTTSEntry(entryNo); + } + } + + if (hasCTTS){ + MP4::CTTSEntry cttsEntry = cttsBox.getCTTSEntry(cttsIndex); + cttsEntryRead++; + if (cttsEntryRead >= cttsEntry.sampleCount){ + ++cttsIndex; + cttsEntryRead = 0; + } + BsetPart.timeOffset = (cttsEntry.sampleOffset * 1000)/timescale; + }else{ + BsetPart.timeOffset = 0; + } + + 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; + } + if (!MP4::skipBox(inFile)){//moving on to next box + FAIL_MSG("Error in Skipping box, exiting"); + return false; + } + } + clearerr(inFile); + + //outputting dtsh file + myMeta.toFile(config->getString("input") + ".dtsh"); + return true; + } + + void inputMP4::getNext(bool smart){//get next part from track in stream + if (curPositions.empty()){ + thisPacket.null(); + return; + } + //pop uit set + mp4PartTime curPart = *curPositions.begin(); + curPositions.erase(curPositions.begin()); + + bool isKeyframe = false; + if(nextKeyframe[curPart.trackID] < myMeta.tracks[curPart.trackID].keys.size()){ + //checking if this is a keyframe + if (myMeta.tracks[curPart.trackID].type == "video" && (long long int) curPart.time == myMeta.tracks[curPart.trackID].keys[(nextKeyframe[curPart.trackID])].getTime()){ + isKeyframe = true; + } + //if a keyframe has passed, we find the next keyframe + if (myMeta.tracks[curPart.trackID].keys[(nextKeyframe[curPart.trackID])].getTime() <= (long long int)curPart.time){ + nextKeyframe[curPart.trackID] ++; + } + } + if (fseeko(inFile,curPart.bpos,SEEK_SET)){ + FAIL_MSG("seek unsuccessful @bpos %" PRIu64 ": %s",curPart.bpos, strerror(errno)); + thisPacket.null(); + return; + } + if (curPart.size > malSize){ + data = (char*)realloc(data, curPart.size); + malSize = curPart.size; + } + if (fread(data, curPart.size, 1, inFile)!=1){ + FAIL_MSG("read unsuccessful at %" PRIu64, ftell(inFile)); + thisPacket.null(); + return; + } + + if (myMeta.tracks[curPart.trackID].codec == "subtitle"){ + unsigned int txtLen = Bit::btohs(data); + if (!txtLen && false ){ + curPart.index ++; + return getNext(smart); + //thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, " ", 1, 0/*Note: no bpos*/, isKeyframe); + }else{ + + static JSON::Value thisPack; + thisPack.null(); + thisPack["trackid"] = (uint64_t)curPart.trackID; + thisPack["bpos"] = curPart.bpos; //(long long)fileSource.tellg(); + thisPack["data"] = std::string(data+2,txtLen); + thisPack["time"] = curPart.time; + if (curPart.duration){ + thisPack["duration"] = curPart.duration; + } + 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{ + thisPacket.genericFill(curPart.time, curPart.offset, curPart.trackID, data, curPart.size, 0/*Note: no bpos*/, isKeyframe); + } + + //get the next part for this track + curPart.index ++; + if (curPart.index < headerData[curPart.trackID].size()){ + headerData[curPart.trackID].getPart(curPart.index, curPart.bpos, curPart.size, curPart.time, curPart.offset, curPart.duration); + curPositions.insert(curPart); + } + } + + void inputMP4::seek(int seekTime){//seek to a point + nextKeyframe.clear(); + //for all tracks + curPositions.clear(); + for (std::set::iterator it = selectedTracks.begin(); it != selectedTracks.end(); it++){ + nextKeyframe[*it] = 0; + mp4PartTime addPart; + addPart.bpos = 0; + addPart.size = 0; + addPart.time = 0; + addPart.trackID = *it; + //for all indexes in those tracks + for (unsigned int i = 0; i < headerData[*it].size(); i++){ + //if time > seekTime + headerData[*it].getPart(i, addPart.bpos, addPart.size, addPart.time, addPart.offset, addPart.duration); + //check for keyframe time in myMeta and update nextKeyframe + // + if (myMeta.tracks[*it].keys[(nextKeyframe[*it])].getTime() < addPart.time){ + nextKeyframe[*it] ++; + } + if (addPart.time >= seekTime){ + addPart.index = i; + //use addPart thingy in time set and break + curPositions.insert(addPart); + break; + }//end if time > seektime + }//end for all indexes + }//rof all tracks + } + + void inputMP4::trackSelect(std::string trackSpec){ + selectedTracks.clear(); + long long int index; + while (trackSpec != ""){ + index = trackSpec.find(' '); + 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); + if (index != std::string::npos){ + trackSpec.erase(0, index + 1); + }else{ + trackSpec = ""; + } + } + } +} + diff --git a/src/input/input_mp4.h b/src/input/input_mp4.h new file mode 100644 index 00000000..32468adb --- /dev/null +++ b/src/input/input_mp4.h @@ -0,0 +1,110 @@ +#include "input.h" +#include +#include +#include +namespace Mist { + class mp4PartTime{ + public: + mp4PartTime() : time(0), offset(0), trackID(0), bpos(0), size(0), index(0), duration(0) {} + bool operator < (const mp4PartTime & rhs) const { + if (time < rhs.time){ + return true; + } + if (time > rhs.time){ + return false; + } + if (trackID < rhs.trackID){ + return true; + } + return (trackID == rhs.trackID && bpos < rhs.bpos); + } + uint64_t time; + uint64_t duration; + int32_t offset; + size_t trackID; + uint64_t bpos; + uint32_t size; + uint64_t index; + }; + + struct mp4PartBpos{ + bool operator < (const mp4PartBpos & rhs) const { + if (time < rhs.time){ + return true; + } + if (time > rhs.time){ + return false; + } + if (trackID < rhs.trackID){ + return true; + } + return (trackID == rhs.trackID && bpos < rhs.bpos); + } + uint64_t time; + size_t trackID; + uint64_t bpos; + uint64_t size; + uint64_t stcoNr; + int32_t timeOffset; + bool keyframe; + }; + + class mp4TrackHeader{ + public: + mp4TrackHeader(); + void read(MP4::TRAK & trakBox); + MP4::STCO stcoBox; + MP4::CO64 co64Box; + MP4::STSZ stszBox; + MP4::STTS sttsBox; + bool hasCTTS; + MP4::CTTS cttsBox; + MP4::STSC stscBox; + uint64_t timeScale; + void getPart(uint64_t index, uint64_t & offset, uint32_t & size, uint64_t & timestamp, int32_t & timeOffset, uint64_t & duration); + uint64_t size(); + private: + bool initialised; + //next variables are needed for the stsc/stco loop + uint64_t stscStart; + uint64_t sampleIndex; + //next variables are needed for the stts loop + uint64_t deltaIndex;///< Index in STTS box + uint64_t deltaPos;///< Sample counter for STTS box + uint64_t deltaTotal;///< Total timestamp for STTS box + //for CTTS box loop + uint64_t offsetIndex;///< Index in CTTS box + uint64_t offsetPos;///< Sample counter for CTTS box + + bool stco64; + }; + + class inputMP4 : public Input { + public: + inputMP4(Util::Config * cfg); + ~inputMP4(); + protected: + //Private Functions + bool checkArguments(); + bool preRun(); + bool readHeader(); + bool needHeader(){return true;} + void getNext(bool smart = true); + void seek(int seekTime); + void trackSelect(std::string trackSpec); + + FILE * inFile; + + std::map headerData; + std::set curPositions; + + //remember last seeked keyframe; + std::map nextKeyframe; + + //these next two variables keep a buffer for reading from filepointer inFile; + uint64_t malSize; + char* data;///\todo rename this variable to a more sensible name, it is a temporary piece of memory to read from files + }; +} + +typedef Mist::inputMP4 mistIn;