| /********** |
| 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 class that encapsulates a Matroska file. |
| // Implementation |
| |
| #include "MatroskaFileParser.hh" |
| #include "MatroskaDemuxedTrack.hh" |
| #include <ByteStreamFileSource.hh> |
| #include <H264VideoStreamDiscreteFramer.hh> |
| #include <H265VideoStreamDiscreteFramer.hh> |
| #include <MPEG1or2AudioRTPSink.hh> |
| #include <MPEG4GenericRTPSink.hh> |
| #include <AC3AudioRTPSink.hh> |
| #include <SimpleRTPSink.hh> |
| #include <VorbisAudioRTPSink.hh> |
| #include <H264VideoRTPSink.hh> |
| #include <H265VideoRTPSink.hh> |
| #include <VP8VideoRTPSink.hh> |
| #include <VP9VideoRTPSink.hh> |
| #include <TheoraVideoRTPSink.hh> |
| #include <RawVideoRTPSink.hh> |
| #include <T140TextRTPSink.hh> |
| #include <Base64.hh> |
| #include <H264VideoFileSink.hh> |
| #include <H265VideoFileSink.hh> |
| #include <AMRAudioFileSink.hh> |
| #include <OggFileSink.hh> |
| |
| ////////// CuePoint definition ////////// |
| |
| class CuePoint { |
| public: |
| CuePoint(double cueTime, u_int64_t clusterOffsetInFile, unsigned blockNumWithinCluster/* 1-based */); |
| virtual ~CuePoint(); |
| |
| static void addCuePoint(CuePoint*& root, double cueTime, u_int64_t clusterOffsetInFile, unsigned blockNumWithinCluster/* 1-based */, |
| Boolean& needToReviseBalanceOfParent); |
| // If "cueTime" == "root.fCueTime", replace the existing data, otherwise add to the left or right subtree. |
| // (Note that this is a static member function because - as a result of tree rotation - "root" might change.) |
| |
| Boolean lookup(double& cueTime, u_int64_t& resultClusterOffsetInFile, unsigned& resultBlockNumWithinCluster); |
| |
| static void fprintf(FILE* fid, CuePoint* cuePoint); // used for debugging; it's static to allow for "cuePoint == NULL" |
| |
| private: |
| // The "CuePoint" tree is implemented as an AVL Tree, to keep it balanced (for efficient lookup). |
| CuePoint* fSubTree[2]; // 0 => left; 1 => right |
| CuePoint* left() const { return fSubTree[0]; } |
| CuePoint* right() const { return fSubTree[1]; } |
| char fBalance; // height of right subtree - height of left subtree |
| |
| static void rotate(unsigned direction/*0 => left; 1 => right*/, CuePoint*& root); // used to keep the tree in balance |
| |
| double fCueTime; |
| u_int64_t fClusterOffsetInFile; |
| unsigned fBlockNumWithinCluster; // 0-based |
| }; |
| |
| UsageEnvironment& operator<<(UsageEnvironment& env, const CuePoint* cuePoint); // used for debugging |
| |
| |
| ////////// MatroskaTrackTable definition ///////// |
| |
| // For looking up and iterating over the file's tracks: |
| class MatroskaTrackTable { |
| public: |
| MatroskaTrackTable(); |
| virtual ~MatroskaTrackTable(); |
| |
| void add(MatroskaTrack* newTrack, unsigned trackNumber); |
| MatroskaTrack* lookup(unsigned trackNumber); |
| |
| unsigned numTracks() const; |
| |
| class Iterator { |
| public: |
| Iterator(MatroskaTrackTable& ourTable); |
| virtual ~Iterator(); |
| MatroskaTrack* next(); |
| private: |
| HashTable::Iterator* fIter; |
| }; |
| |
| private: |
| friend class Iterator; |
| HashTable* fTable; |
| }; |
| |
| |
| |
| ////////// MatroskaFile implementation ////////// |
| |
| void MatroskaFile |
| ::createNew(UsageEnvironment& env, char const* fileName, onCreationFunc* onCreation, void* onCreationClientData, |
| char const* preferredLanguage) { |
| new MatroskaFile(env, fileName, onCreation, onCreationClientData, preferredLanguage); |
| } |
| |
| MatroskaFile::MatroskaFile(UsageEnvironment& env, char const* fileName, onCreationFunc* onCreation, void* onCreationClientData, |
| char const* preferredLanguage) |
| : Medium(env), |
| fFileName(strDup(fileName)), fOnCreation(onCreation), fOnCreationClientData(onCreationClientData), |
| fPreferredLanguage(strDup(preferredLanguage)), |
| fTimecodeScale(1000000), fSegmentDuration(0.0), fSegmentDataOffset(0), fClusterOffset(0), fCuesOffset(0), fCuePoints(NULL), |
| fChosenVideoTrackNumber(0), fChosenAudioTrackNumber(0), fChosenSubtitleTrackNumber(0) { |
| fTrackTable = new MatroskaTrackTable; |
| fDemuxesTable = HashTable::create(ONE_WORD_HASH_KEYS); |
| |
| FramedSource* inputSource = ByteStreamFileSource::createNew(envir(), fileName); |
| if (inputSource == NULL) { |
| // The specified input file does not exist! |
| fParserForInitialization = NULL; |
| handleEndOfTrackHeaderParsing(); // we have no file, and thus no tracks, but we still need to signal this |
| } else { |
| // Initialize ourselves by parsing the file's 'Track' headers: |
| fParserForInitialization = new MatroskaFileParser(*this, inputSource, handleEndOfTrackHeaderParsing, this, NULL); |
| } |
| } |
| |
| MatroskaFile::~MatroskaFile() { |
| delete fParserForInitialization; |
| delete fCuePoints; |
| |
| // Delete any outstanding "MatroskaDemux"s, and the table for them: |
| MatroskaDemux* demux; |
| while ((demux = (MatroskaDemux*)fDemuxesTable->RemoveNext()) != NULL) { |
| delete demux; |
| } |
| delete fDemuxesTable; |
| delete fTrackTable; |
| |
| delete[] (char*)fPreferredLanguage; |
| delete[] (char*)fFileName; |
| } |
| |
| void MatroskaFile::handleEndOfTrackHeaderParsing(void* clientData) { |
| ((MatroskaFile*)clientData)->handleEndOfTrackHeaderParsing(); |
| } |
| |
| class TrackChoiceRecord { |
| public: |
| unsigned trackNumber; |
| u_int8_t trackType; |
| unsigned choiceFlags; |
| }; |
| |
| void MatroskaFile::handleEndOfTrackHeaderParsing() { |
| // Having parsed all of our track headers, iterate through the tracks to figure out which ones should be played. |
| // The Matroska 'specification' is rather imprecise about this (as usual). However, we use the following algorithm: |
| // - Use one (but no more) enabled track of each type (video, audio, subtitle). (Ignore all tracks that are not 'enabled'.) |
| // - For each track type, choose the one that's 'forced'. |
| // - If more than one is 'forced', choose the first one that matches our preferred language, or the first if none matches. |
| // - If none is 'forced', choose the one that's 'default'. |
| // - If more than one is 'default', choose the first one that matches our preferred language, or the first if none matches. |
| // - If none is 'default', choose the first one that matches our preferred language, or the first if none matches. |
| unsigned numTracks = fTrackTable->numTracks(); |
| if (numTracks > 0) { |
| TrackChoiceRecord* trackChoice = new TrackChoiceRecord[numTracks]; |
| unsigned numEnabledTracks = 0; |
| MatroskaTrackTable::Iterator iter(*fTrackTable); |
| MatroskaTrack* track; |
| while ((track = iter.next()) != NULL) { |
| if (!track->isEnabled || track->trackType == 0 || track->mimeType[0] == '\0') continue; // track not enabled, or not fully-defined |
| |
| trackChoice[numEnabledTracks].trackNumber = track->trackNumber; |
| trackChoice[numEnabledTracks].trackType = track->trackType; |
| |
| // Assign flags for this track so that, when sorted, the largest value becomes our choice: |
| unsigned choiceFlags = 0; |
| if (fPreferredLanguage != NULL && track->language != NULL && strcmp(fPreferredLanguage, track->language) == 0) { |
| // This track matches our preferred language: |
| choiceFlags |= 1; |
| } |
| if (track->isForced) { |
| choiceFlags |= 4; |
| } else if (track->isDefault) { |
| choiceFlags |= 2; |
| } |
| trackChoice[numEnabledTracks].choiceFlags = choiceFlags; |
| |
| ++numEnabledTracks; |
| } |
| |
| // Choose the desired track for each track type: |
| for (u_int8_t trackType = 0x01; trackType != MATROSKA_TRACK_TYPE_OTHER; trackType <<= 1) { |
| int bestNum = -1; |
| int bestChoiceFlags = -1; |
| for (unsigned i = 0; i < numEnabledTracks; ++i) { |
| if (trackChoice[i].trackType == trackType && (int)trackChoice[i].choiceFlags > bestChoiceFlags) { |
| bestNum = i; |
| bestChoiceFlags = (int)trackChoice[i].choiceFlags; |
| } |
| } |
| if (bestChoiceFlags >= 0) { // There is a track for this track type |
| if (trackType == MATROSKA_TRACK_TYPE_VIDEO) fChosenVideoTrackNumber = trackChoice[bestNum].trackNumber; |
| else if (trackType == MATROSKA_TRACK_TYPE_AUDIO) fChosenAudioTrackNumber = trackChoice[bestNum].trackNumber; |
| else fChosenSubtitleTrackNumber = trackChoice[bestNum].trackNumber; |
| } |
| } |
| |
| delete[] trackChoice; |
| } |
| |
| #ifdef DEBUG |
| if (fChosenVideoTrackNumber > 0) fprintf(stderr, "Chosen video track: #%d\n", fChosenVideoTrackNumber); else fprintf(stderr, "No chosen video track\n"); |
| if (fChosenAudioTrackNumber > 0) fprintf(stderr, "Chosen audio track: #%d\n", fChosenAudioTrackNumber); else fprintf(stderr, "No chosen audio track\n"); |
| if (fChosenSubtitleTrackNumber > 0) fprintf(stderr, "Chosen subtitle track: #%d\n", fChosenSubtitleTrackNumber); else fprintf(stderr, "No chosen subtitle track\n"); |
| #endif |
| |
| // Delete our parser, because it's done its job now: |
| delete fParserForInitialization; fParserForInitialization = NULL; |
| |
| // Finally, signal our caller that we've been created and initialized: |
| if (fOnCreation != NULL) (*fOnCreation)(this, fOnCreationClientData); |
| } |
| |
| MatroskaTrack* MatroskaFile::lookup(unsigned trackNumber) const { |
| return fTrackTable->lookup(trackNumber); |
| } |
| |
| MatroskaDemux* MatroskaFile::newDemux() { |
| MatroskaDemux* demux = new MatroskaDemux(*this); |
| fDemuxesTable->Add((char const*)demux, demux); |
| |
| return demux; |
| } |
| |
| void MatroskaFile::removeDemux(MatroskaDemux* demux) { |
| fDemuxesTable->Remove((char const*)demux); |
| } |
| |
| #define getPrivByte(b) if (n == 0) break; else do {b = *p++; --n;} while (0) /* Vorbis/Theora configuration header parsing */ |
| #define CHECK_PTR if (ptr >= limit) break /* H.264/H.265 parsing */ |
| #define NUM_BYTES_REMAINING (unsigned)(limit - ptr) /* H.264/H.265 parsing */ |
| |
| void MatroskaFile::getH264ConfigData(MatroskaTrack const* track, |
| u_int8_t*& sps, unsigned& spsSize, |
| u_int8_t*& pps, unsigned& ppsSize) { |
| sps = pps = NULL; |
| spsSize = ppsSize = 0; |
| |
| do { |
| if (track == NULL) break; |
| |
| // Use our track's 'Codec Private' data: Bytes 5 and beyond contain SPS and PPSs: |
| if (track->codecPrivateSize < 6) break; |
| u_int8_t* SPSandPPSBytes = &track->codecPrivate[5]; |
| unsigned numSPSandPPSBytes = track->codecPrivateSize - 5; |
| |
| // Extract, from "SPSandPPSBytes", one SPS NAL unit, and one PPS NAL unit. |
| // (I hope one is all we need of each.) |
| unsigned i; |
| u_int8_t* ptr = SPSandPPSBytes; |
| u_int8_t* limit = &SPSandPPSBytes[numSPSandPPSBytes]; |
| |
| unsigned numSPSs = (*ptr++)&0x1F; CHECK_PTR; |
| for (i = 0; i < numSPSs; ++i) { |
| unsigned spsSize1 = (*ptr++)<<8; CHECK_PTR; |
| spsSize1 |= *ptr++; CHECK_PTR; |
| |
| if (spsSize1 > NUM_BYTES_REMAINING) break; |
| u_int8_t nal_unit_type = ptr[0]&0x1F; |
| if (sps == NULL && nal_unit_type == 7/*sanity check*/) { // save the first one |
| spsSize = spsSize1; |
| sps = new u_int8_t[spsSize]; |
| memmove(sps, ptr, spsSize); |
| } |
| ptr += spsSize1; |
| } |
| |
| unsigned numPPSs = (*ptr++)&0x1F; CHECK_PTR; |
| for (i = 0; i < numPPSs; ++i) { |
| unsigned ppsSize1 = (*ptr++)<<8; CHECK_PTR; |
| ppsSize1 |= *ptr++; CHECK_PTR; |
| |
| if (ppsSize1 > NUM_BYTES_REMAINING) break; |
| u_int8_t nal_unit_type = ptr[0]&0x1F; |
| if (pps == NULL && nal_unit_type == 8/*sanity check*/) { // save the first one |
| ppsSize = ppsSize1; |
| pps = new u_int8_t[ppsSize]; |
| memmove(pps, ptr, ppsSize); |
| } |
| ptr += ppsSize1; |
| } |
| |
| return; |
| } while (0); |
| |
| // An error occurred: |
| delete[] sps; sps = NULL; spsSize = 0; |
| delete[] pps; pps = NULL; ppsSize = 0; |
| } |
| |
| void MatroskaFile::getH265ConfigData(MatroskaTrack const* track, |
| u_int8_t*& vps, unsigned& vpsSize, |
| u_int8_t*& sps, unsigned& spsSize, |
| u_int8_t*& pps, unsigned& ppsSize) { |
| vps = sps = pps = NULL; |
| vpsSize = spsSize = ppsSize = 0; |
| |
| do { |
| if (track == NULL) break; |
| |
| u_int8_t* VPS_SPS_PPSBytes = NULL; unsigned numVPS_SPS_PPSBytes = 0; |
| unsigned i; |
| |
| if (track->codecPrivateUsesH264FormatForH265) { |
| // The data uses the H.264-style format (but including VPS NAL unit(s)). |
| // The VPS,SPS,PPS NAL unit information starts at byte #5: |
| if (track->codecPrivateSize >= 6) { |
| numVPS_SPS_PPSBytes = track->codecPrivateSize - 5; |
| VPS_SPS_PPSBytes = &track->codecPrivate[5]; |
| } |
| } else { |
| // The data uses the proper H.265-style format. |
| // The VPS,SPS,PPS NAL unit information starts at byte #22: |
| if (track->codecPrivateSize >= 23) { |
| numVPS_SPS_PPSBytes = track->codecPrivateSize - 22; |
| VPS_SPS_PPSBytes = &track->codecPrivate[22]; |
| } |
| } |
| if (VPS_SPS_PPSBytes == NULL) break; // no VPS,SPS,PPS NAL unit information was present |
| |
| // Extract, from "VPS_SPS_PPSBytes", one VPS NAL unit, one SPS NAL unit, and one PPS NAL unit. |
| // (I hope one is all we need of each.) |
| u_int8_t* ptr = VPS_SPS_PPSBytes; |
| u_int8_t* limit = &VPS_SPS_PPSBytes[numVPS_SPS_PPSBytes]; |
| |
| if (track->codecPrivateUsesH264FormatForH265) { |
| // The data uses the H.264-style format (but including VPS NAL unit(s)). |
| while (NUM_BYTES_REMAINING > 0) { |
| unsigned numNALUnits = (*ptr++)&0x1F; CHECK_PTR; |
| for (i = 0; i < numNALUnits; ++i) { |
| unsigned nalUnitLength = (*ptr++)<<8; CHECK_PTR; |
| nalUnitLength |= *ptr++; CHECK_PTR; |
| |
| if (nalUnitLength > NUM_BYTES_REMAINING) break; |
| u_int8_t nal_unit_type = (ptr[0]&0x7E)>>1; |
| if (nal_unit_type == 32) { // VPS |
| vpsSize = nalUnitLength; |
| delete[] vps; vps = new u_int8_t[nalUnitLength]; |
| memmove(vps, ptr, nalUnitLength); |
| } else if (nal_unit_type == 33) { // SPS |
| spsSize = nalUnitLength; |
| delete[] sps; sps = new u_int8_t[nalUnitLength]; |
| memmove(sps, ptr, nalUnitLength); |
| } else if (nal_unit_type == 34) { // PPS |
| ppsSize = nalUnitLength; |
| delete[] pps; pps = new u_int8_t[nalUnitLength]; |
| memmove(pps, ptr, nalUnitLength); |
| } |
| ptr += nalUnitLength; |
| } |
| } |
| } else { |
| // The data uses the proper H.265-style format. |
| unsigned numOfArrays = *ptr++; CHECK_PTR; |
| for (unsigned j = 0; j < numOfArrays; ++j) { |
| ++ptr; CHECK_PTR; // skip the 'array_completeness'|'reserved'|'NAL_unit_type' byte |
| |
| unsigned numNalus = (*ptr++)<<8; CHECK_PTR; |
| numNalus |= *ptr++; CHECK_PTR; |
| |
| for (i = 0; i < numNalus; ++i) { |
| unsigned nalUnitLength = (*ptr++)<<8; CHECK_PTR; |
| nalUnitLength |= *ptr++; CHECK_PTR; |
| |
| if (nalUnitLength > NUM_BYTES_REMAINING) break; |
| u_int8_t nal_unit_type = (ptr[0]&0x7E)>>1; |
| if (nal_unit_type == 32) { // VPS |
| vpsSize = nalUnitLength; |
| delete[] vps; vps = new u_int8_t[nalUnitLength]; |
| memmove(vps, ptr, nalUnitLength); |
| } else if (nal_unit_type == 33) { // SPS |
| spsSize = nalUnitLength; |
| delete[] sps; sps = new u_int8_t[nalUnitLength]; |
| memmove(sps, ptr, nalUnitLength); |
| } else if (nal_unit_type == 34) { // PPS |
| ppsSize = nalUnitLength; |
| delete[] pps; pps = new u_int8_t[nalUnitLength]; |
| memmove(pps, ptr, nalUnitLength); |
| } |
| ptr += nalUnitLength; |
| } |
| } |
| } |
| |
| return; |
| } while (0); |
| |
| // An error occurred: |
| delete[] vps; vps = NULL; vpsSize = 0; |
| delete[] sps; sps = NULL; spsSize = 0; |
| delete[] pps; pps = NULL; ppsSize = 0; |
| } |
| |
| void MatroskaFile |
| ::getVorbisOrTheoraConfigData(MatroskaTrack const* track, |
| u_int8_t*& identificationHeader, unsigned& identificationHeaderSize, |
| u_int8_t*& commentHeader, unsigned& commentHeaderSize, |
| u_int8_t*& setupHeader, unsigned& setupHeaderSize) { |
| identificationHeader = commentHeader = setupHeader = NULL; |
| identificationHeaderSize = commentHeaderSize = setupHeaderSize = 0; |
| |
| do { |
| if (track == NULL) break; |
| |
| // The Matroska file's 'Codec Private' data is assumed to be the codec configuration |
| // information, containing the "Identification", "Comment", and "Setup" headers. |
| // Extract these headers now: |
| Boolean isTheora = strcmp(track->mimeType, "video/THEORA") == 0; // otherwise, Vorbis |
| u_int8_t* p = track->codecPrivate; |
| unsigned n = track->codecPrivateSize; |
| if (n == 0 || p == NULL) break; // we have no 'Codec Private' data |
| |
| u_int8_t numHeaders; |
| getPrivByte(numHeaders); |
| unsigned headerSize[3]; // we don't handle any more than 2+1 headers |
| |
| // Extract the sizes of each of these headers: |
| unsigned sizesSum = 0; |
| Boolean success = True; |
| unsigned i; |
| for (i = 0; i < numHeaders && i < 3; ++i) { |
| unsigned len = 0; |
| u_int8_t c; |
| |
| do { |
| success = False; |
| getPrivByte(c); |
| success = True; |
| |
| len += c; |
| } while (c == 255); |
| if (!success || len == 0) break; |
| |
| headerSize[i] = len; |
| sizesSum += len; |
| } |
| if (!success) break; |
| |
| // Compute the implicit size of the final header: |
| if (numHeaders < 3) { |
| int finalHeaderSize = n - sizesSum; |
| if (finalHeaderSize <= 0) break; // error in data; give up |
| |
| headerSize[numHeaders] = (unsigned)finalHeaderSize; |
| ++numHeaders; // include the final header now |
| } else { |
| numHeaders = 3; // The maximum number of headers that we handle |
| } |
| |
| // Then, extract and classify each header: |
| for (i = 0; i < numHeaders; ++i) { |
| success = False; |
| unsigned newHeaderSize = headerSize[i]; |
| u_int8_t* newHeader = new u_int8_t[newHeaderSize]; |
| if (newHeader == NULL) break; |
| |
| u_int8_t* hdr = newHeader; |
| while (newHeaderSize-- > 0) { |
| success = False; |
| getPrivByte(*hdr++); |
| success = True; |
| } |
| if (!success) { |
| delete[] newHeader; |
| break; |
| } |
| |
| u_int8_t headerType = newHeader[0]; |
| if (headerType == 1 || (isTheora && headerType == 0x80)) { // "identification" header |
| delete[] identificationHeader; identificationHeader = newHeader; |
| identificationHeaderSize = headerSize[i]; |
| } else if (headerType == 3 || (isTheora && headerType == 0x81)) { // "comment" header |
| delete[] commentHeader; commentHeader = newHeader; |
| commentHeaderSize = headerSize[i]; |
| } else if (headerType == 5 || (isTheora && headerType == 0x82)) { // "setup" header |
| delete[] setupHeader; setupHeader = newHeader; |
| setupHeaderSize = headerSize[i]; |
| } else { |
| delete[] newHeader; // because it was a header type that we don't understand |
| } |
| } |
| if (!success) break; |
| |
| return; |
| } while (0); |
| |
| // An error occurred: |
| delete[] identificationHeader; identificationHeader = NULL; identificationHeaderSize = 0; |
| delete[] commentHeader; commentHeader = NULL; commentHeaderSize = 0; |
| delete[] setupHeader; setupHeader = NULL; setupHeaderSize = 0; |
| } |
| |
| float MatroskaFile::fileDuration() { |
| if (fCuePoints == NULL) return 0.0; // Hack, because the RTSP server code assumes that duration > 0 => seekable. (fix this) ##### |
| |
| return segmentDuration()*(timecodeScale()/1000000000.0f); |
| } |
| |
| // The size of the largest key frame that we expect. This determines our buffer sizes: |
| #define MAX_KEY_FRAME_SIZE 300000 |
| |
| FramedSource* MatroskaFile |
| ::createSourceForStreaming(FramedSource* baseSource, unsigned trackNumber, |
| unsigned& estBitrate, unsigned& numFiltersInFrontOfTrack) { |
| if (baseSource == NULL) return NULL; |
| |
| FramedSource* result = baseSource; // by default |
| estBitrate = 100; // by default |
| numFiltersInFrontOfTrack = 0; // by default |
| |
| // Look at the track's MIME type to set its estimated bitrate (for use by RTCP). |
| // (Later, try to be smarter about figuring out the bitrate.) ##### |
| // Some MIME types also require adding a special 'framer' in front of the source. |
| MatroskaTrack* track = lookup(trackNumber); |
| if (track != NULL) { // should always be true |
| if (strcmp(track->mimeType, "audio/MPEG") == 0) { |
| estBitrate = 128; |
| } else if (strcmp(track->mimeType, "audio/AAC") == 0) { |
| estBitrate = 96; |
| } else if (strcmp(track->mimeType, "audio/AC3") == 0) { |
| estBitrate = 48; |
| } else if (strcmp(track->mimeType, "audio/VORBIS") == 0) { |
| estBitrate = 96; |
| } else if (strcmp(track->mimeType, "video/H264") == 0) { |
| estBitrate = 500; |
| // Allow for the possibility of very large NAL units being fed to the sink object: |
| OutPacketBuffer::increaseMaxSizeTo(MAX_KEY_FRAME_SIZE); // bytes |
| |
| // Add a framer in front of the source: |
| result = H264VideoStreamDiscreteFramer::createNew(envir(), result); |
| ++numFiltersInFrontOfTrack; |
| } else if (strcmp(track->mimeType, "video/H265") == 0) { |
| estBitrate = 500; |
| // Allow for the possibility of very large NAL units being fed to the sink object: |
| OutPacketBuffer::increaseMaxSizeTo(MAX_KEY_FRAME_SIZE); // bytes |
| |
| // Add a framer in front of the source: |
| result = H265VideoStreamDiscreteFramer::createNew(envir(), result); |
| ++numFiltersInFrontOfTrack; |
| } else if (strcmp(track->mimeType, "video/VP8") == 0) { |
| estBitrate = 500; |
| } else if (strcmp(track->mimeType, "video/VP9") == 0) { |
| estBitrate = 500; |
| } else if (strcmp(track->mimeType, "video/THEORA") == 0) { |
| estBitrate = 500; |
| } else if (strcmp(track->mimeType, "text/T140") == 0) { |
| estBitrate = 48; |
| } |
| } |
| |
| return result; |
| } |
| |
| char const* MatroskaFile::trackMIMEType(unsigned trackNumber) const { |
| MatroskaTrack* track = lookup(trackNumber); |
| if (track == NULL) return NULL; |
| |
| return track->mimeType; |
| } |
| |
| RTPSink* MatroskaFile |
| ::createRTPSinkForTrackNumber(unsigned trackNumber, Groupsock* rtpGroupsock, |
| unsigned char rtpPayloadTypeIfDynamic) { |
| RTPSink* result = NULL; // default value, if an error occurs |
| |
| do { |
| MatroskaTrack* track = lookup(trackNumber); |
| if (track == NULL) break; |
| |
| if (strcmp(track->mimeType, "audio/L16") == 0) { |
| result = SimpleRTPSink::createNew(envir(), rtpGroupsock,rtpPayloadTypeIfDynamic, track->samplingFrequency, "audio", "L16", track->numChannels); |
| } else if (strcmp(track->mimeType, "audio/MPEG") == 0) { |
| result = MPEG1or2AudioRTPSink::createNew(envir(), rtpGroupsock); |
| } else if (strcmp(track->mimeType, "audio/AAC") == 0) { |
| // The Matroska file's 'Codec Private' data is assumed to be the AAC configuration |
| // information. Use this to generate a hexadecimal 'config' string for the new RTP sink: |
| char* configStr = new char[2*track->codecPrivateSize + 1]; if (configStr == NULL) break; |
| // 2 hex digits per byte, plus the trailing '\0' |
| for (unsigned i = 0; i < track->codecPrivateSize; ++i) { |
| sprintf(&configStr[2*i], "%02X", track->codecPrivate[i]); |
| } |
| |
| result = MPEG4GenericRTPSink::createNew(envir(), rtpGroupsock, |
| rtpPayloadTypeIfDynamic, |
| track->samplingFrequency, |
| "audio", "AAC-hbr", configStr, |
| track->numChannels); |
| delete[] configStr; |
| } else if (strcmp(track->mimeType, "audio/AC3") == 0) { |
| result = AC3AudioRTPSink |
| ::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, track->samplingFrequency); |
| } else if (strcmp(track->mimeType, "audio/OPUS") == 0) { |
| result = SimpleRTPSink |
| ::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| 48000, "audio", "OPUS", 2, False/*only 1 Opus 'packet' in each RTP packet*/); |
| } else if (strcmp(track->mimeType, "audio/VORBIS") == 0 || strcmp(track->mimeType, "video/THEORA") == 0) { |
| u_int8_t* identificationHeader; unsigned identificationHeaderSize; |
| u_int8_t* commentHeader; unsigned commentHeaderSize; |
| u_int8_t* setupHeader; unsigned setupHeaderSize; |
| getVorbisOrTheoraConfigData(track, |
| identificationHeader, identificationHeaderSize, |
| commentHeader, commentHeaderSize, |
| setupHeader, setupHeaderSize); |
| |
| if (strcmp(track->mimeType, "video/THEORA") == 0) { |
| result = TheoraVideoRTPSink |
| ::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| identificationHeader, identificationHeaderSize, |
| commentHeader, commentHeaderSize, |
| setupHeader, setupHeaderSize); |
| } else { // Vorbis |
| result = VorbisAudioRTPSink |
| ::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| track->samplingFrequency, track->numChannels, |
| identificationHeader, identificationHeaderSize, |
| commentHeader, commentHeaderSize, |
| setupHeader, setupHeaderSize); |
| } |
| delete[] identificationHeader; delete[] commentHeader; delete[] setupHeader; |
| } else if (strcmp(track->mimeType, "video/RAW") == 0) { |
| result = RawVideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| track->pixelHeight, track->pixelWidth, track->bitDepth, track->colorSampling, track->colorimetry); |
| } else if (strcmp(track->mimeType, "video/H264") == 0) { |
| u_int8_t* sps; unsigned spsSize; |
| u_int8_t* pps; unsigned ppsSize; |
| |
| getH264ConfigData(track, sps, spsSize, pps, ppsSize); |
| result = H264VideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| sps, spsSize, pps, ppsSize); |
| delete[] sps; delete[] pps; |
| } else if (strcmp(track->mimeType, "video/H265") == 0) { |
| u_int8_t* vps; unsigned vpsSize; |
| u_int8_t* sps; unsigned spsSize; |
| u_int8_t* pps; unsigned ppsSize; |
| |
| getH265ConfigData(track, vps, vpsSize, sps, spsSize, pps, ppsSize); |
| result = H265VideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic, |
| vps, vpsSize, sps, spsSize, pps, ppsSize); |
| delete[] vps; delete[] sps; delete[] pps; |
| } else if (strcmp(track->mimeType, "video/VP8") == 0) { |
| result = VP8VideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic); |
| } else if (strcmp(track->mimeType, "video/VP9") == 0) { |
| result = VP9VideoRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic); |
| } else if (strcmp(track->mimeType, "text/T140") == 0) { |
| result = T140TextRTPSink::createNew(envir(), rtpGroupsock, rtpPayloadTypeIfDynamic); |
| } |
| } while (0); |
| |
| return result; |
| } |
| |
| FileSink* MatroskaFile::createFileSinkForTrackNumber(unsigned trackNumber, char const* fileName) { |
| FileSink* result = NULL; // default value, if an error occurs |
| Boolean createOggFileSink = False; // by default |
| |
| do { |
| MatroskaTrack* track = lookup(trackNumber); |
| if (track == NULL) break; |
| |
| if (strcmp(track->mimeType, "video/H264") == 0) { |
| u_int8_t* sps; unsigned spsSize; |
| u_int8_t* pps; unsigned ppsSize; |
| |
| getH264ConfigData(track, sps, spsSize, pps, ppsSize); |
| |
| char* sps_base64 = base64Encode((char*)sps, spsSize); |
| char* pps_base64 = base64Encode((char*)pps, ppsSize); |
| delete[] sps; delete[] pps; |
| |
| char* sPropParameterSetsStr |
| = new char[(sps_base64 == NULL ? 0 : strlen(sps_base64)) + |
| (pps_base64 == NULL ? 0 : strlen(pps_base64)) + |
| 10 /*more than enough space*/]; |
| sprintf(sPropParameterSetsStr, "%s,%s", sps_base64, pps_base64); |
| delete[] sps_base64; delete[] pps_base64; |
| |
| result = H264VideoFileSink::createNew(envir(), fileName, |
| sPropParameterSetsStr, |
| MAX_KEY_FRAME_SIZE); // extra large buffer size for large key frames |
| delete[] sPropParameterSetsStr; |
| } else if (strcmp(track->mimeType, "video/H265") == 0) { |
| u_int8_t* vps; unsigned vpsSize; |
| u_int8_t* sps; unsigned spsSize; |
| u_int8_t* pps; unsigned ppsSize; |
| |
| getH265ConfigData(track, vps, vpsSize, sps, spsSize, pps, ppsSize); |
| |
| char* vps_base64 = base64Encode((char*)vps, vpsSize); |
| char* sps_base64 = base64Encode((char*)sps, spsSize); |
| char* pps_base64 = base64Encode((char*)pps, ppsSize); |
| delete[] vps; delete[] sps; delete[] pps; |
| |
| result = H265VideoFileSink::createNew(envir(), fileName, |
| vps_base64, sps_base64, pps_base64, |
| MAX_KEY_FRAME_SIZE); // extra large buffer size for large key frames |
| delete[] vps_base64; delete[] sps_base64; delete[] pps_base64; |
| } else if (strcmp(track->mimeType, "video/THEORA") == 0) { |
| createOggFileSink = True; |
| } else if (strcmp(track->mimeType, "audio/AMR") == 0 || |
| strcmp(track->mimeType, "audio/AMR-WB") == 0) { |
| // For AMR audio streams, we use a special sink that inserts AMR frame hdrs: |
| result = AMRAudioFileSink::createNew(envir(), fileName); |
| } else if (strcmp(track->mimeType, "audio/VORBIS") == 0 || |
| strcmp(track->mimeType, "audio/OPUS") == 0) { |
| createOggFileSink = True; |
| } |
| |
| if (createOggFileSink) { |
| char* configStr = NULL; // by default |
| |
| if (strcmp(track->mimeType, "audio/VORBIS") == 0 || strcmp(track->mimeType, "video/THEORA") == 0) { |
| u_int8_t* identificationHeader; unsigned identificationHeaderSize; |
| u_int8_t* commentHeader; unsigned commentHeaderSize; |
| u_int8_t* setupHeader; unsigned setupHeaderSize; |
| getVorbisOrTheoraConfigData(track, |
| identificationHeader, identificationHeaderSize, |
| commentHeader, commentHeaderSize, |
| setupHeader, setupHeaderSize); |
| u_int32_t identField = 0xFACADE; // Can we get a real value from the file somehow? |
| configStr = generateVorbisOrTheoraConfigStr(identificationHeader, identificationHeaderSize, |
| commentHeader, commentHeaderSize, |
| setupHeader, setupHeaderSize, |
| identField); |
| delete[] identificationHeader; delete[] commentHeader; delete[] setupHeader; |
| } |
| |
| result = OggFileSink::createNew(envir(), fileName, track->samplingFrequency, configStr, MAX_KEY_FRAME_SIZE); |
| delete[] configStr; |
| } else if (result == NULL) { |
| // By default, just create a regular "FileSink": |
| result = FileSink::createNew(envir(), fileName, MAX_KEY_FRAME_SIZE); |
| } |
| } while (0); |
| |
| return result; |
| } |
| |
| void MatroskaFile::addTrack(MatroskaTrack* newTrack, unsigned trackNumber) { |
| fTrackTable->add(newTrack, trackNumber); |
| } |
| |
| void MatroskaFile::addCuePoint(double cueTime, u_int64_t clusterOffsetInFile, unsigned blockNumWithinCluster) { |
| Boolean dummy = False; // not used |
| CuePoint::addCuePoint(fCuePoints, cueTime, clusterOffsetInFile, blockNumWithinCluster, dummy); |
| } |
| |
| Boolean MatroskaFile::lookupCuePoint(double& cueTime, u_int64_t& resultClusterOffsetInFile, unsigned& resultBlockNumWithinCluster) { |
| if (fCuePoints == NULL) return False; |
| |
| (void)fCuePoints->lookup(cueTime, resultClusterOffsetInFile, resultBlockNumWithinCluster); |
| return True; |
| } |
| |
| void MatroskaFile::printCuePoints(FILE* fid) { |
| CuePoint::fprintf(fid, fCuePoints); |
| } |
| |
| |
| ////////// MatroskaTrackTable implementation ////////// |
| |
| MatroskaTrackTable::MatroskaTrackTable() |
| : fTable(HashTable::create(ONE_WORD_HASH_KEYS)) { |
| } |
| |
| MatroskaTrackTable::~MatroskaTrackTable() { |
| // Remove and delete all of our "MatroskaTrack" descriptors, and the hash table itself: |
| MatroskaTrack* track; |
| while ((track = (MatroskaTrack*)fTable->RemoveNext()) != NULL) { |
| delete track; |
| } |
| delete fTable; |
| } |
| |
| void MatroskaTrackTable::add(MatroskaTrack* newTrack, unsigned trackNumber) { |
| if (newTrack != NULL && newTrack->trackNumber != 0) fTable->Remove((char const*)newTrack->trackNumber); |
| MatroskaTrack* existingTrack = (MatroskaTrack*)fTable->Add((char const*)trackNumber, newTrack); |
| delete existingTrack; // in case it wasn't NULL |
| } |
| |
| MatroskaTrack* MatroskaTrackTable::lookup(unsigned trackNumber) { |
| return (MatroskaTrack*)fTable->Lookup((char const*)trackNumber); |
| } |
| |
| unsigned MatroskaTrackTable::numTracks() const { return fTable->numEntries(); } |
| |
| MatroskaTrackTable::Iterator::Iterator(MatroskaTrackTable& ourTable) { |
| fIter = HashTable::Iterator::create(*(ourTable.fTable)); |
| } |
| |
| MatroskaTrackTable::Iterator::~Iterator() { |
| delete fIter; |
| } |
| |
| MatroskaTrack* MatroskaTrackTable::Iterator::next() { |
| char const* key; |
| return (MatroskaTrack*)fIter->next(key); |
| } |
| |
| |
| ////////// MatroskaTrack implementation ////////// |
| |
| MatroskaTrack::MatroskaTrack() |
| : trackNumber(0/*not set*/), trackType(0/*unknown*/), |
| isEnabled(True), isDefault(True), isForced(False), |
| defaultDuration(0), |
| name(NULL), language(NULL), codecID(NULL), |
| samplingFrequency(0), numChannels(2), mimeType(""), |
| codecPrivateSize(0), codecPrivate(NULL), |
| codecPrivateUsesH264FormatForH265(False), codecIsOpus(False), |
| headerStrippedBytesSize(0), headerStrippedBytes(NULL), |
| colorSampling(""), colorimetry("BT709-2") /*Matroska default value for Primaries */, |
| pixelWidth(0), pixelHeight(0), bitDepth(8), subframeSizeSize(0) { |
| } |
| |
| MatroskaTrack::~MatroskaTrack() { |
| delete[] name; delete[] language; delete[] codecID; |
| delete[] codecPrivate; |
| delete[] headerStrippedBytes; |
| } |
| |
| |
| ////////// MatroskaDemux implementation ////////// |
| |
| MatroskaDemux::MatroskaDemux(MatroskaFile& ourFile) |
| : Medium(ourFile.envir()), |
| fOurFile(ourFile), fDemuxedTracksTable(HashTable::create(ONE_WORD_HASH_KEYS)), |
| fNextTrackTypeToCheck(0x1) { |
| fOurParser = new MatroskaFileParser(ourFile, ByteStreamFileSource::createNew(envir(), ourFile.fileName()), |
| handleEndOfFile, this, this); |
| } |
| |
| MatroskaDemux::~MatroskaDemux() { |
| // Begin by acting as if we've reached the end of the source file. This should cause all of our demuxed tracks to get closed. |
| handleEndOfFile(); |
| |
| // Then delete our table of "MatroskaDemuxedTrack"s |
| // - but not the "MatroskaDemuxedTrack"s themselves; that should have already happened: |
| delete fDemuxedTracksTable; |
| |
| delete fOurParser; |
| fOurFile.removeDemux(this); |
| } |
| |
| FramedSource* MatroskaDemux::newDemuxedTrack() { |
| unsigned dummyResultTrackNumber; |
| return newDemuxedTrack(dummyResultTrackNumber); |
| } |
| |
| FramedSource* MatroskaDemux::newDemuxedTrack(unsigned& resultTrackNumber) { |
| FramedSource* result; |
| resultTrackNumber = 0; |
| |
| for (result = NULL; result == NULL && fNextTrackTypeToCheck != MATROSKA_TRACK_TYPE_OTHER; |
| fNextTrackTypeToCheck <<= 1) { |
| if (fNextTrackTypeToCheck == MATROSKA_TRACK_TYPE_VIDEO) resultTrackNumber = fOurFile.chosenVideoTrackNumber(); |
| else if (fNextTrackTypeToCheck == MATROSKA_TRACK_TYPE_AUDIO) resultTrackNumber = fOurFile.chosenAudioTrackNumber(); |
| else if (fNextTrackTypeToCheck == MATROSKA_TRACK_TYPE_SUBTITLE) resultTrackNumber = fOurFile.chosenSubtitleTrackNumber(); |
| |
| result = newDemuxedTrackByTrackNumber(resultTrackNumber); |
| } |
| |
| return result; |
| } |
| |
| FramedSource* MatroskaDemux::newDemuxedTrackByTrackNumber(unsigned trackNumber) { |
| if (trackNumber == 0) return NULL; |
| |
| FramedSource* trackSource = new MatroskaDemuxedTrack(envir(), trackNumber, *this); |
| fDemuxedTracksTable->Add((char const*)trackNumber, trackSource); |
| return trackSource; |
| } |
| |
| MatroskaDemuxedTrack* MatroskaDemux::lookupDemuxedTrack(unsigned trackNumber) { |
| return (MatroskaDemuxedTrack*)fDemuxedTracksTable->Lookup((char const*)trackNumber); |
| } |
| |
| void MatroskaDemux::removeTrack(unsigned trackNumber) { |
| fDemuxedTracksTable->Remove((char const*)trackNumber); |
| if (fDemuxedTracksTable->numEntries() == 0) { |
| // We no longer have any demuxed tracks, so delete ourselves now: |
| Medium::close(this); |
| } |
| } |
| |
| void MatroskaDemux::continueReading() { |
| fOurParser->continueParsing(); |
| } |
| |
| void MatroskaDemux::seekToTime(double& seekNPT) { |
| if (fOurParser != NULL) fOurParser->seekToTime(seekNPT); |
| } |
| |
| void MatroskaDemux::handleEndOfFile(void* clientData) { |
| ((MatroskaDemux*)clientData)->handleEndOfFile(); |
| } |
| |
| void MatroskaDemux::handleEndOfFile() { |
| // Iterate through all of our 'demuxed tracks', handling 'end of input' on each one. |
| // Hack: Because this can cause the hash table to get modified underneath us, we don't call the handlers until after we've |
| // first iterated through all of the tracks. |
| unsigned numTracks = fDemuxedTracksTable->numEntries(); |
| if (numTracks == 0) return; |
| MatroskaDemuxedTrack** tracks = new MatroskaDemuxedTrack*[numTracks]; |
| |
| HashTable::Iterator* iter = HashTable::Iterator::create(*fDemuxedTracksTable); |
| unsigned i; |
| char const* trackNumber; |
| |
| for (i = 0; i < numTracks; ++i) { |
| tracks[i] = (MatroskaDemuxedTrack*)iter->next(trackNumber); |
| } |
| delete iter; |
| |
| for (i = 0; i < numTracks; ++i) { |
| if (tracks[i] == NULL) continue; // sanity check; shouldn't happen |
| tracks[i]->handleClosure(); |
| } |
| |
| delete[] tracks; |
| } |
| |
| |
| ////////// CuePoint implementation ////////// |
| |
| CuePoint::CuePoint(double cueTime, u_int64_t clusterOffsetInFile, unsigned blockNumWithinCluster) |
| : fBalance(0), |
| fCueTime(cueTime), fClusterOffsetInFile(clusterOffsetInFile), fBlockNumWithinCluster(blockNumWithinCluster - 1) { |
| fSubTree[0] = fSubTree[1] = NULL; |
| } |
| |
| CuePoint::~CuePoint() { |
| delete fSubTree[0]; delete fSubTree[1]; |
| } |
| |
| void CuePoint::addCuePoint(CuePoint*& root, double cueTime, u_int64_t clusterOffsetInFile, unsigned blockNumWithinCluster, |
| Boolean& needToReviseBalanceOfParent) { |
| needToReviseBalanceOfParent = False; // by default; may get changed below |
| |
| if (root == NULL) { |
| root = new CuePoint(cueTime, clusterOffsetInFile, blockNumWithinCluster); |
| needToReviseBalanceOfParent = True; |
| } else if (cueTime == root->fCueTime) { |
| // Replace existing data: |
| root->fClusterOffsetInFile = clusterOffsetInFile; |
| root->fBlockNumWithinCluster = blockNumWithinCluster - 1; |
| } else { |
| // Add to our left or right subtree: |
| int direction = cueTime > root->fCueTime; // 0 (left) or 1 (right) |
| Boolean needToReviseOurBalance = False; |
| addCuePoint(root->fSubTree[direction], cueTime, clusterOffsetInFile, blockNumWithinCluster, needToReviseOurBalance); |
| |
| if (needToReviseOurBalance) { |
| // We need to change our 'balance' number, perhaps while also performing a rotation to bring ourself back into balance: |
| if (root->fBalance == 0) { |
| // We were balanced before, but now we're unbalanced (by 1) on the "direction" side: |
| root->fBalance = -1 + 2*direction; // -1 for "direction" 0; 1 for "direction" 1 |
| needToReviseBalanceOfParent = True; |
| } else if (root->fBalance == 1 - 2*direction) { // 1 for "direction" 0; -1 for "direction" 1 |
| // We were unbalanced (by 1) on the side opposite to where we added an entry, so now we're balanced: |
| root->fBalance = 0; |
| } else { |
| // We were unbalanced (by 1) on the side where we added an entry, so now we're unbalanced by 2, and have to rebalance: |
| if (root->fSubTree[direction]->fBalance == -1 + 2*direction) { // -1 for "direction" 0; 1 for "direction" 1 |
| // We're 'doubly-unbalanced' on this side, so perform a single rotation in the opposite direction: |
| root->fBalance = root->fSubTree[direction]->fBalance = 0; |
| rotate(1-direction, root); |
| } else { |
| // This is the Left-Right case (for "direction" 0) or the Right-Left case (for "direction" 1); perform two rotations: |
| char newParentCurBalance = root->fSubTree[direction]->fSubTree[1-direction]->fBalance; |
| if (newParentCurBalance == 1 - 2*direction) { // 1 for "direction" 0; -1 for "direction" 1 |
| root->fBalance = 0; |
| root->fSubTree[direction]->fBalance = -1 + 2*direction; // -1 for "direction" 0; 1 for "direction" 1 |
| } else if (newParentCurBalance == 0) { |
| root->fBalance = 0; |
| root->fSubTree[direction]->fBalance = 0; |
| } else { |
| root->fBalance = 1 - 2*direction; // 1 for "direction" 0; -1 for "direction" 1 |
| root->fSubTree[direction]->fBalance = 0; |
| } |
| rotate(direction, root->fSubTree[direction]); |
| |
| root->fSubTree[direction]->fBalance = 0; // the new root will be balanced |
| rotate(1-direction, root); |
| } |
| } |
| } |
| } |
| } |
| |
| Boolean CuePoint::lookup(double& cueTime, u_int64_t& resultClusterOffsetInFile, unsigned& resultBlockNumWithinCluster) { |
| if (cueTime < fCueTime) { |
| if (left() == NULL) { |
| resultClusterOffsetInFile = 0; |
| resultBlockNumWithinCluster = 0; |
| return False; |
| } else { |
| return left()->lookup(cueTime, resultClusterOffsetInFile, resultBlockNumWithinCluster); |
| } |
| } else { |
| if (right() == NULL || !right()->lookup(cueTime, resultClusterOffsetInFile, resultBlockNumWithinCluster)) { |
| // Use this record: |
| cueTime = fCueTime; |
| resultClusterOffsetInFile = fClusterOffsetInFile; |
| resultBlockNumWithinCluster = fBlockNumWithinCluster; |
| } |
| return True; |
| } |
| } |
| |
| void CuePoint::fprintf(FILE* fid, CuePoint* cuePoint) { |
| if (cuePoint != NULL) { |
| ::fprintf(fid, "["); |
| fprintf(fid, cuePoint->left()); |
| |
| ::fprintf(fid, ",%.1f{%d},", cuePoint->fCueTime, cuePoint->fBalance); |
| |
| fprintf(fid, cuePoint->right()); |
| ::fprintf(fid, "]"); |
| } |
| } |
| |
| void CuePoint::rotate(unsigned direction/*0 => left; 1 => right*/, CuePoint*& root) { |
| CuePoint* pivot = root->fSubTree[1-direction]; // ASSERT: pivot != NULL |
| root->fSubTree[1-direction] = pivot->fSubTree[direction]; |
| pivot->fSubTree[direction] = root; |
| root = pivot; |
| } |