| /********** |
| This library is free software; you can redistribute it and/or modify it under |
| the terms of the GNU Lesser General Public License as published by the |
| Free Software Foundation; either version 3 of the License, or (at your |
| option) any later version. (See <http://www.gnu.org/copyleft/lesser.html>.) |
| |
| This library is distributed in the hope that it will be useful, but WITHOUT |
| ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for |
| more details. |
| |
| You should have received a copy of the GNU Lesser General Public License |
| along with this library; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| **********/ |
| // "liveMedia" |
| // Copyright (c) 1996-2020 Live Networks, Inc. All rights reserved. |
| // A sink that generates a QuickTime file from a composite media session |
| // Implementation |
| |
| #include "QuickTimeFileSink.hh" |
| #include "QuickTimeGenericRTPSource.hh" |
| #include "GroupsockHelper.hh" |
| #include "InputFile.hh" |
| #include "OutputFile.hh" |
| #include "H263plusVideoRTPSource.hh" // for the special header |
| #include "MPEG4GenericRTPSource.hh" //for "samplingFrequencyFromAudioSpecificConfig()" |
| #include "MPEG4LATMAudioRTPSource.hh" // for "parseGeneralConfigStr()" |
| #include "Base64.hh" |
| |
| #include <ctype.h> |
| |
| #define fourChar(x,y,z,w) ( ((x)<<24)|((y)<<16)|((z)<<8)|(w) ) |
| |
| #define H264_IDR_FRAME 0x65 //bit 8 == 0, bits 7-6 (ref) == 3, bits 5-0 (type) == 5 |
| |
| ////////// SubsessionIOState, ChunkDescriptor /////////// |
| // A structure used to represent the I/O state of each input 'subsession': |
| |
| class ChunkDescriptor { |
| public: |
| ChunkDescriptor(int64_t offsetInFile, unsigned size, |
| unsigned frameSize, unsigned frameDuration, |
| struct timeval presentationTime); |
| |
| ChunkDescriptor* extendChunk(int64_t newOffsetInFile, unsigned newSize, |
| unsigned newFrameSize, |
| unsigned newFrameDuration, |
| struct timeval newPresentationTime); |
| // this may end up allocating a new chunk instead |
| public: |
| ChunkDescriptor* fNextChunk; |
| int64_t fOffsetInFile; |
| unsigned fNumFrames; |
| unsigned fFrameSize; |
| unsigned fFrameDuration; |
| struct timeval fPresentationTime; // of the start of the data |
| }; |
| |
| class SubsessionBuffer { |
| public: |
| SubsessionBuffer(unsigned bufferSize) |
| : fBufferSize(bufferSize) { |
| reset(); |
| fData = new unsigned char[bufferSize]; |
| } |
| virtual ~SubsessionBuffer() { delete[] fData; } |
| void reset() { fBytesInUse = 0; } |
| void addBytes(unsigned numBytes) { fBytesInUse += numBytes; } |
| |
| unsigned char* dataStart() { return &fData[0]; } |
| unsigned char* dataEnd() { return &fData[fBytesInUse]; } |
| unsigned bytesInUse() const { return fBytesInUse; } |
| unsigned bytesAvailable() const { return fBufferSize - fBytesInUse; } |
| |
| void setPresentationTime(struct timeval const& presentationTime) { |
| fPresentationTime = presentationTime; |
| } |
| struct timeval const& presentationTime() const {return fPresentationTime;} |
| |
| private: |
| unsigned fBufferSize; |
| struct timeval fPresentationTime; |
| unsigned char* fData; |
| unsigned fBytesInUse; |
| }; |
| |
| class SyncFrame { |
| public: |
| SyncFrame(unsigned frameNum); |
| |
| public: |
| class SyncFrame *nextSyncFrame; |
| unsigned sfFrameNum; |
| }; |
| |
| // A 64-bit counter, used below: |
| class Count64 { |
| public: |
| Count64() |
| : hi(0), lo(0) { |
| } |
| |
| void operator+=(unsigned arg); |
| |
| u_int32_t hi, lo; |
| }; |
| |
| class SubsessionIOState { |
| public: |
| SubsessionIOState(QuickTimeFileSink& sink, MediaSubsession& subsession); |
| virtual ~SubsessionIOState(); |
| |
| Boolean setQTstate(); |
| void setFinalQTstate(); |
| |
| void afterGettingFrame(unsigned packetDataSize, |
| struct timeval presentationTime); |
| void onSourceClosure(); |
| |
| Boolean syncOK(struct timeval presentationTime); |
| // returns true iff data is usable despite a sync check |
| |
| static void setHintTrack(SubsessionIOState* hintedTrack, |
| SubsessionIOState* hintTrack); |
| Boolean isHintTrack() const { return fTrackHintedByUs != NULL; } |
| Boolean hasHintTrack() const { return fHintTrackForUs != NULL; } |
| |
| UsageEnvironment& envir() const { return fOurSink.envir(); } |
| |
| public: |
| static unsigned fCurrentTrackNumber; |
| unsigned fTrackID; |
| SubsessionIOState* fHintTrackForUs; SubsessionIOState* fTrackHintedByUs; |
| |
| SubsessionBuffer *fBuffer, *fPrevBuffer; |
| QuickTimeFileSink& fOurSink; |
| MediaSubsession& fOurSubsession; |
| |
| unsigned short fLastPacketRTPSeqNum; |
| Boolean fOurSourceIsActive; |
| |
| Boolean fHaveBeenSynced; // used in synchronizing with other streams |
| struct timeval fSyncTime; |
| |
| Boolean fQTEnableTrack; |
| unsigned fQTcomponentSubtype; |
| char const* fQTcomponentName; |
| typedef unsigned (QuickTimeFileSink::*atomCreationFunc)(); |
| atomCreationFunc fQTMediaInformationAtomCreator; |
| atomCreationFunc fQTMediaDataAtomCreator; |
| char const* fQTAudioDataType; |
| unsigned short fQTSoundSampleVersion; |
| unsigned fQTTimeScale; |
| unsigned fQTTimeUnitsPerSample; |
| unsigned fQTBytesPerFrame; |
| unsigned fQTSamplesPerFrame; |
| // These next fields are derived from the ones above, |
| // plus the information from each chunk: |
| unsigned fQTTotNumSamples; |
| unsigned fQTDurationM; // in media time units |
| unsigned fQTDurationT; // in track time units |
| int64_t fTKHD_durationPosn; |
| // position of the duration in the output 'tkhd' atom |
| unsigned fQTInitialOffsetDuration; |
| // if there's a pause at the beginning |
| |
| ChunkDescriptor *fHeadChunk, *fTailChunk; |
| unsigned fNumChunks; |
| SyncFrame *fHeadSyncFrame, *fTailSyncFrame; |
| |
| // Counters to be used in the hint track's 'udta'/'hinf' atom; |
| struct hinf { |
| Count64 trpy; |
| Count64 nump; |
| Count64 tpyl; |
| // Is 'maxr' needed? Computing this would be a PITA. ##### |
| Count64 dmed; |
| Count64 dimm; |
| // 'drep' is always 0 |
| // 'tmin' and 'tmax' are always 0 |
| unsigned pmax; |
| unsigned dmax; |
| } fHINF; |
| |
| private: |
| void useFrame(SubsessionBuffer& buffer); |
| void useFrameForHinting(unsigned frameSize, |
| struct timeval presentationTime, |
| unsigned startSampleNumber); |
| |
| // used by the above two routines: |
| unsigned useFrame1(unsigned sourceDataSize, |
| struct timeval presentationTime, |
| unsigned frameDuration, int64_t destFileOffset); |
| // returns the number of samples in this data |
| |
| private: |
| // A structure used for temporarily storing frame state: |
| struct { |
| unsigned frameSize; |
| struct timeval presentationTime; |
| int64_t destFileOffset; // used for non-hint tracks only |
| |
| // The remaining fields are used for hint tracks only: |
| unsigned startSampleNumber; |
| unsigned short seqNum; |
| unsigned rtpHeader; |
| unsigned char numSpecialHeaders; // used when our RTP source has special headers |
| unsigned specialHeaderBytesLength; // ditto |
| unsigned char specialHeaderBytes[SPECIAL_HEADER_BUFFER_SIZE]; // ditto |
| unsigned packetSizes[256]; |
| } fPrevFrameState; |
| }; |
| |
| |
| ////////// QuickTimeFileSink implementation ////////// |
| |
| QuickTimeFileSink::QuickTimeFileSink(UsageEnvironment& env, |
| MediaSession& inputSession, |
| char const* outputFileName, |
| unsigned bufferSize, |
| unsigned short movieWidth, |
| unsigned short movieHeight, |
| unsigned movieFPS, |
| Boolean packetLossCompensate, |
| Boolean syncStreams, |
| Boolean generateHintTracks, |
| Boolean generateMP4Format) |
| : Medium(env), fInputSession(inputSession), |
| fBufferSize(bufferSize), fPacketLossCompensate(packetLossCompensate), |
| fSyncStreams(syncStreams), fGenerateMP4Format(generateMP4Format), |
| fAreCurrentlyBeingPlayed(False), |
| fLargestRTPtimestampFrequency(0), |
| fNumSubsessions(0), fNumSyncedSubsessions(0), |
| fHaveCompletedOutputFile(False), |
| fMovieWidth(movieWidth), fMovieHeight(movieHeight), |
| fMovieFPS(movieFPS), fMaxTrackDurationM(0) { |
| fOutFid = OpenOutputFile(env, outputFileName); |
| if (fOutFid == NULL) return; |
| |
| fNewestSyncTime.tv_sec = fNewestSyncTime.tv_usec = 0; |
| fFirstDataTime.tv_sec = fFirstDataTime.tv_usec = (unsigned)(~0); |
| |
| // Set up I/O state for each input subsession: |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| // Ignore subsessions without a data source: |
| FramedSource* subsessionSource = subsession->readSource(); |
| if (subsessionSource == NULL) continue; |
| |
| // If "subsession's" SDP description specified screen dimension |
| // or frame rate parameters, then use these. (Note that this must |
| // be done before the call to "setQTState()" below.) |
| if (subsession->videoWidth() != 0) { |
| fMovieWidth = subsession->videoWidth(); |
| } |
| if (subsession->videoHeight() != 0) { |
| fMovieHeight = subsession->videoHeight(); |
| } |
| if (subsession->videoFPS() != 0) { |
| fMovieFPS = subsession->videoFPS(); |
| } |
| |
| SubsessionIOState* ioState |
| = new SubsessionIOState(*this, *subsession); |
| if (ioState == NULL || !ioState->setQTstate()) { |
| // We're not able to output a QuickTime track for this subsession |
| delete ioState; ioState = NULL; |
| continue; |
| } |
| subsession->miscPtr = (void*)ioState; |
| |
| if (generateHintTracks) { |
| // Also create a hint track for this track: |
| SubsessionIOState* hintTrack |
| = new SubsessionIOState(*this, *subsession); |
| SubsessionIOState::setHintTrack(ioState, hintTrack); |
| if (!hintTrack->setQTstate()) { |
| delete hintTrack; |
| SubsessionIOState::setHintTrack(ioState, NULL); |
| } |
| } |
| |
| // Also set a 'BYE' handler for this subsession's RTCP instance: |
| if (subsession->rtcpInstance() != NULL) { |
| subsession->rtcpInstance()->setByeHandler(onRTCPBye, ioState); |
| } |
| |
| unsigned rtpTimestampFrequency = subsession->rtpTimestampFrequency(); |
| if (rtpTimestampFrequency > fLargestRTPtimestampFrequency) { |
| fLargestRTPtimestampFrequency = rtpTimestampFrequency; |
| } |
| |
| ++fNumSubsessions; |
| } |
| |
| // Use the current time as the file's creation and modification |
| // time. Use Apple's time format: seconds (UTC) since January 1, 1904 |
| |
| gettimeofday(&fStartTime, NULL); |
| fAppleCreationTime = fStartTime.tv_sec - 0x83da4f80; |
| |
| // Begin by writing a "mdat" atom at the start of the file. |
| // (Later, when we've finished copying data to the file, we'll come |
| // back and fill in its size.) |
| fMDATposition = TellFile64(fOutFid); |
| addAtomHeader64("mdat"); |
| // add 64Bit offset |
| fMDATposition += 8; |
| } |
| |
| QuickTimeFileSink::~QuickTimeFileSink() { |
| completeOutputFile(); |
| |
| // Then, stop streaming and delete each active "SubsessionIOState": |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| if (subsession->readSource() != NULL) subsession->readSource()->stopGettingFrames(); |
| |
| SubsessionIOState* ioState |
| = (SubsessionIOState*)(subsession->miscPtr); |
| if (ioState == NULL) continue; |
| |
| delete ioState->fHintTrackForUs; // if any |
| delete ioState; |
| } |
| |
| // Finally, close our output file: |
| CloseOutputFile(fOutFid); |
| } |
| |
| QuickTimeFileSink* |
| QuickTimeFileSink::createNew(UsageEnvironment& env, |
| MediaSession& inputSession, |
| char const* outputFileName, |
| unsigned bufferSize, |
| unsigned short movieWidth, |
| unsigned short movieHeight, |
| unsigned movieFPS, |
| Boolean packetLossCompensate, |
| Boolean syncStreams, |
| Boolean generateHintTracks, |
| Boolean generateMP4Format) { |
| QuickTimeFileSink* newSink = |
| new QuickTimeFileSink(env, inputSession, outputFileName, bufferSize, movieWidth, movieHeight, movieFPS, |
| packetLossCompensate, syncStreams, generateHintTracks, generateMP4Format); |
| if (newSink == NULL || newSink->fOutFid == NULL) { |
| Medium::close(newSink); |
| return NULL; |
| } |
| |
| return newSink; |
| } |
| |
| void QuickTimeFileSink |
| ::noteRecordedFrame(MediaSubsession& /*inputSubsession*/, |
| unsigned /*packetDataSize*/, struct timeval const& /*presentationTime*/) { |
| // Default implementation: Do nothing |
| } |
| |
| Boolean QuickTimeFileSink::startPlaying(afterPlayingFunc* afterFunc, |
| void* afterClientData) { |
| // Make sure we're not already being played: |
| if (fAreCurrentlyBeingPlayed) { |
| envir().setResultMsg("This sink has already been played"); |
| return False; |
| } |
| |
| fAreCurrentlyBeingPlayed = True; |
| fAfterFunc = afterFunc; |
| fAfterClientData = afterClientData; |
| |
| return continuePlaying(); |
| } |
| |
| Boolean QuickTimeFileSink::continuePlaying() { |
| // Run through each of our input session's 'subsessions', |
| // asking for a frame from each one: |
| Boolean haveActiveSubsessions = False; |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| FramedSource* subsessionSource = subsession->readSource(); |
| if (subsessionSource == NULL) continue; |
| |
| if (subsessionSource->isCurrentlyAwaitingData()) continue; |
| |
| SubsessionIOState* ioState |
| = (SubsessionIOState*)(subsession->miscPtr); |
| if (ioState == NULL) continue; |
| |
| haveActiveSubsessions = True; |
| unsigned char* toPtr = ioState->fBuffer->dataEnd(); |
| unsigned toSize = ioState->fBuffer->bytesAvailable(); |
| subsessionSource->getNextFrame(toPtr, toSize, |
| afterGettingFrame, ioState, |
| onSourceClosure, ioState); |
| } |
| if (!haveActiveSubsessions) { |
| envir().setResultMsg("No subsessions are currently active"); |
| return False; |
| } |
| |
| return True; |
| } |
| |
| void QuickTimeFileSink |
| ::afterGettingFrame(void* clientData, unsigned packetDataSize, |
| unsigned numTruncatedBytes, |
| struct timeval presentationTime, |
| unsigned /*durationInMicroseconds*/) { |
| SubsessionIOState* ioState = (SubsessionIOState*)clientData; |
| if (!ioState->syncOK(presentationTime)) { |
| // Ignore this data: |
| ioState->fOurSink.continuePlaying(); |
| return; |
| } |
| if (numTruncatedBytes > 0) { |
| ioState->envir() << "QuickTimeFileSink::afterGettingFrame(): The input frame data was too large for our buffer. " |
| << numTruncatedBytes |
| << " bytes of trailing data was dropped! Correct this by increasing the \"bufferSize\" parameter in the \"createNew()\" call.\n"; |
| } |
| ioState->afterGettingFrame(packetDataSize, presentationTime); |
| } |
| |
| void QuickTimeFileSink::onSourceClosure(void* clientData) { |
| SubsessionIOState* ioState = (SubsessionIOState*)clientData; |
| ioState->onSourceClosure(); |
| } |
| |
| void QuickTimeFileSink::onSourceClosure1() { |
| // Check whether *all* of the subsession sources have closed. |
| // If not, do nothing for now: |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| SubsessionIOState* ioState |
| = (SubsessionIOState*)(subsession->miscPtr); |
| if (ioState == NULL) continue; |
| |
| if (ioState->fOurSourceIsActive) return; // this source hasn't closed |
| } |
| |
| completeOutputFile(); |
| |
| // Call our specified 'after' function: |
| if (fAfterFunc != NULL) { |
| (*fAfterFunc)(fAfterClientData); |
| } |
| } |
| |
| void QuickTimeFileSink::onRTCPBye(void* clientData) { |
| SubsessionIOState* ioState = (SubsessionIOState*)clientData; |
| |
| struct timeval timeNow; |
| gettimeofday(&timeNow, NULL); |
| unsigned secsDiff |
| = timeNow.tv_sec - ioState->fOurSink.fStartTime.tv_sec; |
| |
| MediaSubsession& subsession = ioState->fOurSubsession; |
| ioState->envir() << "Received RTCP \"BYE\" on \"" |
| << subsession.mediumName() |
| << "/" << subsession.codecName() |
| << "\" subsession (after " |
| << secsDiff << " seconds)\n"; |
| |
| // Handle the reception of a RTCP "BYE" as if the source had closed: |
| ioState->onSourceClosure(); |
| } |
| |
| static Boolean timevalGE(struct timeval const& tv1, |
| struct timeval const& tv2) { |
| return (unsigned)tv1.tv_sec > (unsigned)tv2.tv_sec |
| || (tv1.tv_sec == tv2.tv_sec |
| && (unsigned)tv1.tv_usec >= (unsigned)tv2.tv_usec); |
| } |
| |
| void QuickTimeFileSink::completeOutputFile() { |
| if (fHaveCompletedOutputFile || fOutFid == NULL) return; |
| |
| // Begin by filling in the initial "mdat" atom with the current |
| // file size: |
| int64_t curFileSize = TellFile64(fOutFid); |
| setWord64(fMDATposition, (u_int64_t)curFileSize); |
| |
| // Then, note the time of the first received data: |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| SubsessionIOState* ioState |
| = (SubsessionIOState*)(subsession->miscPtr); |
| if (ioState == NULL) continue; |
| |
| ChunkDescriptor* const headChunk = ioState->fHeadChunk; |
| if (headChunk != NULL |
| && timevalGE(fFirstDataTime, headChunk->fPresentationTime)) { |
| fFirstDataTime = headChunk->fPresentationTime; |
| } |
| } |
| |
| // Then, update the QuickTime-specific state for each active track: |
| iter.reset(); |
| while ((subsession = iter.next()) != NULL) { |
| SubsessionIOState* ioState |
| = (SubsessionIOState*)(subsession->miscPtr); |
| if (ioState == NULL) continue; |
| |
| ioState->setFinalQTstate(); |
| // Do the same for a hint track (if any): |
| if (ioState->hasHintTrack()) { |
| ioState->fHintTrackForUs->setFinalQTstate(); |
| } |
| } |
| |
| if (fGenerateMP4Format) { |
| // Begin with a "ftyp" atom: |
| addAtom_ftyp(); |
| } |
| |
| // Then, add a "moov" atom for the file metadata: |
| addAtom_moov(); |
| |
| // We're done: |
| fHaveCompletedOutputFile = True; |
| } |
| |
| |
| ////////// SubsessionIOState, ChunkDescriptor implementation /////////// |
| |
| unsigned SubsessionIOState::fCurrentTrackNumber = 0; |
| |
| SubsessionIOState::SubsessionIOState(QuickTimeFileSink& sink, |
| MediaSubsession& subsession) |
| : fHintTrackForUs(NULL), fTrackHintedByUs(NULL), |
| fOurSink(sink), fOurSubsession(subsession), |
| fLastPacketRTPSeqNum(0), fHaveBeenSynced(False), fQTTotNumSamples(0), |
| fHeadChunk(NULL), fTailChunk(NULL), fNumChunks(0), |
| fHeadSyncFrame(NULL), fTailSyncFrame(NULL) { |
| fTrackID = ++fCurrentTrackNumber; |
| |
| fBuffer = new SubsessionBuffer(fOurSink.fBufferSize); |
| fPrevBuffer = sink.fPacketLossCompensate |
| ? new SubsessionBuffer(fOurSink.fBufferSize) : NULL; |
| |
| FramedSource* subsessionSource = subsession.readSource(); |
| fOurSourceIsActive = subsessionSource != NULL; |
| |
| fPrevFrameState.presentationTime.tv_sec = 0; |
| fPrevFrameState.presentationTime.tv_usec = 0; |
| fPrevFrameState.seqNum = 0; |
| } |
| |
| SubsessionIOState::~SubsessionIOState() { |
| delete fBuffer; delete fPrevBuffer; |
| |
| // Delete the list of chunk descriptors: |
| ChunkDescriptor* chunk = fHeadChunk; |
| while (chunk != NULL) { |
| ChunkDescriptor* next = chunk->fNextChunk; |
| delete chunk; |
| chunk = next; |
| } |
| |
| // Delete the list of sync frames: |
| SyncFrame* syncFrame = fHeadSyncFrame; |
| while (syncFrame != NULL) { |
| SyncFrame* next = syncFrame->nextSyncFrame; |
| delete syncFrame; |
| syncFrame = next; |
| } |
| } |
| |
| Boolean SubsessionIOState::setQTstate() { |
| char const* noCodecWarning1 = "Warning: We don't implement a QuickTime "; |
| char const* noCodecWarning2 = " Media Data Type for the \""; |
| char const* noCodecWarning3 = "\" track, so we'll insert a dummy \"????\" Media Data Atom instead. A separate, codec-specific editing pass will be needed before this track can be played.\n"; |
| |
| do { |
| fQTEnableTrack = True; // enable this track in the movie by default |
| fQTTimeScale = fOurSubsession.rtpTimestampFrequency(); // by default |
| fQTTimeUnitsPerSample = 1; // by default |
| fQTBytesPerFrame = 0; |
| // by default - indicates that the whole packet data is a frame |
| fQTSamplesPerFrame = 1; // by default |
| |
| // Make sure our subsession's medium is one that we know how to |
| // represent in a QuickTime file: |
| if (isHintTrack()) { |
| // Hint tracks are treated specially |
| fQTEnableTrack = False; // hint tracks are marked as inactive |
| fQTcomponentSubtype = fourChar('h','i','n','t'); |
| fQTcomponentName = "hint media handler"; |
| fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_gmhd; |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_rtp; |
| } else if (strcmp(fOurSubsession.mediumName(), "audio") == 0) { |
| fQTcomponentSubtype = fourChar('s','o','u','n'); |
| fQTcomponentName = "Apple Sound Media Handler"; |
| fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_smhd; |
| fQTMediaDataAtomCreator |
| = &QuickTimeFileSink::addAtom_soundMediaGeneral; // by default |
| fQTSoundSampleVersion = 0; // by default |
| |
| // Make sure that our subsession's codec is one that we can handle: |
| if (strcmp(fOurSubsession.codecName(), "X-QT") == 0 || |
| strcmp(fOurSubsession.codecName(), "X-QUICKTIME") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_genericMedia; |
| } else if (strcmp(fOurSubsession.codecName(), "PCMU") == 0) { |
| fQTAudioDataType = "ulaw"; |
| fQTBytesPerFrame = 1; |
| } else if (strcmp(fOurSubsession.codecName(), "GSM") == 0) { |
| fQTAudioDataType = "agsm"; |
| fQTBytesPerFrame = 33; |
| fQTSamplesPerFrame = 160; |
| } else if (strcmp(fOurSubsession.codecName(), "PCMA") == 0) { |
| fQTAudioDataType = "alaw"; |
| fQTBytesPerFrame = 1; |
| } else if (strcmp(fOurSubsession.codecName(), "QCELP") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_Qclp; |
| fQTSamplesPerFrame = 160; |
| } else if (strcmp(fOurSubsession.codecName(), "MPEG4-GENERIC") == 0 || |
| strcmp(fOurSubsession.codecName(), "MP4A-LATM") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_mp4a; |
| fQTTimeUnitsPerSample = 1024; // QT considers each frame to be a 'sample' |
| // The time scale (frequency) comes from the 'config' information. |
| // It might be different from the RTP timestamp frequency (e.g., aacPlus). |
| unsigned frequencyFromConfig |
| = samplingFrequencyFromAudioSpecificConfig(fOurSubsession.fmtp_config()); |
| if (frequencyFromConfig != 0) fQTTimeScale = frequencyFromConfig; |
| } else { |
| envir() << noCodecWarning1 << "Audio" << noCodecWarning2 |
| << fOurSubsession.codecName() << noCodecWarning3; |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_dummy; |
| fQTEnableTrack = False; // disable this track in the movie |
| } |
| } else if (strcmp(fOurSubsession.mediumName(), "video") == 0) { |
| fQTcomponentSubtype = fourChar('v','i','d','e'); |
| fQTcomponentName = "Apple Video Media Handler"; |
| fQTMediaInformationAtomCreator = &QuickTimeFileSink::addAtom_vmhd; |
| |
| // Make sure that our subsession's codec is one that we can handle: |
| if (strcmp(fOurSubsession.codecName(), "X-QT") == 0 || |
| strcmp(fOurSubsession.codecName(), "X-QUICKTIME") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_genericMedia; |
| } else if (strcmp(fOurSubsession.codecName(), "H263-1998") == 0 || |
| strcmp(fOurSubsession.codecName(), "H263-2000") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_h263; |
| fQTTimeScale = 600; |
| fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS; |
| } else if (strcmp(fOurSubsession.codecName(), "H264") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_avc1; |
| fQTTimeScale = 600; |
| fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS; |
| } else if (strcmp(fOurSubsession.codecName(), "MP4V-ES") == 0) { |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_mp4v; |
| fQTTimeScale = 600; |
| fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS; |
| } else { |
| envir() << noCodecWarning1 << "Video" << noCodecWarning2 |
| << fOurSubsession.codecName() << noCodecWarning3; |
| fQTMediaDataAtomCreator = &QuickTimeFileSink::addAtom_dummy; |
| fQTEnableTrack = False; // disable this track in the movie |
| } |
| } else { |
| envir() << "Warning: We don't implement a QuickTime Media Handler for media type \"" |
| << fOurSubsession.mediumName() << "\""; |
| break; |
| } |
| |
| #ifdef QT_SUPPORT_PARTIALLY_ONLY |
| envir() << "Warning: We don't have sufficient codec-specific information (e.g., sample sizes) to fully generate the \"" |
| << fOurSubsession.mediumName() << "/" << fOurSubsession.codecName() |
| << "\" track, so we'll disable this track in the movie. A separate, codec-specific editing pass will be needed before this track can be played\n"; |
| fQTEnableTrack = False; // disable this track in the movie |
| #endif |
| |
| return True; |
| } while (0); |
| |
| envir() << ", so a track for the \"" << fOurSubsession.mediumName() |
| << "/" << fOurSubsession.codecName() |
| << "\" subsession will not be included in the output QuickTime file\n"; |
| return False; |
| } |
| |
| void SubsessionIOState::setFinalQTstate() { |
| // Compute derived parameters, by running through the list of chunks: |
| fQTDurationT = 0; |
| |
| ChunkDescriptor* chunk = fHeadChunk; |
| while (chunk != NULL) { |
| unsigned const numFrames = chunk->fNumFrames; |
| unsigned const dur = numFrames*chunk->fFrameDuration; |
| fQTDurationT += dur; |
| |
| chunk = chunk->fNextChunk; |
| } |
| |
| // Convert this duration from track to movie time scale: |
| double scaleFactor = fOurSink.movieTimeScale()/(double)fQTTimeScale; |
| fQTDurationM = (unsigned)(fQTDurationT*scaleFactor); |
| |
| if (fQTDurationM > fOurSink.fMaxTrackDurationM) { |
| fOurSink.fMaxTrackDurationM = fQTDurationM; |
| } |
| } |
| |
| void SubsessionIOState::afterGettingFrame(unsigned packetDataSize, |
| struct timeval presentationTime) { |
| // Begin by checking whether there was a gap in the RTP stream. |
| // If so, try to compensate for this (if desired): |
| if (fOurSubsession.rtpSource() != NULL) { // we have a RTP stream |
| unsigned short rtpSeqNum |
| = fOurSubsession.rtpSource()->curPacketRTPSeqNum(); |
| if (fOurSink.fPacketLossCompensate && fPrevBuffer->bytesInUse() > 0) { |
| short seqNumGap = rtpSeqNum - fLastPacketRTPSeqNum; |
| for (short i = 1; i < seqNumGap; ++i) { |
| // Insert a copy of the previous frame, to compensate for the loss: |
| useFrame(*fPrevBuffer); |
| } |
| } |
| fLastPacketRTPSeqNum = rtpSeqNum; |
| } |
| |
| // Now, continue working with the frame that we just got |
| fOurSink.noteRecordedFrame(fOurSubsession, packetDataSize, presentationTime); |
| |
| if (fBuffer->bytesInUse() == 0) { |
| fBuffer->setPresentationTime(presentationTime); |
| } |
| fBuffer->addBytes(packetDataSize); |
| |
| // If our RTP source is a "QuickTimeGenericRTPSource", then |
| // use its 'qtState' to set some parameters that we need: |
| if (fOurSubsession.rtpSource() != NULL // we have a RTP stream |
| && fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_genericMedia) { |
| QuickTimeGenericRTPSource* rtpSource |
| = (QuickTimeGenericRTPSource*)fOurSubsession.rtpSource(); |
| QuickTimeGenericRTPSource::QTState& qtState = rtpSource->qtState; |
| fQTTimeScale = qtState.timescale; |
| if (qtState.width != 0) { |
| fOurSink.fMovieWidth = qtState.width; |
| } |
| if (qtState.height != 0) { |
| fOurSink.fMovieHeight = qtState.height; |
| } |
| |
| // Also, if the media type in the "sdAtom" is one that we recognize |
| // to have a special parameters, then fix this here: |
| if (qtState.sdAtomSize >= 8) { |
| char const* atom = qtState.sdAtom; |
| unsigned mediaType = fourChar(atom[4],atom[5],atom[6],atom[7]); |
| switch (mediaType) { |
| case fourChar('a','g','s','m'): { |
| fQTBytesPerFrame = 33; |
| fQTSamplesPerFrame = 160; |
| break; |
| } |
| case fourChar('Q','c','l','p'): { |
| fQTBytesPerFrame = 35; |
| fQTSamplesPerFrame = 160; |
| break; |
| } |
| case fourChar('H','c','l','p'): { |
| fQTBytesPerFrame = 17; |
| fQTSamplesPerFrame = 160; |
| break; |
| } |
| case fourChar('h','2','6','3'): { |
| fQTTimeUnitsPerSample = fQTTimeScale/fOurSink.fMovieFPS; |
| break; |
| } |
| } |
| } |
| } else if (fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_Qclp) { |
| // For QCELP data, make a note of the frame size (even though it's the |
| // same as the packet data size), because it varies depending on the |
| // 'rate' of the stream, and this size gets used later when setting up |
| // the 'Qclp' QuickTime atom: |
| fQTBytesPerFrame = packetDataSize; |
| } |
| |
| useFrame(*fBuffer); |
| if (fOurSink.fPacketLossCompensate) { |
| // Save this frame, in case we need it for recovery: |
| SubsessionBuffer* tmp = fPrevBuffer; // assert: != NULL |
| fPrevBuffer = fBuffer; |
| fBuffer = tmp; |
| } |
| fBuffer->reset(); // for the next input |
| |
| // Now, try getting more frames: |
| fOurSink.continuePlaying(); |
| } |
| |
| void SubsessionIOState::useFrame(SubsessionBuffer& buffer) { |
| unsigned char* const frameSource = buffer.dataStart(); |
| unsigned const frameSize = buffer.bytesInUse(); |
| struct timeval const& presentationTime = buffer.presentationTime(); |
| int64_t const destFileOffset = TellFile64(fOurSink.fOutFid); |
| unsigned sampleNumberOfFrameStart = fQTTotNumSamples + 1; |
| Boolean avcHack = fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_avc1; |
| |
| // If we're not syncing streams, or this subsession is not video, then |
| // just give this frame a fixed duration: |
| if (!fOurSink.fSyncStreams |
| || fQTcomponentSubtype != fourChar('v','i','d','e')) { |
| unsigned const frameDuration = fQTTimeUnitsPerSample*fQTSamplesPerFrame; |
| unsigned frameSizeToUse = frameSize; |
| if (avcHack) frameSizeToUse += 4; // H.264/AVC gets the frame size prefix |
| |
| fQTTotNumSamples += useFrame1(frameSizeToUse, presentationTime, frameDuration, destFileOffset); |
| } else { |
| // For synced video streams, we use the difference between successive |
| // frames' presentation times as the 'frame duration'. So, record |
| // information about the *previous* frame: |
| struct timeval const& ppt = fPrevFrameState.presentationTime; //abbrev |
| if (ppt.tv_sec != 0 || ppt.tv_usec != 0) { |
| // There has been a previous frame. |
| double duration = (presentationTime.tv_sec - ppt.tv_sec) |
| + (presentationTime.tv_usec - ppt.tv_usec)/1000000.0; |
| if (duration < 0.0) duration = 0.0; |
| unsigned frameDuration |
| = (unsigned)((2*duration*fQTTimeScale+1)/2); // round |
| unsigned frameSizeToUse = fPrevFrameState.frameSize; |
| if (avcHack) frameSizeToUse += 4; // H.264/AVC gets the frame size prefix |
| |
| unsigned numSamples |
| = useFrame1(frameSizeToUse, ppt, frameDuration, fPrevFrameState.destFileOffset); |
| fQTTotNumSamples += numSamples; |
| sampleNumberOfFrameStart = fQTTotNumSamples + 1; |
| } |
| |
| if (avcHack && (*frameSource == H264_IDR_FRAME)) { |
| SyncFrame* newSyncFrame = new SyncFrame(fQTTotNumSamples + 1); |
| if (fTailSyncFrame == NULL) { |
| fHeadSyncFrame = newSyncFrame; |
| } else { |
| fTailSyncFrame->nextSyncFrame = newSyncFrame; |
| } |
| fTailSyncFrame = newSyncFrame; |
| } |
| |
| // Remember the current frame for next time: |
| fPrevFrameState.frameSize = frameSize; |
| fPrevFrameState.presentationTime = presentationTime; |
| fPrevFrameState.destFileOffset = destFileOffset; |
| } |
| |
| if (avcHack) fOurSink.addWord(frameSize); |
| |
| // Write the data into the file: |
| fwrite(frameSource, 1, frameSize, fOurSink.fOutFid); |
| |
| // If we have a hint track, then write to it also (only if we have a RTP stream): |
| if (hasHintTrack() && fOurSubsession.rtpSource() != NULL) { |
| // Because presentation times are used for RTP packet timestamps, |
| // we don't starting writing to the hint track until we've been synced: |
| if (!fHaveBeenSynced) { |
| fHaveBeenSynced = fOurSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP(); |
| } |
| if (fHaveBeenSynced) { |
| fHintTrackForUs->useFrameForHinting(frameSize, presentationTime, |
| sampleNumberOfFrameStart); |
| } |
| } |
| } |
| |
| void SubsessionIOState::useFrameForHinting(unsigned frameSize, |
| struct timeval presentationTime, |
| unsigned startSampleNumber) { |
| // At this point, we have a single, combined frame - not individual packets. |
| // For the hint track, we need to split the frame back up into separate packets. |
| // However, for some RTP sources, then we also need to reuse the special |
| // header bytes that were at the start of each of the RTP packets. |
| Boolean hack263 = strcmp(fOurSubsession.codecName(), "H263-1998") == 0; |
| Boolean hackm4a_generic = strcmp(fOurSubsession.mediumName(), "audio") == 0 |
| && strcmp(fOurSubsession.codecName(), "MPEG4-GENERIC") == 0; |
| Boolean hackm4a_latm = strcmp(fOurSubsession.mediumName(), "audio") == 0 |
| && strcmp(fOurSubsession.codecName(), "MP4A-LATM") == 0; |
| Boolean hackm4a = hackm4a_generic || hackm4a_latm; |
| Boolean haveSpecialHeaders = (hack263 || hackm4a_generic); |
| |
| // If there has been a previous frame, then output a 'hint sample' for it. |
| // (We use the current frame's presentation time to compute the previous |
| // hint sample's duration.) |
| RTPSource* const rs = fOurSubsession.rtpSource(); // abbrev (ASSERT: != NULL) |
| struct timeval const& ppt = fPrevFrameState.presentationTime; //abbrev |
| if (ppt.tv_sec != 0 || ppt.tv_usec != 0) { |
| double duration = (presentationTime.tv_sec - ppt.tv_sec) |
| + (presentationTime.tv_usec - ppt.tv_usec)/1000000.0; |
| if (duration < 0.0) duration = 0.0; |
| unsigned msDuration = (unsigned)(duration*1000); // milliseconds |
| if (msDuration > fHINF.dmax) fHINF.dmax = msDuration; |
| unsigned hintSampleDuration |
| = (unsigned)((2*duration*fQTTimeScale+1)/2); // round |
| if (hackm4a) { |
| // Because multiple AAC frames can appear in a RTP packet, the presentation |
| // times of the second and subsequent frames will not be accurate. |
| // So, use the known "hintSampleDuration" instead: |
| hintSampleDuration = fTrackHintedByUs->fQTTimeUnitsPerSample; |
| |
| // Also, if the 'time scale' was different from the RTP timestamp frequency, |
| // (as can happen with aacPlus), then we need to scale "hintSampleDuration" |
| // accordingly: |
| if (fTrackHintedByUs->fQTTimeScale != fOurSubsession.rtpTimestampFrequency()) { |
| unsigned const scalingFactor |
| = fOurSubsession.rtpTimestampFrequency()/fTrackHintedByUs->fQTTimeScale ; |
| hintSampleDuration *= scalingFactor; |
| } |
| } |
| |
| int64_t const hintSampleDestFileOffset = TellFile64(fOurSink.fOutFid); |
| |
| unsigned const maxPacketSize = 1450; |
| unsigned short numPTEntries |
| = (fPrevFrameState.frameSize + (maxPacketSize-1))/maxPacketSize; // normal case |
| unsigned char* immediateDataPtr = NULL; |
| unsigned immediateDataBytesRemaining = 0; |
| if (haveSpecialHeaders) { // special case |
| numPTEntries = fPrevFrameState.numSpecialHeaders; |
| immediateDataPtr = fPrevFrameState.specialHeaderBytes; |
| immediateDataBytesRemaining |
| = fPrevFrameState.specialHeaderBytesLength; |
| } |
| unsigned hintSampleSize |
| = fOurSink.addHalfWord(numPTEntries);// Entry count |
| hintSampleSize += fOurSink.addHalfWord(0x0000); // Reserved |
| |
| unsigned offsetWithinSample = 0; |
| for (unsigned i = 0; i < numPTEntries; ++i) { |
| // Output a Packet Table entry (representing a single RTP packet): |
| unsigned short numDTEntries = 1; |
| unsigned short seqNum = fPrevFrameState.seqNum++; |
| // Note: This assumes that the input stream had no packets lost ##### |
| unsigned rtpHeader = fPrevFrameState.rtpHeader; |
| if (i+1 < numPTEntries) { |
| // This is not the last RTP packet, so clear the marker bit: |
| rtpHeader &=~ (1<<23); |
| } |
| unsigned dataFrameSize = (i+1 < numPTEntries) |
| ? maxPacketSize : fPrevFrameState.frameSize - i*maxPacketSize; // normal case |
| unsigned sampleNumber = fPrevFrameState.startSampleNumber; |
| |
| unsigned char immediateDataLen = 0; |
| if (haveSpecialHeaders) { // special case |
| ++numDTEntries; // to include a Data Table entry for the special hdr |
| if (immediateDataBytesRemaining > 0) { |
| if (hack263) { |
| immediateDataLen = *immediateDataPtr++; |
| --immediateDataBytesRemaining; |
| if (immediateDataLen > immediateDataBytesRemaining) { |
| // shouldn't happen (length byte was bad) |
| immediateDataLen = immediateDataBytesRemaining; |
| } |
| } else { |
| immediateDataLen = fPrevFrameState.specialHeaderBytesLength; |
| } |
| } |
| dataFrameSize = fPrevFrameState.packetSizes[i] - immediateDataLen; |
| |
| if (hack263) { |
| Boolean PbitSet |
| = immediateDataLen >= 1 && (immediateDataPtr[0]&0x4) != 0; |
| if (PbitSet) { |
| offsetWithinSample += 2; // to omit the two leading 0 bytes |
| } |
| } |
| } |
| |
| // Output the Packet Table: |
| hintSampleSize += fOurSink.addWord(0); // Relative transmission time |
| hintSampleSize += fOurSink.addWord(rtpHeader|seqNum); |
| // RTP header info + RTP sequence number |
| hintSampleSize += fOurSink.addHalfWord(0x0000); // Flags |
| hintSampleSize += fOurSink.addHalfWord(numDTEntries); // Entry count |
| unsigned totalPacketSize = 0; |
| |
| // Output the Data Table: |
| if (haveSpecialHeaders) { |
| // use the "Immediate Data" format (1): |
| hintSampleSize += fOurSink.addByte(1); // Source |
| unsigned char len = immediateDataLen > 14 ? 14 : immediateDataLen; |
| hintSampleSize += fOurSink.addByte(len); // Length |
| totalPacketSize += len; fHINF.dimm += len; |
| unsigned char j; |
| for (j = 0; j < len; ++j) { |
| hintSampleSize += fOurSink.addByte(immediateDataPtr[j]); // Data |
| } |
| for (j = len; j < 14; ++j) { |
| hintSampleSize += fOurSink.addByte(0); // Data (padding) |
| } |
| |
| immediateDataPtr += immediateDataLen; |
| immediateDataBytesRemaining -= immediateDataLen; |
| } |
| // use the "Sample Data" format (2): |
| hintSampleSize += fOurSink.addByte(2); // Source |
| hintSampleSize += fOurSink.addByte(0); // Track ref index |
| hintSampleSize += fOurSink.addHalfWord(dataFrameSize); // Length |
| totalPacketSize += dataFrameSize; fHINF.dmed += dataFrameSize; |
| hintSampleSize += fOurSink.addWord(sampleNumber); // Sample number |
| hintSampleSize += fOurSink.addWord(offsetWithinSample); // Offset |
| // Get "bytes|samples per compression block" from the hinted track: |
| unsigned short const bytesPerCompressionBlock |
| = fTrackHintedByUs->fQTBytesPerFrame; |
| unsigned short const samplesPerCompressionBlock |
| = fTrackHintedByUs->fQTSamplesPerFrame; |
| hintSampleSize += fOurSink.addHalfWord(bytesPerCompressionBlock); |
| hintSampleSize += fOurSink.addHalfWord(samplesPerCompressionBlock); |
| |
| offsetWithinSample += dataFrameSize;// for the next iteration (if any) |
| |
| // Tally statistics for this packet: |
| fHINF.nump += 1; |
| fHINF.tpyl += totalPacketSize; |
| totalPacketSize += 12; // add in the size of the RTP header |
| fHINF.trpy += totalPacketSize; |
| if (totalPacketSize > fHINF.pmax) fHINF.pmax = totalPacketSize; |
| } |
| |
| // Make note of this completed hint sample frame: |
| fQTTotNumSamples += useFrame1(hintSampleSize, ppt, hintSampleDuration, |
| hintSampleDestFileOffset); |
| } |
| |
| // Remember this frame for next time: |
| fPrevFrameState.frameSize = frameSize; |
| fPrevFrameState.presentationTime = presentationTime; |
| fPrevFrameState.startSampleNumber = startSampleNumber; |
| fPrevFrameState.rtpHeader |
| = rs->curPacketMarkerBit()<<23 |
| | (rs->rtpPayloadFormat()&0x7F)<<16; |
| if (hack263) { |
| H263plusVideoRTPSource* rs_263 = (H263plusVideoRTPSource*)rs; |
| fPrevFrameState.numSpecialHeaders = rs_263->fNumSpecialHeaders; |
| fPrevFrameState.specialHeaderBytesLength = rs_263->fSpecialHeaderBytesLength; |
| unsigned i; |
| for (i = 0; i < rs_263->fSpecialHeaderBytesLength; ++i) { |
| fPrevFrameState.specialHeaderBytes[i] = rs_263->fSpecialHeaderBytes[i]; |
| } |
| for (i = 0; i < rs_263->fNumSpecialHeaders; ++i) { |
| fPrevFrameState.packetSizes[i] = rs_263->fPacketSizes[i]; |
| } |
| } else if (hackm4a_generic) { |
| // Synthesize a special header, so that this frame can be in its own RTP packet. |
| unsigned const sizeLength = fOurSubsession.attrVal_unsigned("sizelength"); |
| unsigned const indexLength = fOurSubsession.attrVal_unsigned("indexlength"); |
| if (sizeLength + indexLength != 16) { |
| envir() << "Warning: unexpected 'sizeLength' " << sizeLength |
| << " and 'indexLength' " << indexLength |
| << "seen when creating hint track\n"; |
| } |
| fPrevFrameState.numSpecialHeaders = 1; |
| fPrevFrameState.specialHeaderBytesLength = 4; |
| fPrevFrameState.specialHeaderBytes[0] = 0; // AU_headers_length (high byte) |
| fPrevFrameState.specialHeaderBytes[1] = 16; // AU_headers_length (low byte) |
| fPrevFrameState.specialHeaderBytes[2] = ((frameSize<<indexLength)&0xFF00)>>8; |
| fPrevFrameState.specialHeaderBytes[3] = (frameSize<<indexLength); |
| fPrevFrameState.packetSizes[0] |
| = fPrevFrameState.specialHeaderBytesLength + frameSize; |
| } |
| } |
| |
| unsigned SubsessionIOState::useFrame1(unsigned sourceDataSize, |
| struct timeval presentationTime, |
| unsigned frameDuration, |
| int64_t destFileOffset) { |
| // Figure out the actual frame size for this data: |
| unsigned frameSize = fQTBytesPerFrame; |
| if (frameSize == 0) { |
| // The entire packet data is assumed to be a frame: |
| frameSize = sourceDataSize; |
| } |
| unsigned const numFrames = sourceDataSize/frameSize; |
| unsigned const numSamples = numFrames*fQTSamplesPerFrame; |
| |
| // Record the information about which 'chunk' this data belongs to: |
| ChunkDescriptor* newTailChunk; |
| if (fTailChunk == NULL) { |
| newTailChunk = fHeadChunk |
| = new ChunkDescriptor(destFileOffset, sourceDataSize, |
| frameSize, frameDuration, presentationTime); |
| } else { |
| newTailChunk = fTailChunk->extendChunk(destFileOffset, sourceDataSize, |
| frameSize, frameDuration, |
| presentationTime); |
| } |
| if (newTailChunk != fTailChunk) { |
| // This data created a new chunk, rather than extending the old one |
| ++fNumChunks; |
| fTailChunk = newTailChunk; |
| } |
| |
| return numSamples; |
| } |
| |
| void SubsessionIOState::onSourceClosure() { |
| fOurSourceIsActive = False; |
| fOurSink.onSourceClosure1(); |
| } |
| |
| Boolean SubsessionIOState::syncOK(struct timeval presentationTime) { |
| QuickTimeFileSink& s = fOurSink; // abbreviation |
| if (!s.fSyncStreams || fOurSubsession.rtpSource() == NULL) return True; // we don't care |
| |
| if (s.fNumSyncedSubsessions < s.fNumSubsessions) { |
| // Not all subsessions have yet been synced. Check whether ours was |
| // one of the unsynced ones, and, if so, whether it is now synced: |
| if (!fHaveBeenSynced) { |
| // We weren't synchronized before |
| if (fOurSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP()) { |
| // H264 ? |
| if (fQTMediaDataAtomCreator == &QuickTimeFileSink::addAtom_avc1) { |
| // special case: audio + H264 video: wait until audio is in sync |
| if ((s.fNumSubsessions == 2) && (s.fNumSyncedSubsessions < (s.fNumSubsessions - 1))) return False; |
| |
| // if audio is in sync, wait for the next IDR frame to start |
| unsigned char* const frameSource = fBuffer->dataStart(); |
| if (*frameSource != H264_IDR_FRAME) return False; |
| } |
| // But now we are |
| fHaveBeenSynced = True; |
| fSyncTime = presentationTime; |
| ++s.fNumSyncedSubsessions; |
| |
| if (timevalGE(fSyncTime, s.fNewestSyncTime)) { |
| s.fNewestSyncTime = fSyncTime; |
| } |
| } |
| } |
| } |
| |
| // Check again whether all subsessions have been synced: |
| if (s.fNumSyncedSubsessions < s.fNumSubsessions) return False; |
| |
| // Allow this data if it is more recent than the newest sync time: |
| return timevalGE(presentationTime, s.fNewestSyncTime); |
| } |
| |
| void SubsessionIOState::setHintTrack(SubsessionIOState* hintedTrack, |
| SubsessionIOState* hintTrack) { |
| if (hintedTrack != NULL) hintedTrack->fHintTrackForUs = hintTrack; |
| if (hintTrack != NULL) hintTrack->fTrackHintedByUs = hintedTrack; |
| } |
| |
| SyncFrame::SyncFrame(unsigned frameNum) |
| : nextSyncFrame(NULL), sfFrameNum(frameNum) { |
| } |
| |
| void Count64::operator+=(unsigned arg) { |
| unsigned newLo = lo + arg; |
| if (newLo < lo) { // lo has overflowed |
| ++hi; |
| } |
| lo = newLo; |
| } |
| |
| ChunkDescriptor |
| ::ChunkDescriptor(int64_t offsetInFile, unsigned size, |
| unsigned frameSize, unsigned frameDuration, |
| struct timeval presentationTime) |
| : fNextChunk(NULL), fOffsetInFile(offsetInFile), |
| fNumFrames(size/frameSize), |
| fFrameSize(frameSize), fFrameDuration(frameDuration), |
| fPresentationTime(presentationTime) { |
| } |
| |
| ChunkDescriptor* ChunkDescriptor |
| ::extendChunk(int64_t newOffsetInFile, unsigned newSize, |
| unsigned newFrameSize, unsigned newFrameDuration, |
| struct timeval newPresentationTime) { |
| // First, check whether the new space is just at the end of this |
| // existing chunk: |
| if (newOffsetInFile == fOffsetInFile + fNumFrames*fFrameSize) { |
| // We can extend this existing chunk, provided that the frame size |
| // and frame duration have not changed: |
| if (newFrameSize == fFrameSize && newFrameDuration == fFrameDuration) { |
| fNumFrames += newSize/fFrameSize; |
| return this; |
| } |
| } |
| |
| // We'll allocate a new ChunkDescriptor, and link it to the end of us: |
| ChunkDescriptor* newDescriptor |
| = new ChunkDescriptor(newOffsetInFile, newSize, |
| newFrameSize, newFrameDuration, |
| newPresentationTime); |
| |
| fNextChunk = newDescriptor; |
| |
| return newDescriptor; |
| } |
| |
| |
| ////////// QuickTime-specific implementation ////////// |
| |
| unsigned QuickTimeFileSink::addWord64(u_int64_t word) { |
| addByte((unsigned char)(word>>56)); addByte((unsigned char)(word>>48)); |
| addByte((unsigned char)(word>>40)); addByte((unsigned char)(word>>32)); |
| addByte((unsigned char)(word>>24)); addByte((unsigned char)(word>>16)); |
| addByte((unsigned char)(word>>8)); addByte((unsigned char)(word)); |
| |
| return 8; |
| } |
| |
| unsigned QuickTimeFileSink::addWord(unsigned word) { |
| addByte(word>>24); addByte(word>>16); |
| addByte(word>>8); addByte(word); |
| |
| return 4; |
| } |
| |
| unsigned QuickTimeFileSink::addHalfWord(unsigned short halfWord) { |
| addByte((unsigned char)(halfWord>>8)); addByte((unsigned char)halfWord); |
| |
| return 2; |
| } |
| |
| unsigned QuickTimeFileSink::addZeroWords(unsigned numWords) { |
| for (unsigned i = 0; i < numWords; ++i) { |
| addWord(0); |
| } |
| |
| return numWords*4; |
| } |
| |
| unsigned QuickTimeFileSink::add4ByteString(char const* str) { |
| addByte(str[0]); addByte(str[1]); addByte(str[2]); addByte(str[3]); |
| |
| return 4; |
| } |
| |
| unsigned QuickTimeFileSink::addArbitraryString(char const* str, |
| Boolean oneByteLength) { |
| unsigned size = 0; |
| if (oneByteLength) { |
| // Begin with a byte containing the string length: |
| unsigned strLength = strlen(str); |
| if (strLength >= 256) { |
| envir() << "QuickTimeFileSink::addArbitraryString(\"" |
| << str << "\") saw string longer than we know how to handle (" |
| << strLength << "\n"; |
| } |
| size += addByte((unsigned char)strLength); |
| } |
| |
| while (*str != '\0') { |
| size += addByte(*str++); |
| } |
| |
| return size; |
| } |
| |
| unsigned QuickTimeFileSink::addAtomHeader(char const* atomName) { |
| // Output a placeholder for the 4-byte size: |
| addWord(0); |
| |
| // Output the 4-byte atom name: |
| add4ByteString(atomName); |
| |
| return 8; |
| } |
| |
| unsigned QuickTimeFileSink::addAtomHeader64(char const* atomName) { |
| // Output 64Bit size marker |
| addWord(1); |
| |
| // Output the 4-byte atom name: |
| add4ByteString(atomName); |
| |
| addWord64(0); |
| |
| return 16; |
| } |
| |
| void QuickTimeFileSink::setWord(int64_t filePosn, unsigned size) { |
| do { |
| if (SeekFile64(fOutFid, filePosn, SEEK_SET) < 0) break; |
| addWord(size); |
| if (SeekFile64(fOutFid, 0, SEEK_END) < 0) break; // go back to where we were |
| |
| return; |
| } while (0); |
| |
| // One of the SeekFile64()s failed, probable because we're not a seekable file |
| envir() << "QuickTimeFileSink::setWord(): SeekFile64 failed (err " |
| << envir().getErrno() << ")\n"; |
| } |
| |
| void QuickTimeFileSink::setWord64(int64_t filePosn, u_int64_t size) { |
| do { |
| if (SeekFile64(fOutFid, filePosn, SEEK_SET) < 0) break; |
| addWord64(size); |
| if (SeekFile64(fOutFid, 0, SEEK_END) < 0) break; // go back to where we were |
| |
| return; |
| } while (0); |
| |
| // One of the SeekFile64()s failed, probable because we're not a seekable file |
| envir() << "QuickTimeFileSink::setWord64(): SeekFile64 failed (err " |
| << envir().getErrno() << ")\n"; |
| } |
| |
| // Methods for writing particular atoms. Note the following macros: |
| |
| #define addAtom(name) \ |
| unsigned QuickTimeFileSink::addAtom_##name() { \ |
| int64_t initFilePosn = TellFile64(fOutFid); \ |
| unsigned size = addAtomHeader("" #name "") |
| |
| #define addAtomEnd \ |
| setWord(initFilePosn, size); \ |
| return size; \ |
| } |
| |
| addAtom(ftyp); |
| size += add4ByteString("mp42"); |
| size += addWord(0x00000000); |
| size += add4ByteString("mp42"); |
| size += add4ByteString("isom"); |
| addAtomEnd; |
| |
| addAtom(moov); |
| size += addAtom_mvhd(); |
| |
| if (fGenerateMP4Format) { |
| size += addAtom_iods(); |
| } |
| |
| // Add a 'trak' atom for each subsession: |
| // (For some unknown reason, QuickTime Player (5.0 at least) |
| // doesn't display the movie correctly unless the audio track |
| // (if present) appears before the video track. So ensure this here.) |
| MediaSubsessionIterator iter(fInputSession); |
| MediaSubsession* subsession; |
| while ((subsession = iter.next()) != NULL) { |
| fCurrentIOState = (SubsessionIOState*)(subsession->miscPtr); |
| if (fCurrentIOState == NULL) continue; |
| if (strcmp(subsession->mediumName(), "audio") != 0) continue; |
| |
| size += addAtom_trak(); |
| |
| if (fCurrentIOState->hasHintTrack()) { |
| // This track has a hint track; output it also: |
| fCurrentIOState = fCurrentIOState->fHintTrackForUs; |
| size += addAtom_trak(); |
| } |
| } |
| iter.reset(); |
| while ((subsession = iter.next()) != NULL) { |
| fCurrentIOState = (SubsessionIOState*)(subsession->miscPtr); |
| if (fCurrentIOState == NULL) continue; |
| if (strcmp(subsession->mediumName(), "audio") == 0) continue; |
| |
| size += addAtom_trak(); |
| |
| if (fCurrentIOState->hasHintTrack()) { |
| // This track has a hint track; output it also: |
| fCurrentIOState = fCurrentIOState->fHintTrackForUs; |
| size += addAtom_trak(); |
| } |
| } |
| addAtomEnd; |
| |
| addAtom(mvhd); |
| size += addWord(0x00000000); // Version + Flags |
| size += addWord(fAppleCreationTime); // Creation time |
| size += addWord(fAppleCreationTime); // Modification time |
| |
| // For the "Time scale" field, use the largest RTP timestamp frequency |
| // that we saw in any of the subsessions. |
| size += addWord(movieTimeScale()); // Time scale |
| |
| unsigned const duration = fMaxTrackDurationM; |
| fMVHD_durationPosn = TellFile64(fOutFid); |
| size += addWord(duration); // Duration |
| |
| size += addWord(0x00010000); // Preferred rate |
| size += addWord(0x01000000); // Preferred volume + Reserved[0] |
| size += addZeroWords(2); // Reserved[1-2] |
| size += addWord(0x00010000); // matrix top left corner |
| size += addZeroWords(3); // matrix |
| size += addWord(0x00010000); // matrix center |
| size += addZeroWords(3); // matrix |
| size += addWord(0x40000000); // matrix bottom right corner |
| size += addZeroWords(6); // various time fields |
| size += addWord(SubsessionIOState::fCurrentTrackNumber+1);// Next track ID |
| addAtomEnd; |
| |
| addAtom(iods); |
| size += addWord(0x00000000); // Version + Flags |
| size += addWord(0x10808080); |
| size += addWord(0x07004FFF); |
| size += addWord(0xFF0FFFFF); |
| addAtomEnd; |
| |
| addAtom(trak); |
| size += addAtom_tkhd(); |
| |
| // If we're synchronizing the media streams (or are a hint track), |
| // add an edit list that helps do this: |
| if (fCurrentIOState->fHeadChunk != NULL |
| && (fSyncStreams || fCurrentIOState->isHintTrack())) { |
| size += addAtom_edts(); |
| } |
| |
| // If we're generating a hint track, add a 'tref' atom: |
| if (fCurrentIOState->isHintTrack()) size += addAtom_tref(); |
| |
| size += addAtom_mdia(); |
| |
| // If we're generating a hint track, add a 'udta' atom: |
| if (fCurrentIOState->isHintTrack()) size += addAtom_udta(); |
| addAtomEnd; |
| |
| addAtom(tkhd); |
| if (fCurrentIOState->fQTEnableTrack) { |
| size += addWord(0x0000000F); // Version + Flags |
| } else { |
| // Disable this track in the movie: |
| size += addWord(0x00000000); // Version + Flags |
| } |
| size += addWord(fAppleCreationTime); // Creation time |
| size += addWord(fAppleCreationTime); // Modification time |
| size += addWord(fCurrentIOState->fTrackID); // Track ID |
| size += addWord(0x00000000); // Reserved |
| |
| unsigned const duration = fCurrentIOState->fQTDurationM; // movie units |
| fCurrentIOState->fTKHD_durationPosn = TellFile64(fOutFid); |
| size += addWord(duration); // Duration |
| size += addZeroWords(3); // Reserved+Layer+Alternate grp |
| size += addWord(0x01000000); // Volume + Reserved |
| size += addWord(0x00010000); // matrix top left corner |
| size += addZeroWords(3); // matrix |
| size += addWord(0x00010000); // matrix center |
| size += addZeroWords(3); // matrix |
| size += addWord(0x40000000); // matrix bottom right corner |
| if (strcmp(fCurrentIOState->fOurSubsession.mediumName(), "video") == 0) { |
| size += addWord(fMovieWidth<<16); // Track width |
| size += addWord(fMovieHeight<<16); // Track height |
| } else { |
| size += addZeroWords(2); // not video: leave width and height fields zero |
| } |
| addAtomEnd; |
| |
| addAtom(edts); |
| size += addAtom_elst(); |
| addAtomEnd; |
| |
| #define addEdit1(duration,trackPosition) do { \ |
| unsigned trackDuration \ |
| = (unsigned) ((2*(duration)*movieTimeScale()+1)/2); \ |
| /* in movie time units */ \ |
| size += addWord(trackDuration); /* Track duration */ \ |
| totalDurationOfEdits += trackDuration; \ |
| size += addWord(trackPosition); /* Media time */ \ |
| size += addWord(0x00010000); /* Media rate (1x) */ \ |
| ++numEdits; \ |
| } while (0) |
| #define addEdit(duration) addEdit1((duration),editTrackPosition) |
| #define addEmptyEdit(duration) addEdit1((duration),(~0)) |
| |
| addAtom(elst); |
| size += addWord(0x00000000); // Version + Flags |
| |
| // Add a dummy "Number of entries" field |
| // (and remember its position). We'll fill this field in later: |
| int64_t numEntriesPosition = TellFile64(fOutFid); |
| size += addWord(0); // dummy for "Number of entries" |
| unsigned numEdits = 0; |
| unsigned totalDurationOfEdits = 0; // in movie time units |
| |
| // Run through our chunks, looking at their presentation times. |
| // From these, figure out the edits that need to be made to keep |
| // the track media data in sync with the presentation times. |
| |
| double const syncThreshold = 0.1; // 100 ms |
| // don't allow the track to get out of sync by more than this |
| |
| struct timeval editStartTime = fFirstDataTime; |
| unsigned editTrackPosition = 0; |
| unsigned currentTrackPosition = 0; |
| double trackDurationOfEdit = 0.0; |
| unsigned chunkDuration = 0; |
| |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| struct timeval const& chunkStartTime = chunk->fPresentationTime; |
| double movieDurationOfEdit |
| = (chunkStartTime.tv_sec - editStartTime.tv_sec) |
| + (chunkStartTime.tv_usec - editStartTime.tv_usec)/1000000.0; |
| trackDurationOfEdit = (currentTrackPosition-editTrackPosition) |
| / (double)(fCurrentIOState->fQTTimeScale); |
| |
| double outOfSync = movieDurationOfEdit - trackDurationOfEdit; |
| |
| if (outOfSync > syncThreshold) { |
| // The track's data is too short, so end this edit, add a new |
| // 'empty' edit after it, and start a new edit |
| // (at the current track posn.): |
| if (trackDurationOfEdit > 0.0) addEdit(trackDurationOfEdit); |
| addEmptyEdit(outOfSync); |
| |
| editStartTime = chunkStartTime; |
| editTrackPosition = currentTrackPosition; |
| } else if (outOfSync < -syncThreshold) { |
| // The track's data is too long, so end this edit, and start |
| // a new edit (pointing at the current track posn.): |
| if (movieDurationOfEdit > 0.0) addEdit(movieDurationOfEdit); |
| |
| editStartTime = chunkStartTime; |
| editTrackPosition = currentTrackPosition; |
| } |
| |
| // Note the duration of this chunk: |
| unsigned numChannels = fCurrentIOState->fOurSubsession.numChannels(); |
| chunkDuration = chunk->fNumFrames*chunk->fFrameDuration/numChannels; |
| currentTrackPosition += chunkDuration; |
| |
| chunk = chunk->fNextChunk; |
| } |
| |
| // Write out the final edit |
| trackDurationOfEdit |
| += (double)chunkDuration/fCurrentIOState->fQTTimeScale; |
| if (trackDurationOfEdit > 0.0) addEdit(trackDurationOfEdit); |
| |
| // Now go back and fill in the "Number of entries" field: |
| setWord(numEntriesPosition, numEdits); |
| |
| // Also, if the sum of all of the edit durations exceeds the |
| // track duration that we already computed (from sample durations), |
| // then reset the track duration to this new value: |
| if (totalDurationOfEdits > fCurrentIOState->fQTDurationM) { |
| fCurrentIOState->fQTDurationM = totalDurationOfEdits; |
| setWord(fCurrentIOState->fTKHD_durationPosn, totalDurationOfEdits); |
| |
| // Also, check whether the overall movie duration needs to change: |
| if (totalDurationOfEdits > fMaxTrackDurationM) { |
| fMaxTrackDurationM = totalDurationOfEdits; |
| setWord(fMVHD_durationPosn, totalDurationOfEdits); |
| } |
| |
| // Also, convert to track time scale: |
| double scaleFactor |
| = fCurrentIOState->fQTTimeScale/(double)movieTimeScale(); |
| fCurrentIOState->fQTDurationT |
| = (unsigned)(totalDurationOfEdits*scaleFactor); |
| } |
| addAtomEnd; |
| |
| addAtom(tref); |
| size += addAtom_hint(); |
| addAtomEnd; |
| |
| addAtom(hint); |
| SubsessionIOState* hintedTrack = fCurrentIOState->fTrackHintedByUs; |
| // Assert: hintedTrack != NULL |
| size += addWord(hintedTrack->fTrackID); |
| addAtomEnd; |
| |
| addAtom(mdia); |
| size += addAtom_mdhd(); |
| size += addAtom_hdlr(); |
| size += addAtom_minf(); |
| addAtomEnd; |
| |
| addAtom(mdhd); |
| size += addWord(0x00000000); // Version + Flags |
| size += addWord(fAppleCreationTime); // Creation time |
| size += addWord(fAppleCreationTime); // Modification time |
| |
| unsigned const timeScale = fCurrentIOState->fQTTimeScale; |
| size += addWord(timeScale); // Time scale |
| |
| unsigned const duration = fCurrentIOState->fQTDurationT; // track units |
| size += addWord(duration); // Duration |
| |
| size += addWord(0x00000000); // Language+Quality |
| addAtomEnd; |
| |
| addAtom(hdlr); |
| size += addWord(0x00000000); // Version + Flags |
| size += add4ByteString("mhlr"); // Component type |
| size += addWord(fCurrentIOState->fQTcomponentSubtype); |
| // Component subtype |
| size += add4ByteString("appl"); // Component manufacturer |
| size += addWord(0x00000000); // Component flags |
| size += addWord(0x00000000); // Component flags mask |
| size += addArbitraryString(fCurrentIOState->fQTcomponentName); |
| // Component name |
| addAtomEnd; |
| |
| addAtom(minf); |
| SubsessionIOState::atomCreationFunc mediaInformationAtomCreator |
| = fCurrentIOState->fQTMediaInformationAtomCreator; |
| size += (this->*mediaInformationAtomCreator)(); |
| size += addAtom_hdlr2(); |
| size += addAtom_dinf(); |
| size += addAtom_stbl(); |
| addAtomEnd; |
| |
| addAtom(smhd); |
| size += addZeroWords(2); // Version+Flags+Balance+Reserved |
| addAtomEnd; |
| |
| addAtom(vmhd); |
| size += addWord(0x00000001); // Version + Flags |
| size += addWord(0x00408000); // Graphics mode + Opcolor[red] |
| size += addWord(0x80008000); // Opcolor[green} + Opcolor[blue] |
| addAtomEnd; |
| |
| addAtom(gmhd); |
| size += addAtom_gmin(); |
| addAtomEnd; |
| |
| addAtom(gmin); |
| size += addWord(0x00000000); // Version + Flags |
| // The following fields probably aren't used for hint tracks, so just |
| // use values that I've seen in other files: |
| size += addWord(0x00408000); // Graphics mode + Opcolor (1st 2 bytes) |
| size += addWord(0x80008000); // Opcolor (last 4 bytes) |
| size += addWord(0x00000000); // Balance + Reserved |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_hdlr2() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| unsigned size = addAtomHeader("hdlr"); |
| size += addWord(0x00000000); // Version + Flags |
| size += add4ByteString("dhlr"); // Component type |
| size += add4ByteString("alis"); // Component subtype |
| size += add4ByteString("appl"); // Component manufacturer |
| size += addZeroWords(2); // Component flags+Component flags mask |
| size += addArbitraryString("Apple Alias Data Handler"); // Component name |
| addAtomEnd; |
| |
| addAtom(dinf); |
| size += addAtom_dref(); |
| addAtomEnd; |
| |
| addAtom(dref); |
| size += addWord(0x00000000); // Version + Flags |
| size += addWord(0x00000001); // Number of entries |
| size += addAtom_alis(); |
| addAtomEnd; |
| |
| addAtom(alis); |
| size += addWord(0x00000001); // Version + Flags |
| addAtomEnd; |
| |
| addAtom(stbl); |
| size += addAtom_stsd(); |
| size += addAtom_stts(); |
| if (fCurrentIOState->fQTcomponentSubtype == fourChar('v','i','d','e')) { |
| size += addAtom_stss(); // only for video streams |
| } |
| size += addAtom_stsc(); |
| size += addAtom_stsz(); |
| size += addAtom_co64(); |
| addAtomEnd; |
| |
| addAtom(stsd); |
| size += addWord(0x00000000); // Version+Flags |
| size += addWord(0x00000001); // Number of entries |
| SubsessionIOState::atomCreationFunc mediaDataAtomCreator |
| = fCurrentIOState->fQTMediaDataAtomCreator; |
| size += (this->*mediaDataAtomCreator)(); |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_genericMedia() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| |
| // Our source is assumed to be a "QuickTimeGenericRTPSource" |
| // Use its "sdAtom" state for our contents: |
| QuickTimeGenericRTPSource* rtpSource = (QuickTimeGenericRTPSource*) |
| fCurrentIOState->fOurSubsession.rtpSource(); |
| unsigned size = 0; |
| if (rtpSource != NULL) { |
| QuickTimeGenericRTPSource::QTState& qtState = rtpSource->qtState; |
| char const* from = qtState.sdAtom; |
| size = qtState.sdAtomSize; |
| for (unsigned i = 0; i < size; ++i) addByte(from[i]); |
| } |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_soundMediaGeneral() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| unsigned size = addAtomHeader(fCurrentIOState->fQTAudioDataType); |
| |
| // General sample description fields: |
| size += addWord(0x00000000); // Reserved |
| size += addWord(0x00000001); // Reserved+Data reference index |
| // Sound sample description fields: |
| unsigned short const version = fCurrentIOState->fQTSoundSampleVersion; |
| size += addWord(version<<16); // Version+Revision level |
| size += addWord(0x00000000); // Vendor |
| unsigned short numChannels |
| = (unsigned short)(fCurrentIOState->fOurSubsession.numChannels()); |
| size += addHalfWord(numChannels); // Number of channels |
| size += addHalfWord(0x0010); // Sample size |
| // size += addWord(0x00000000); // Compression ID+Packet size |
| size += addWord(0xfffe0000); // Compression ID+Packet size ##### |
| |
| unsigned const sampleRateFixedPoint = fCurrentIOState->fQTTimeScale << 16; |
| size += addWord(sampleRateFixedPoint); // Sample rate |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_Qclp() { |
| // The beginning of this atom looks just like a general Sound Media atom, |
| // except with a version field of 1: |
| int64_t initFilePosn = TellFile64(fOutFid); |
| fCurrentIOState->fQTAudioDataType = "Qclp"; |
| fCurrentIOState->fQTSoundSampleVersion = 1; |
| unsigned size = addAtom_soundMediaGeneral(); |
| |
| // Next, add the four fields that are particular to version 1: |
| // (Later, parameterize these #####) |
| size += addWord(0x000000a0); // samples per packet |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x00000002); // bytes per sample (uncompressed) |
| |
| // Other special fields are in a 'wave' atom that follows: |
| size += addAtom_wave(); |
| addAtomEnd; |
| |
| addAtom(wave); |
| size += addAtom_frma(); |
| if (strcmp(fCurrentIOState->fQTAudioDataType, "Qclp") == 0) { |
| size += addWord(0x00000014); // ??? |
| size += add4ByteString("Qclp"); // ??? |
| if (fCurrentIOState->fQTBytesPerFrame == 35) { |
| size += addAtom_Fclp(); // full-rate QCELP |
| } else { |
| size += addAtom_Hclp(); // half-rate QCELP |
| } // what about other QCELP 'rates'??? ##### |
| size += addWord(0x00000008); // ??? |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x00000008); // ??? |
| } else if (strcmp(fCurrentIOState->fQTAudioDataType, "mp4a") == 0) { |
| size += addWord(0x0000000c); // ??? |
| size += add4ByteString("mp4a"); // ??? |
| size += addWord(0x00000000); // ??? |
| size += addAtom_esds(); // ESDescriptor |
| size += addWord(0x00000008); // ??? |
| size += addWord(0x00000000); // ??? |
| } |
| addAtomEnd; |
| |
| addAtom(frma); |
| size += add4ByteString(fCurrentIOState->fQTAudioDataType); // ??? |
| addAtomEnd; |
| |
| addAtom(Fclp); |
| size += addWord(0x00000000); // ??? |
| addAtomEnd; |
| |
| addAtom(Hclp); |
| size += addWord(0x00000000); // ??? |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_mp4a() { |
| unsigned size = 0; |
| // The beginning of this atom looks just like a general Sound Media atom, |
| // except with a version field of 1: |
| int64_t initFilePosn = TellFile64(fOutFid); |
| fCurrentIOState->fQTAudioDataType = "mp4a"; |
| |
| if (fGenerateMP4Format) { |
| fCurrentIOState->fQTSoundSampleVersion = 0; |
| size = addAtom_soundMediaGeneral(); |
| size += addAtom_esds(); |
| } else { |
| fCurrentIOState->fQTSoundSampleVersion = 1; |
| size = addAtom_soundMediaGeneral(); |
| |
| // Next, add the four fields that are particular to version 1: |
| // (Later, parameterize these #####) |
| size += addWord(fCurrentIOState->fQTTimeUnitsPerSample); |
| size += addWord(0x00000001); // ??? |
| size += addWord(0x00000001); // ??? |
| size += addWord(0x00000002); // bytes per sample (uncompressed) |
| |
| // Other special fields are in a 'wave' atom that follows: |
| size += addAtom_wave(); |
| } |
| addAtomEnd; |
| |
| addAtom(esds); |
| //##### |
| MediaSubsession& subsession = fCurrentIOState->fOurSubsession; |
| if (strcmp(subsession.mediumName(), "audio") == 0) { |
| // MPEG-4 audio |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x03808080); // ??? |
| size += addWord(0x2a000000); // ??? |
| size += addWord(0x04808080); // ??? |
| size += addWord(0x1c401500); // ??? |
| size += addWord(0x18000000); // ??? |
| size += addWord(0x6d600000); // ??? |
| size += addWord(0x6d600580); // ??? |
| size += addByte(0x80); size += addByte(0x80); // ??? |
| } else if (strcmp(subsession.mediumName(), "video") == 0) { |
| // MPEG-4 video |
| size += addWord(0x00000000); // ??? |
| size += addWord(0x03330000); // ??? |
| size += addWord(0x1f042b20); // ??? |
| size += addWord(0x1104fd46); // ??? |
| size += addWord(0x000d4e10); // ??? |
| size += addWord(0x000d4e10); // ??? |
| size += addByte(0x05); // ??? |
| } |
| |
| // Add the source's 'config' information: |
| unsigned configSize; |
| unsigned char* config |
| = parseGeneralConfigStr(subsession.fmtp_config(), configSize); |
| size += addByte(configSize); |
| for (unsigned i = 0; i < configSize; ++i) { |
| size += addByte(config[i]); |
| } |
| delete[] config; |
| |
| if (strcmp(subsession.mediumName(), "audio") == 0) { |
| // MPEG-4 audio |
| size += addWord(0x06808080); // ??? |
| size += addHalfWord(0x0102); // ??? |
| } else { |
| // MPEG-4 video |
| size += addHalfWord(0x0601); // ??? |
| size += addByte(0x02); // ??? |
| } |
| //##### |
| addAtomEnd; |
| |
| addAtom(srcq); |
| //##### |
| size += addWord(0x00000040); // ??? |
| //##### |
| addAtomEnd; |
| |
| addAtom(h263); |
| // General sample description fields: |
| size += addWord(0x00000000); // Reserved |
| size += addWord(0x00000001); // Reserved+Data reference index |
| // Video sample description fields: |
| size += addWord(0x00020001); // Version+Revision level |
| size += add4ByteString("appl"); // Vendor |
| size += addWord(0x00000000); // Temporal quality |
| size += addWord(0x000002fc); // Spatial quality |
| unsigned const widthAndHeight = (fMovieWidth<<16)|fMovieHeight; |
| size += addWord(widthAndHeight); // Width+height |
| size += addWord(0x00480000); // Horizontal resolution |
| size += addWord(0x00480000); // Vertical resolution |
| size += addWord(0x00000000); // Data size |
| size += addWord(0x00010548); // Frame count+Compressor name (start) |
| // "H.263" |
| size += addWord(0x2e323633); // Compressor name (continued) |
| size += addZeroWords(6); // Compressor name (continued - zero) |
| size += addWord(0x00000018); // Compressor name (final)+Depth |
| size += addHalfWord(0xffff); // Color table id |
| addAtomEnd; |
| |
| addAtom(avc1); |
| // General sample description fields: |
| size += addWord(0x00000000); // Reserved |
| size += addWord(0x00000001); // Reserved+Data reference index |
| // Video sample description fields: |
| size += addWord(0x00000000); // Version+Revision level |
| size += add4ByteString("appl"); // Vendor |
| size += addWord(0x00000000); // Temporal quality |
| size += addWord(0x00000000); // Spatial quality |
| unsigned const widthAndHeight = (fMovieWidth<<16)|fMovieHeight; |
| size += addWord(widthAndHeight); // Width+height |
| size += addWord(0x00480000); // Horizontal resolution |
| size += addWord(0x00480000); // Vertical resolution |
| size += addWord(0x00000000); // Data size |
| size += addWord(0x00010548); // Frame count+Compressor name (start) |
| // "H.264" |
| size += addWord(0x2e323634); // Compressor name (continued) |
| size += addZeroWords(6); // Compressor name (continued - zero) |
| size += addWord(0x00000018); // Compressor name (final)+Depth |
| size += addHalfWord(0xffff); // Color table id |
| size += addAtom_avcC(); |
| addAtomEnd; |
| |
| addAtom(avcC); |
| // Begin by Base-64 decoding the "sprop" parameter sets strings: |
| char* psets = strDup(fCurrentIOState->fOurSubsession.fmtp_spropparametersets()); |
| if (psets == NULL) return 0; |
| |
| size_t comma_pos = strcspn(psets, ","); |
| psets[comma_pos] = '\0'; |
| char const* sps_b64 = psets; |
| char const* pps_b64 = &psets[comma_pos+1]; |
| unsigned sps_count; |
| unsigned char* sps_data = base64Decode(sps_b64, sps_count, false); |
| unsigned pps_count; |
| unsigned char* pps_data = base64Decode(pps_b64, pps_count, false); |
| |
| // Then add the decoded data: |
| size += addByte(0x01); // configuration version |
| size += addByte(sps_data[1]); // profile |
| size += addByte(sps_data[2]); // profile compat |
| size += addByte(sps_data[3]); // level |
| size += addByte(0xff); /* 0b11111100 | lengthsize = 0x11 */ |
| size += addByte(0xe0 | (sps_count > 0 ? 1 : 0) ); |
| if (sps_count > 0) { |
| size += addHalfWord(sps_count); |
| for (unsigned i = 0; i < sps_count; i++) { |
| size += addByte(sps_data[i]); |
| } |
| } |
| size += addByte(pps_count > 0 ? 1 : 0); |
| if (pps_count > 0) { |
| size += addHalfWord(pps_count); |
| for (unsigned i = 0; i < pps_count; i++) { |
| size += addByte(pps_data[i]); |
| } |
| } |
| |
| // Finally, delete the data that we allocated: |
| delete[] pps_data; delete[] sps_data; |
| delete[] psets; |
| addAtomEnd; |
| |
| addAtom(mp4v); |
| // General sample description fields: |
| size += addWord(0x00000000); // Reserved |
| size += addWord(0x00000001); // Reserved+Data reference index |
| // Video sample description fields: |
| size += addWord(0x00020001); // Version+Revision level |
| size += add4ByteString("appl"); // Vendor |
| size += addWord(0x00000200); // Temporal quality |
| size += addWord(0x00000400); // Spatial quality |
| unsigned const widthAndHeight = (fMovieWidth<<16)|fMovieHeight; |
| size += addWord(widthAndHeight); // Width+height |
| size += addWord(0x00480000); // Horizontal resolution |
| size += addWord(0x00480000); // Vertical resolution |
| size += addWord(0x00000000); // Data size |
| size += addWord(0x00010c4d); // Frame count+Compressor name (start) |
| // "MPEG-4 Video" |
| size += addWord(0x5045472d); // Compressor name (continued) |
| size += addWord(0x34205669); // Compressor name (continued) |
| size += addWord(0x64656f00); // Compressor name (continued) |
| size += addZeroWords(4); // Compressor name (continued - zero) |
| size += addWord(0x00000018); // Compressor name (final)+Depth |
| size += addHalfWord(0xffff); // Color table id |
| size += addAtom_esds(); // ESDescriptor |
| size += addWord(0x00000000); // ??? |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_rtp() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| unsigned size = addAtomHeader("rtp "); |
| |
| size += addWord(0x00000000); // Reserved (1st 4 bytes) |
| size += addWord(0x00000001); // Reserved (last 2 bytes) + Data ref index |
| size += addWord(0x00010001); // Hint track version + Last compat htv |
| size += addWord(1450); // Max packet size |
| |
| size += addAtom_tims(); |
| addAtomEnd; |
| |
| addAtom(tims); |
| size += addWord(fCurrentIOState->fOurSubsession.rtpTimestampFrequency()); |
| addAtomEnd; |
| |
| addAtom(stts); // Time-to-Sample |
| size += addWord(0x00000000); // Version+flags |
| |
| // First, add a dummy "Number of entries" field |
| // (and remember its position). We'll fill this field in later: |
| int64_t numEntriesPosition = TellFile64(fOutFid); |
| size += addWord(0); // dummy for "Number of entries" |
| |
| // Then, run through the chunk descriptors, and enter the entries |
| // in this (compressed) Time-to-Sample table: |
| unsigned numEntries = 0, numSamplesSoFar = 0; |
| unsigned prevSampleDuration = 0; |
| unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame; |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| unsigned const sampleDuration = chunk->fFrameDuration/samplesPerFrame; |
| if (sampleDuration != prevSampleDuration) { |
| // This chunk will start a new table entry, |
| // so write out the old one (if any): |
| if (chunk != fCurrentIOState->fHeadChunk) { |
| ++numEntries; |
| size += addWord(numSamplesSoFar); // Sample count |
| size += addWord(prevSampleDuration); // Sample duration |
| numSamplesSoFar = 0; |
| } |
| } |
| |
| unsigned const numSamples = chunk->fNumFrames*samplesPerFrame; |
| numSamplesSoFar += numSamples; |
| prevSampleDuration = sampleDuration; |
| chunk = chunk->fNextChunk; |
| } |
| |
| // Then, write out the last entry: |
| ++numEntries; |
| size += addWord(numSamplesSoFar); // Sample count |
| size += addWord(prevSampleDuration); // Sample duration |
| |
| // Now go back and fill in the "Number of entries" field: |
| setWord(numEntriesPosition, numEntries); |
| addAtomEnd; |
| |
| addAtom(stss); // Sync-Sample |
| size += addWord(0x00000000); // Version+flags |
| |
| // First, add a dummy "Number of entries" field |
| // (and remember its position). We'll fill this field in later: |
| int64_t numEntriesPosition = TellFile64(fOutFid); |
| size += addWord(0); // dummy for "Number of entries" |
| |
| unsigned numEntries = 0, numSamplesSoFar = 0; |
| if (fCurrentIOState->fHeadSyncFrame != NULL) { |
| SyncFrame* currentSyncFrame = fCurrentIOState->fHeadSyncFrame; |
| |
| // First, count the number of frames (to use as a sanity check; see below): |
| unsigned totNumFrames = 0; |
| for (ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; chunk != NULL; chunk = chunk->fNextChunk) totNumFrames += chunk->fNumFrames; |
| |
| while (currentSyncFrame != NULL) { |
| if (currentSyncFrame->sfFrameNum >= totNumFrames) break; // sanity check |
| |
| ++numEntries; |
| size += addWord(currentSyncFrame->sfFrameNum); |
| currentSyncFrame = currentSyncFrame->nextSyncFrame; |
| } |
| } else { |
| // First, run through the chunk descriptors, counting up the total number of samples: |
| unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame; |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| unsigned const numSamples = chunk->fNumFrames*samplesPerFrame; |
| numSamplesSoFar += numSamples; |
| chunk = chunk->fNextChunk; |
| } |
| |
| // Then, write out the sample numbers that we deem correspond to 'sync samples': |
| unsigned i; |
| for (i = 0; i < numSamplesSoFar; i += 12) { |
| // For an explanation of the constant "12", see http://lists.live555.com/pipermail/live-devel/2009-July/010969.html |
| // (Perhaps we should really try to keep track of which 'samples' ('frames' for video) really are 'key frames'?) |
| size += addWord(i+1); |
| ++numEntries; |
| } |
| |
| // Then, write out the last entry (if we haven't already done so): |
| if (i != (numSamplesSoFar - 1)) { |
| size += addWord(numSamplesSoFar); |
| ++numEntries; |
| } |
| } |
| |
| // Now go back and fill in the "Number of entries" field: |
| setWord(numEntriesPosition, numEntries); |
| addAtomEnd; |
| |
| addAtom(stsc); // Sample-to-Chunk |
| size += addWord(0x00000000); // Version+flags |
| |
| // First, add a dummy "Number of entries" field |
| // (and remember its position). We'll fill this field in later: |
| int64_t numEntriesPosition = TellFile64(fOutFid); |
| size += addWord(0); // dummy for "Number of entries" |
| |
| // Then, run through the chunk descriptors, and enter the entries |
| // in this (compressed) Sample-to-Chunk table: |
| unsigned numEntries = 0, chunkNumber = 0; |
| unsigned prevSamplesPerChunk = ~0; |
| unsigned const samplesPerFrame = fCurrentIOState->fQTSamplesPerFrame; |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| ++chunkNumber; |
| unsigned const samplesPerChunk = chunk->fNumFrames*samplesPerFrame; |
| if (samplesPerChunk != prevSamplesPerChunk) { |
| // This chunk will be a new table entry: |
| ++numEntries; |
| size += addWord(chunkNumber); // Chunk number |
| size += addWord(samplesPerChunk); // Samples per chunk |
| size += addWord(0x00000001); // Sample description ID |
| |
| prevSamplesPerChunk = samplesPerChunk; |
| } |
| chunk = chunk->fNextChunk; |
| } |
| |
| // Now go back and fill in the "Number of entries" field: |
| setWord(numEntriesPosition, numEntries); |
| addAtomEnd; |
| |
| addAtom(stsz); // Sample Size |
| size += addWord(0x00000000); // Version+flags |
| |
| // Begin by checking whether our chunks all have the same |
| // 'bytes-per-sample'. This determines whether this atom's table |
| // has just a single entry, or multiple entries. |
| Boolean haveSingleEntryTable = True; |
| double firstBPS = 0.0; |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| double bps |
| = (double)(chunk->fFrameSize)/(fCurrentIOState->fQTSamplesPerFrame); |
| if (bps < 1.0) { |
| // I don't think a multiple-entry table would make sense in |
| // this case, so assume a single entry table ??? ##### |
| break; |
| } |
| |
| if (firstBPS == 0.0) { |
| firstBPS = bps; |
| } else if (bps != firstBPS) { |
| haveSingleEntryTable = False; |
| break; |
| } |
| |
| chunk = chunk->fNextChunk; |
| } |
| |
| unsigned sampleSize; |
| if (haveSingleEntryTable) { |
| if (fCurrentIOState->isHintTrack() |
| && fCurrentIOState->fHeadChunk != NULL) { |
| sampleSize = fCurrentIOState->fHeadChunk->fFrameSize |
| / fCurrentIOState->fQTSamplesPerFrame; |
| } else { |
| // The following doesn't seem right, but seems to do the right thing: |
| sampleSize = fCurrentIOState->fQTTimeUnitsPerSample; //??? |
| } |
| } else { |
| sampleSize = 0; // indicates a multiple-entry table |
| } |
| size += addWord(sampleSize); // Sample size |
| unsigned const totNumSamples = fCurrentIOState->fQTTotNumSamples; |
| size += addWord(totNumSamples); // Number of entries |
| |
| if (!haveSingleEntryTable) { |
| // Multiple-entry table: |
| // Run through the chunk descriptors, entering the sample sizes: |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| unsigned numSamples |
| = chunk->fNumFrames*(fCurrentIOState->fQTSamplesPerFrame); |
| unsigned sampleSize |
| = chunk->fFrameSize/(fCurrentIOState->fQTSamplesPerFrame); |
| for (unsigned i = 0; i < numSamples; ++i) { |
| size += addWord(sampleSize); |
| } |
| |
| chunk = chunk->fNextChunk; |
| } |
| } |
| addAtomEnd; |
| |
| addAtom(co64); // Chunk Offset |
| size += addWord(0x00000000); // Version+flags |
| size += addWord(fCurrentIOState->fNumChunks); // Number of entries |
| |
| // Run through the chunk descriptors, entering the file offsets: |
| ChunkDescriptor* chunk = fCurrentIOState->fHeadChunk; |
| while (chunk != NULL) { |
| size += addWord64(chunk->fOffsetInFile); |
| |
| chunk = chunk->fNextChunk; |
| } |
| addAtomEnd; |
| |
| addAtom(udta); |
| size += addAtom_name(); |
| size += addAtom_hnti(); |
| size += addAtom_hinf(); |
| addAtomEnd; |
| |
| addAtom(name); |
| char description[100]; |
| sprintf(description, "Hinted %s track", |
| fCurrentIOState->fOurSubsession.mediumName()); |
| size += addArbitraryString(description, False); // name of object |
| addAtomEnd; |
| |
| addAtom(hnti); |
| size += addAtom_sdp(); |
| addAtomEnd; |
| |
| unsigned QuickTimeFileSink::addAtom_sdp() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| unsigned size = addAtomHeader("sdp "); |
| |
| // Add this subsession's SDP lines: |
| char const* sdpLines = fCurrentIOState->fOurSubsession.savedSDPLines(); |
| // We need to change any "a=control:trackID=" values to be this |
| // track's actual track id: |
| char* newSDPLines = new char[strlen(sdpLines)+100/*overkill*/]; |
| char const* searchStr = "a=control:trackid="; |
| Boolean foundSearchString = False; |
| char const *p1, *p2, *p3; |
| for (p1 = sdpLines; *p1 != '\0'; ++p1) { |
| for (p2 = p1,p3 = searchStr; tolower(*p2) == *p3; ++p2,++p3) {} |
| if (*p3 == '\0') { |
| // We found the end of the search string, at p2. |
| int beforeTrackNumPosn = p2-sdpLines; |
| // Look for the subsequent track number, and skip over it: |
| int trackNumLength; |
| if (sscanf(p2, " %*d%n", &trackNumLength) < 0) break; |
| int afterTrackNumPosn = beforeTrackNumPosn + trackNumLength; |
| |
| // Replace the old track number with the correct one: |
| int i; |
| for (i = 0; i < beforeTrackNumPosn; ++i) newSDPLines[i] = sdpLines[i]; |
| sprintf(&newSDPLines[i], "%d", fCurrentIOState->fTrackID); |
| i = afterTrackNumPosn; |
| int j = i + strlen(&newSDPLines[i]); |
| while (1) { |
| if ((newSDPLines[j] = sdpLines[i]) == '\0') break; |
| ++i; ++j; |
| } |
| |
| foundSearchString = True; |
| break; |
| } |
| } |
| |
| if (!foundSearchString) { |
| // Because we didn't find a "a=control:trackID=<trackId>" line, |
| // add one of our own: |
| sprintf(newSDPLines, "%s%s%d\r\n", |
| sdpLines, searchStr, fCurrentIOState->fTrackID); |
| } |
| |
| size += addArbitraryString(newSDPLines, False); |
| delete[] newSDPLines; |
| addAtomEnd; |
| |
| addAtom(hinf); |
| size += addAtom_totl(); |
| size += addAtom_npck(); |
| size += addAtom_tpay(); |
| size += addAtom_trpy(); |
| size += addAtom_nump(); |
| size += addAtom_tpyl(); |
| // Is 'maxr' required? ##### |
| size += addAtom_dmed(); |
| size += addAtom_dimm(); |
| size += addAtom_drep(); |
| size += addAtom_tmin(); |
| size += addAtom_tmax(); |
| size += addAtom_pmax(); |
| size += addAtom_dmax(); |
| size += addAtom_payt(); |
| addAtomEnd; |
| |
| addAtom(totl); |
| size += addWord(fCurrentIOState->fHINF.trpy.lo); |
| addAtomEnd; |
| |
| addAtom(npck); |
| size += addWord(fCurrentIOState->fHINF.nump.lo); |
| addAtomEnd; |
| |
| addAtom(tpay); |
| size += addWord(fCurrentIOState->fHINF.tpyl.lo); |
| addAtomEnd; |
| |
| addAtom(trpy); |
| size += addWord(fCurrentIOState->fHINF.trpy.hi); |
| size += addWord(fCurrentIOState->fHINF.trpy.lo); |
| addAtomEnd; |
| |
| addAtom(nump); |
| size += addWord(fCurrentIOState->fHINF.nump.hi); |
| size += addWord(fCurrentIOState->fHINF.nump.lo); |
| addAtomEnd; |
| |
| addAtom(tpyl); |
| size += addWord(fCurrentIOState->fHINF.tpyl.hi); |
| size += addWord(fCurrentIOState->fHINF.tpyl.lo); |
| addAtomEnd; |
| |
| addAtom(dmed); |
| size += addWord(fCurrentIOState->fHINF.dmed.hi); |
| size += addWord(fCurrentIOState->fHINF.dmed.lo); |
| addAtomEnd; |
| |
| addAtom(dimm); |
| size += addWord(fCurrentIOState->fHINF.dimm.hi); |
| size += addWord(fCurrentIOState->fHINF.dimm.lo); |
| addAtomEnd; |
| |
| addAtom(drep); |
| size += addWord(0); |
| size += addWord(0); |
| addAtomEnd; |
| |
| addAtom(tmin); |
| size += addWord(0); |
| addAtomEnd; |
| |
| addAtom(tmax); |
| size += addWord(0); |
| addAtomEnd; |
| |
| addAtom(pmax); |
| size += addWord(fCurrentIOState->fHINF.pmax); |
| addAtomEnd; |
| |
| addAtom(dmax); |
| size += addWord(fCurrentIOState->fHINF.dmax); |
| addAtomEnd; |
| |
| addAtom(payt); |
| MediaSubsession& ourSubsession = fCurrentIOState->fOurSubsession; |
| RTPSource* rtpSource = ourSubsession.rtpSource(); |
| if (rtpSource != NULL) { |
| size += addWord(rtpSource->rtpPayloadFormat()); |
| |
| // Also, add a 'rtpmap' string: <mime-subtype>/<rtp-frequency> |
| unsigned rtpmapStringLength = strlen(ourSubsession.codecName()) + 20; |
| char* rtpmapString = new char[rtpmapStringLength]; |
| sprintf(rtpmapString, "%s/%d", |
| ourSubsession.codecName(), rtpSource->timestampFrequency()); |
| size += addArbitraryString(rtpmapString); |
| delete[] rtpmapString; |
| } |
| addAtomEnd; |
| |
| // A dummy atom (with name "????"): |
| unsigned QuickTimeFileSink::addAtom_dummy() { |
| int64_t initFilePosn = TellFile64(fOutFid); |
| unsigned size = addAtomHeader("????"); |
| addAtomEnd; |