/* * MatroskaImportPrivate.cpp * * MatroskaImportPrivate.cpp - C++ code for interfacing with libmatroska to import. * * * Copyright (c) 2006 David Conrad * * This program 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; * version 2.1 of the License. * * This program 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 program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "MatroskaImport.h" #include "MatroskaCodecIDs.h" #include "SubImport.h" #include "SubRenderer.h" #include "CommonUtils.h" #include "Codecprintf.h" #include "bitstream_info.h" #include "CompressCodecUtils.h" extern "C" { #include #include "ff_private.h" #undef CodecType } using namespace std; using namespace libmatroska; bool MatroskaImport::OpenFile() { bool valid = true; int upperLevel = 0; ioHandler = new DataHandlerCallback(dataRef, dataRefType, MODE_READ); aStream = new EbmlStream(*ioHandler); el_l0 = aStream->FindNextID(EbmlHead::ClassInfos, ~0); if (el_l0 != NULL) { EbmlElement *dummyElt = NULL; el_l0->Read(*aStream, EbmlHead::ClassInfos.Context, upperLevel, dummyElt, true); if (EbmlId(*el_l0) != EBML_ID(EbmlHead)) { Codecprintf(NULL, "Not a Matroska file\n"); valid = false; goto exit; } EbmlHead *head = static_cast(el_l0); EDocType docType = GetChild(*head); if (string(docType) != "matroska" && string(docType) != "webm") { Codecprintf(NULL, "Unknown Matroska doctype\n"); valid = false; goto exit; } EDocTypeReadVersion readVersion = GetChild(*head); if (UInt64(readVersion) > 2) { Codecprintf(NULL, "Matroska file too new to be read\n"); valid = false; } } else { Codecprintf(NULL, "Matroska file missing EBML Head\n"); valid = false; } exit: delete el_l0; el_l0 = NULL; return valid; } ComponentResult MatroskaImport::ProcessLevel1Element() { int upperLevel = 0; EbmlElement *dummyElt = NULL; if (EbmlId(*el_l1) == KaxInfo::ClassInfos.GlobalId) { el_l1->Read(*aStream, KaxInfo::ClassInfos.Context, upperLevel, dummyElt, true); return ReadSegmentInfo(*static_cast(el_l1)); } else if (EbmlId(*el_l1) == KaxTracks::ClassInfos.GlobalId) { el_l1->Read(*aStream, KaxTracks::ClassInfos.Context, upperLevel, dummyElt, true); return ReadTracks(*static_cast(el_l1)); } else if (EbmlId(*el_l1) == KaxChapters::ClassInfos.GlobalId) { el_l1->Read(*aStream, KaxChapters::ClassInfos.Context, upperLevel, dummyElt, true); return ReadChapters(*static_cast(el_l1)); } else if (EbmlId(*el_l1) == KaxAttachments::ClassInfos.GlobalId) { ComponentResult res; el_l1->Read(*aStream, KaxAttachments::ClassInfos.Context, upperLevel, dummyElt, true); res = ReadAttachments(*static_cast(el_l1)); PrerollSubtitleTracks(); return res; } else if (EbmlId(*el_l1) == KaxSeekHead::ClassInfos.GlobalId) { el_l1->Read(*aStream, KaxSeekHead::ClassInfos.Context, upperLevel, dummyElt, true); return ReadMetaSeek(*static_cast(el_l1)); } return noErr; } ComponentResult MatroskaImport::SetupMovie() { ComponentResult err = noErr; // once we've read the Tracks and Segment Info elements and Chapters if it's in the seek head, // we don't need to read any more of the file bool done = false; el_l0 = aStream->FindNextID(KaxSegment::ClassInfos, ~0); if (!el_l0) return err; // nothing in the file segmentOffset = static_cast(el_l0)->GetDataStart(); SetAutoTrackAlternatesEnabled(theMovie, false); while (!done && NextLevel1Element()) { if (EbmlId(*el_l1) == KaxCluster::ClassInfos.GlobalId) { // all header elements are before clusters in sane files done = true; } else err = ProcessLevel1Element(); if (err) return err; } // some final setup of info across elements since they can come in any order for (int i = 0; i < tracks.size(); i++) { tracks[i].timecodeScale = timecodeScale; // chapter tracks have to be associated with other enabled tracks to display if (chapterTrack) { AddTrackReference(tracks[i].theTrack, chapterTrack, kTrackReferenceChapterList, NULL); } } return err; } EbmlElement * MatroskaImport::NextLevel1Element() { int upperLevel = 0; if (el_l1) { el_l1->SkipData(*aStream, el_l1->Generic().Context); delete el_l1; } el_l1 = aStream->FindNextElement(el_l0->Generic().Context, upperLevel, 0xFFFFFFFFL, true); // dummy element -> probably corrupt file, search for next element in meta seek and continue from there if (el_l1 && el_l1->IsDummy()) { vector::iterator nextElt; MatroskaSeek currElt; currElt.segmentPos = el_l1->GetElementPosition(); currElt.idLength = currElt.ebmlID = 0; nextElt = find_if(levelOneElements.begin(), levelOneElements.end(), bind2nd(greater(), currElt)); if (nextElt != levelOneElements.end()) { SetContext(nextElt->GetSeekContext(segmentOffset)); NextLevel1Element(); } } return el_l1; } ComponentResult MatroskaImport::ReadSegmentInfo(KaxInfo &segmentInfo) { if (seenInfo) return noErr; KaxDuration & duration = GetChild(segmentInfo); KaxTimecodeScale & timecodeScale = GetChild(segmentInfo); KaxTitle & title = GetChild(segmentInfo); KaxWritingApp & writingApp = GetChild(segmentInfo); movieDuration = Float64(duration); this->timecodeScale = UInt64(timecodeScale); SetMovieTimeScale(theMovie, S64Div(1000000000L, this->timecodeScale)); QTMetaDataRef movieMetaData; OSStatus err = QTCopyMovieMetaData(theMovie, &movieMetaData); if (err == noErr) { OSType key; if (!title.IsDefaultValue()) { key = kQTMetaDataCommonKeyDisplayName; QTMetaDataAddItem(movieMetaData, kQTMetaDataStorageFormatQuickTime, kQTMetaDataKeyFormatCommon, (UInt8 *)&key, sizeof(key), (UInt8 *)UTFstring(title).GetUTF8().c_str(), UTFstring(title).GetUTF8().size(), kQTMetaDataTypeUTF8, NULL); } if (!writingApp.IsDefaultValue()) { key = kQTMetaDataCommonKeySoftware; QTMetaDataAddItem(movieMetaData, kQTMetaDataStorageFormatQuickTime, kQTMetaDataKeyFormatCommon, (UInt8 *)&key, sizeof(key), (UInt8 *)UTFstring(writingApp).GetUTF8().c_str(), UTFstring(writingApp).GetUTF8().size(), kQTMetaDataTypeUTF8, NULL); } QTMetaDataRelease(movieMetaData); } seenInfo = true; return noErr; } ComponentResult MatroskaImport::ReadTracks(KaxTracks &trackEntries) { Track firstVideoTrack = NULL; short firstVideoTrackLang = 0; bool videoEnabled = false; Track firstAudioTrack = NULL; short firstAudioTrackLang = 0; bool audioEnabled = false; Track firstSubtitleTrack = NULL; short firstSubtitleTrackLang = 0; bool subtitleEnabled = false; ComponentResult err = noErr; if (seenTracks) return noErr; // Since creating a subtitle track requires a video track to have already been created // (so that it can be sized to fit exactly over the video track), we go through the // track entries in two passes, first to add audio/video, second to add subtitle tracks. for (int pass = 1; pass <= 2; pass++) { for (int i = 0; i < trackEntries.ListSize(); i++) { if (EbmlId(*trackEntries[i]) != KaxTrackEntry::ClassInfos.GlobalId) continue; KaxTrackEntry & track = *static_cast(trackEntries[i]); KaxTrackNumber & number = GetChild(track); KaxTrackType & type = GetChild(track); KaxTrackDefaultDuration * defaultDuration = FindChild(track); KaxTrackFlagDefault & enabled = GetChild(track); KaxTrackFlagLacing & lacing = GetChild(track); MatroskaTrack mkvTrack; mkvTrack.number = uint16(number); mkvTrack.type = uint8(type); if (defaultDuration) mkvTrack.defaultDuration = uint32(*defaultDuration) / float(timecodeScale) + .5; else mkvTrack.defaultDuration = 0; mkvTrack.isEnabled = uint8(enabled); mkvTrack.usesLacing = uint8(lacing); KaxTrackLanguage & trackLang = GetChild(track); KaxTrackName & trackName = GetChild(track); KaxContentEncodings * encodings = FindChild(track); short qtLang = ISO639_2ToQTLangCode(string(trackLang).c_str()); switch (uint8(type)) { case track_video: if (pass == 2) continue; err = AddVideoTrack(track, mkvTrack, encodings); if (err) return err; if (mkvTrack.isEnabled) videoEnabled = true; if (firstVideoTrack && qtLang != firstVideoTrackLang) SetTrackAlternate(firstVideoTrack, mkvTrack.theTrack); else { firstVideoTrack = mkvTrack.theTrack; firstVideoTrackLang = qtLang; } break; case track_audio: if (pass == 2) continue; err = AddAudioTrack(track, mkvTrack, encodings); if (err) return err; if (mkvTrack.isEnabled) audioEnabled = true; if (firstAudioTrack && qtLang != firstAudioTrackLang) SetTrackAlternate(firstAudioTrack, mkvTrack.theTrack); else { firstAudioTrack = mkvTrack.theTrack; firstAudioTrackLang = qtLang; } break; case track_subtitle: if (pass == 1) continue; err = AddSubtitleTrack(track, mkvTrack, encodings); if (err) return err; if (mkvTrack.theTrack == NULL) continue; if (mkvTrack.isEnabled) subtitleEnabled = true; if (firstSubtitleTrack && qtLang != firstSubtitleTrackLang) SetTrackAlternate(firstSubtitleTrack, mkvTrack.theTrack); else { firstSubtitleTrack = mkvTrack.theTrack; firstSubtitleTrackLang = qtLang; } break; case track_complex: case track_logo: case track_buttons: case track_control: // not likely to be implemented soon default: continue; } SetMediaLanguage(mkvTrack.theMedia, qtLang); if (!trackName.IsDefaultValue()) { QTMetaDataRef trackMetaData; err = QTCopyTrackMetaData(mkvTrack.theTrack, &trackMetaData); if (err == noErr) { OSType key = 'name'; // QuickTime differentiates between the title of a track and its name // so we set both QTMetaDataAddItem(trackMetaData, kQTMetaDataStorageFormatQuickTime, kQTMetaDataKeyFormatCommon, (UInt8 *)&key, sizeof(key), (UInt8 *)UTFstring(trackName).GetUTF8().c_str(), UTFstring(trackName).GetUTF8().size(), kQTMetaDataTypeUTF8, NULL); QTMetaDataAddItem(trackMetaData, kQTMetaDataStorageFormatUserData, kQTMetaDataKeyFormatUserData, (UInt8 *)&key, sizeof(key), (UInt8 *)UTFstring(trackName).GetUTF8().c_str(), UTFstring(trackName).GetUTF8().size(), kQTMetaDataTypeUTF8, NULL); QTMetaDataRelease(trackMetaData); } } tracks.push_back(mkvTrack); } } for (int i = 0; i < tracks.size(); i++) { SetTrackEnabled(tracks[i].theTrack, tracks[i].isEnabled); } // ffmpeg used to write a TrackDefault of 0 for all tracks // ensure that at least one track of each media type is enabled, if none were originally // this picks the first track, which may not be the best, but the situation is quite rare anyway // FIXME: properly choose tracks based on forced/default/language flags, and consider turning auto-alternates back on if (!videoEnabled && firstVideoTrack) SetTrackEnabled(firstVideoTrack, 1); if (!audioEnabled && firstAudioTrack) SetTrackEnabled(firstAudioTrack, 1); if (!subtitleEnabled && firstSubtitleTrack) SetTrackEnabled(firstSubtitleTrack, 1); seenTracks = true; return noErr; } ComponentResult MatroskaImport::ReadVobSubContentEncodings(KaxContentEncodings *encodings, MatroskaTrack &mkvTrack) { KaxContentEncoding & encoding = GetChild(*encodings); int scope = uint32(GetChild(encoding)); int type = uint32(GetChild(encoding)); if (scope != 1) { Codecprintf(NULL, "Content encoding scope of %d not expected\n", scope); return -1; } if (type != 0) { Codecprintf(NULL, "Encrypted track\n"); return -2; } KaxContentCompression & comp = GetChild(encoding); int algo = uint32(GetChild(comp)); if (algo != 0) Codecprintf(NULL, "MKV: warning, track compression algorithm %d not zlib\n", algo); if ((*mkvTrack.desc)->dataFormat != kSubFormatVobSub) Codecprintf(NULL, "MKV: warning, compressed track %s probably won't work (not VobSub)\n", FourCCString((*mkvTrack.desc)->dataFormat)); Handle ext = NewHandle(1); **ext = algo; if (mkvTrack.type == track_audio) AddSoundDescriptionExtension((SoundDescriptionHandle)mkvTrack.desc, ext, kMKVCompressionExtension); else AddImageDescriptionExtension((ImageDescriptionHandle)mkvTrack.desc, ext, kMKVCompressionExtension); return noErr; } ComponentResult MatroskaImport::AddVideoTrack(KaxTrackEntry &kaxTrack, MatroskaTrack &mkvTrack, KaxContentEncodings *encodings) { ComponentResult err = noErr; ImageDescriptionHandle imgDesc; Fixed width, height; KaxTrackVideo &videoTrack = GetChild(kaxTrack); KaxVideoDisplayWidth & disp_width = GetChild(videoTrack); KaxVideoDisplayHeight & disp_height = GetChild(videoTrack); KaxVideoPixelWidth & pxl_width = GetChild(videoTrack); KaxVideoPixelHeight & pxl_height = GetChild(videoTrack); // Use the PixelWidth if the DisplayWidth is not set if (disp_width.ValueIsSet() || disp_height.ValueIsSet()) { // some files ignore the spec and treat display width/height as a ratio, not as pixels // so scale the display size to be at least as large as the pixel size here // but don't let it be bigger in both dimensions uint32 displayWidth = disp_width.ValueIsSet() ? uint32(disp_width) : uint32(pxl_width); uint32 displayHeight = disp_height.ValueIsSet() ? uint32(disp_height) : uint32(pxl_height); float horizRatio = float(uint32(pxl_width)) / displayWidth; float vertRatio = float(uint32(pxl_height)) / displayHeight; if (vertRatio > horizRatio && vertRatio > 1) { width = FloatToFixed(displayWidth * vertRatio); height = FloatToFixed(displayHeight * vertRatio); } else if (horizRatio > 1) { width = FloatToFixed(displayWidth * horizRatio); height = FloatToFixed(displayHeight * horizRatio); } else { float dar = displayWidth / (float)displayHeight; float p_ratio = uint32(pxl_width) / (float)uint32(pxl_height); if (dar > p_ratio) { width = FloatToFixed(uint32(pxl_height) * dar); height = IntToFixed(uint32(pxl_height)); } else { width = IntToFixed(uint32(pxl_width)); height = FloatToFixed(uint32(pxl_width) / dar); } } } else if (pxl_width.ValueIsSet() && pxl_height.ValueIsSet()) { width = IntToFixed(uint32(pxl_width)); height = IntToFixed(uint32(pxl_height)); } else { Codecprintf(NULL, "MKV: Video has unknown dimensions.\n"); return invalidTrack; } mkvTrack.theTrack = NewMovieTrack(theMovie, width, height, kNoVolume); if (mkvTrack.theTrack == NULL) return GetMoviesError(); mkvTrack.theMedia = NewTrackMedia(mkvTrack.theTrack, 'vide', GetMovieTimeScale(theMovie), dataRef, dataRefType); if (mkvTrack.theMedia == NULL) { DisposeMovieTrack(mkvTrack.theTrack); return GetMoviesError(); } imgDesc = (ImageDescriptionHandle) NewHandleClear(sizeof(ImageDescription)); (*imgDesc)->idSize = sizeof(ImageDescription); (*imgDesc)->width = uint16(pxl_width); (*imgDesc)->height = uint16(pxl_height); (*imgDesc)->frameCount = 1; (*imgDesc)->cType = MkvGetFourCC(&kaxTrack); (*imgDesc)->depth = 24; (*imgDesc)->clutID = -1; set_track_clean_aperture_ext(imgDesc, width, height, IntToFixed(uint32(pxl_width)), IntToFixed(uint32(pxl_height))); set_track_colorspace_ext(imgDesc, width, height); mkvTrack.desc = (SampleDescriptionHandle) imgDesc; // this sets up anything else needed in the description for the specific codec. err = MkvFinishSampleDescription(&kaxTrack, (SampleDescriptionHandle) imgDesc, kToSampleDescription); if (err) return err; if(encodings) { KaxContentEncoding & encoding = GetChild(*encodings); int scope = uint32(GetChild(encoding)); int type = uint32(GetChild(encoding)); if (scope != 1) { Codecprintf(NULL, "Content encoding scope of %d not expected\n", scope); } if (type != 0) { Codecprintf(NULL, "Encrypted track\n"); } if(scope == 1 && type == 0) { KaxContentCompression & comp = GetChild(encoding); int algo = uint32(GetChild(comp)); if (algo != 3) Codecprintf(NULL, "MKV: warning, track compression algorithm %d not stripped header\n", algo); SampleDescriptionHandle sampleDesc = mkvTrack.desc; OSType compressedType = compressStreamFourCC((*sampleDesc)->dataFormat); if (compressedType == 0) Codecprintf(NULL, "MKV: warning, compressed track %s probably won't work\n", FourCCString((*mkvTrack.desc)->dataFormat)); else { Handle ext = NewHandle(4); memcpy(*ext, &algo, 4); (*sampleDesc)->dataFormat = compressedType; AddImageDescriptionExtension((ImageDescriptionHandle)mkvTrack.desc, ext, kCompressionAlgorithm); DisposeHandle(ext); KaxContentCompSettings & settings = GetChild(comp); uint8_t *compSettings = (uint8_t *)settings.GetBuffer(); int compSize = settings.GetSize(); if(compSize > 0) { ext = NewHandle(compSize); memcpy(*ext, compSettings, compSize); AddImageDescriptionExtension((ImageDescriptionHandle)mkvTrack.desc, ext, kCompressionSettingsExtension); DisposeHandle(ext); } } } } // video tracks can have display offsets, so create a sample table err = QTSampleTableCreateMutable(NULL, GetMovieTimeScale(theMovie), NULL, &mkvTrack.sampleTable); if (err) return err; err = QTSampleTableAddSampleDescription(mkvTrack.sampleTable, mkvTrack.desc, 0, &mkvTrack.qtSampleDesc); return err; } ComponentResult MatroskaImport::AddAudioTrack(KaxTrackEntry &kaxTrack, MatroskaTrack &mkvTrack, KaxContentEncodings *encodings) { SoundDescriptionHandle sndDesc = NULL; AudioStreamBasicDescription asbd = {0}; AudioChannelLayout acl = {0}; AudioChannelLayout *pacl = &acl; ByteCount acl_size = sizeof(acl); ByteCount ioSize = sizeof(asbd); ByteCount cookieSize = 0; Handle cookieH = NULL; Ptr cookie = NULL; OSStatus err = noErr; mkvTrack.theTrack = NewMovieTrack(theMovie, 0, 0, kFullVolume); if (mkvTrack.theTrack == NULL) return GetMoviesError(); mkvTrack.theMedia = NewTrackMedia(mkvTrack.theTrack, 'soun', GetMovieTimeScale(theMovie), dataRef, dataRefType); if (mkvTrack.theMedia == NULL) { DisposeMovieTrack(mkvTrack.theTrack); return GetMoviesError(); } KaxTrackAudio & audioTrack = GetChild(kaxTrack); KaxAudioSamplingFreq & sampleFreq = GetChild(audioTrack); KaxAudioChannels & numChannels = GetChild(audioTrack); KaxAudioBitDepth & bitDepth = GetChild(audioTrack); if (bitDepth.ValueIsSet()) asbd.mBitsPerChannel = uint32(bitDepth); asbd.mFormatID = MkvGetFourCC(&kaxTrack); asbd.mSampleRate = Float64(sampleFreq); asbd.mChannelsPerFrame = uint32(numChannels); MkvFinishAudioDescription(&kaxTrack, &cookieH, &asbd, &acl); if (cookieH) { cookie = *cookieH; cookieSize = GetHandleSize(cookieH); } // get more info about the codec err = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, cookieSize, cookie, &ioSize, &asbd); if(asbd.mChannelsPerFrame == 0 || asbd.mFormatID == 0) { Codecprintf(NULL, "Audio channels or format not set in MKV\n"); goto err; // better to fail than import with the wrong number of channels... } if (err) { Codecprintf(NULL, "AudioFormatGetProperty failed (error %lx / format %lx)\n", err, asbd.mFormatID); } // see ff_private.c initialize_audio_map if (!asbd.mFramesPerPacket && asbd.mFormatID == kAudioFormatMPEGLayer3) asbd.mFramesPerPacket = asbd.mSampleRate > 24000 ? 1152 : 576; // FIXME: mChannelLayoutTag == 0 is valid // but we don't use channel position lists (yet) so it's safe for now if (acl.mChannelLayoutTag == 0) acl = GetDefaultChannelLayout(&asbd); if (acl.mChannelLayoutTag == 0) { pacl = NULL; acl_size = 0; } if(encodings) { KaxContentEncoding & encoding = GetChild(*encodings); int scope = uint32(GetChild(encoding)); int type = uint32(GetChild(encoding)); if (scope != 1) { Codecprintf(NULL, "Content encoding scope of %d not expected\n", scope); } if (type != 0) { Codecprintf(NULL, "Encrypted track\n"); } if(scope == 1 && type == 0) { KaxContentCompression & comp = GetChild(encoding); int algo = uint32(GetChild(comp)); if (algo != 3) Codecprintf(NULL, "MKV: warning, track compression algorithm %d not stripped header\n", algo); OSType compressedType = compressStreamFourCC(asbd.mFormatID); if (compressedType == 0) Codecprintf(NULL, "MKV: warning, compressed track %s probably won't work\n", FourCCString(asbd.mFormatID)); else { asbd.mFormatID = compressedType; uint32_t algoHeader[] = { EndianS32_NtoB(12), EndianS32_NtoB(kCompressionAlgorithm), EndianS32_NtoB(algo) }; Handle newCookieHandle; PtrToHand(algoHeader, &newCookieHandle, sizeof(algoHeader)); KaxContentCompSettings & settings = GetChild(comp); uint8_t *compSettings = (uint8_t *)settings.GetBuffer(); int compSize = settings.GetSize(); if(compSize > 0) { uint32_t settingsHeader[] = { EndianS32_NtoB(8 + compSize), EndianS32_NtoB(kCompressionSettingsExtension), }; PtrAndHand(settingsHeader, newCookieHandle, sizeof(settingsHeader)); PtrAndHand(compSettings, newCookieHandle, compSize); } if(cookieSize) { HandAndHand(cookieH, newCookieHandle); DisposeHandle(cookieH); } cookieH = newCookieHandle; cookie = *cookieH; cookieSize = GetHandleSize(cookieH); } } } err = QTSoundDescriptionCreate(&asbd, pacl, acl_size, cookie, cookieSize, kQTSoundDescriptionKind_Movie_LowestPossibleVersion, &sndDesc); if (err) { Codecprintf(NULL, "Borked audio track entry, hoping we can parse the track for asbd\n"); DisposeHandle((Handle)sndDesc); return noErr; } mkvTrack.desc = (SampleDescriptionHandle) sndDesc; err = QTSampleTableCreateMutable(NULL, GetMovieTimeScale(theMovie), NULL, &mkvTrack.sampleTable); if (err) goto err; err = QTSampleTableAddSampleDescription(mkvTrack.sampleTable, mkvTrack.desc, 0, &mkvTrack.qtSampleDesc); err: if (cookieH) DisposeHandle(cookieH); return err; } ComponentResult MatroskaImport::AddSubtitleTrack(KaxTrackEntry &kaxTrack, MatroskaTrack &mkvTrack, KaxContentEncodings *encodings) { Fixed trackWidth, trackHeight; Rect movieBox; MediaHandler mh; ImageDescriptionHandle imgDesc = (ImageDescriptionHandle) NewHandleClear(sizeof(ImageDescription)); mkvTrack.desc = (SampleDescriptionHandle) imgDesc; // we assume that a video track has already been created, so we can set the subtitle track // to have the same dimensions as it. Note that this doesn't work so well with multiple // video tracks with different dimensions; but QuickTime doesn't expect that; how should we handle them? GetMovieBox(theMovie, &movieBox); trackWidth = IntToFixed(movieBox.right - movieBox.left); trackHeight = IntToFixed(movieBox.bottom - movieBox.top); (*imgDesc)->idSize = sizeof(ImageDescription); (*imgDesc)->cType = MkvGetFourCC(&kaxTrack); (*imgDesc)->frameCount = 1; (*imgDesc)->depth = 32; (*imgDesc)->clutID = -1; if ((*imgDesc)->cType == kSubFormatVobSub) { int width, height; // bitmap width & height is in the codec private in text format KaxCodecPrivate & idxFile = GetChild(kaxTrack); string idxFileStr((char *)idxFile.GetBuffer(), idxFile.GetSize()); string::size_type loc = idxFileStr.find("size:", 0); if (loc == string::npos) return -1; sscanf(&idxFileStr.c_str()[loc], "size: %dx%d", &width, &height); (*imgDesc)->width = width; (*imgDesc)->height = height; if (trackWidth == 0 || trackHeight == 0) { trackWidth = IntToFixed(width); trackHeight = IntToFixed(height); } set_track_colorspace_ext(imgDesc, width, height); mkvTrack.theTrack = NewMovieTrack(theMovie, trackWidth, trackHeight, kNoVolume); if (mkvTrack.theTrack == NULL) return GetMoviesError(); mkvTrack.theMedia = NewTrackMedia(mkvTrack.theTrack, 'vide', GetMovieTimeScale(theMovie), dataRef, dataRefType); if (mkvTrack.theMedia == NULL) return GetMoviesError(); // finally, say that we're transparent mh = GetMediaHandler(mkvTrack.theMedia); SetSubtitleMediaHandlerTransparent(mh); // subtitle tracks should be above the video track, which should be layer 0 SetTrackLayer(mkvTrack.theTrack, -1); mkvTrack.is_vobsub = true; } else if ((*imgDesc)->cType == kSubFormatUTF8 || (*imgDesc)->cType == kSubFormatSSA || (*imgDesc)->cType == kSubFormatASS) { if ((*imgDesc)->cType == kSubFormatASS) (*imgDesc)->cType = kSubFormatSSA; // no real reason to treat them differently UInt32 emptyDataRefExtension[2]; // FIXME: the various uses of this bit of code should be unified mkvTrack.subDataRefHandler = NewHandleClear(sizeof(Handle) + 1); emptyDataRefExtension[0] = EndianU32_NtoB(sizeof(UInt32)*2); emptyDataRefExtension[1] = EndianU32_NtoB(kDataRefExtensionInitializationData); PtrAndHand(&emptyDataRefExtension[0], mkvTrack.subDataRefHandler, sizeof(emptyDataRefExtension)); mkvTrack.theTrack = CreatePlaintextSubTrack(theMovie, imgDesc, GetMovieTimeScale(theMovie), mkvTrack.subDataRefHandler, HandleDataHandlerSubType, (*imgDesc)->cType, NULL, movieBox); if (mkvTrack.theTrack == NULL) return GetMoviesError(); mkvTrack.theMedia = GetTrackMedia(mkvTrack.theTrack); mkvTrack.is_vobsub = false; BeginMediaEdits(mkvTrack.theMedia); } else { Codecprintf(NULL, "MKV: Unsupported subtitle type\n"); return noErr; } // this sets up anything else needed in the description for the specific codec. ComponentResult result = MkvFinishSampleDescription(&kaxTrack, (SampleDescriptionHandle) imgDesc, kToSampleDescription); if(encodings) { ReadVobSubContentEncodings(encodings, mkvTrack); } return result; } ComponentResult MatroskaImport::ReadChapters(KaxChapters &chapterEntries) { KaxEditionEntry & edition = GetChild(chapterEntries); UInt32 emptyDataRefExtension[2]; if (seenChapters) return noErr; chapterTrack = NewMovieTrack(theMovie, 0, 0, kNoVolume); if (chapterTrack == NULL) { Codecprintf(NULL, "MKV: Error creating chapter track %d\n", GetMoviesError()); return GetMoviesError(); } // we use a handle data reference here because I don't see any way to add textual // sample references (TextMediaAddTextSample() will behave the same as AddSample() // in that it modifies the original file if that's the data reference of the media) Handle dataRef = NewHandleClear(sizeof(Handle) + 1); emptyDataRefExtension[0] = EndianU32_NtoB(sizeof(UInt32)*2); emptyDataRefExtension[1] = EndianU32_NtoB(kDataRefExtensionInitializationData); PtrAndHand(&emptyDataRefExtension[0], dataRef, sizeof(emptyDataRefExtension)); Media chapterMedia = NewTrackMedia(chapterTrack, TextMediaType, GetMovieTimeScale(theMovie), dataRef, HandleDataHandlerSubType); if (chapterMedia == NULL) { OSErr err = GetMoviesError(); Codecprintf(NULL, "MKV: Error creating chapter media %d\n", err); DisposeMovieTrack(chapterTrack); return err; } // Name the chapter track "Chapters" for easy distinguishing QTMetaDataRef trackMetaData; OSErr err = QTCopyTrackMetaData(chapterTrack, &trackMetaData); if (err == noErr) { OSType key = kUserDataName; string chapterName("Chapters"); QTMetaDataAddItem(trackMetaData, kQTMetaDataStorageFormatUserData, kQTMetaDataKeyFormatUserData, (UInt8 *)&key, sizeof(key), (UInt8 *)chapterName.c_str(), chapterName.size(), kQTMetaDataTypeUTF8, NULL); QTMetaDataRelease(trackMetaData); } BeginMediaEdits(chapterMedia); // tell the text media handler the upcoming text samples are // encoded in Unicode with a byte order mark (BOM) MediaHandler mediaHandler = GetMediaHandler(chapterMedia); SInt32 dataPtr = kTextEncodingUnicodeDefault; TextMediaSetTextSampleData(mediaHandler, &dataPtr, kTXNTextEncodingAttribute); KaxChapterAtom *chapterAtom = FindChild(edition); while (chapterAtom && chapterAtom->GetSize() > 0) { AddChapterAtom(chapterAtom); chapterAtom = &GetNextChild(edition, *chapterAtom); } EndMediaEdits(chapterMedia); SetTrackEnabled(chapterTrack, false); seenChapters = true; return noErr; } void MatroskaImport::AddChapterAtom(KaxChapterAtom *atom) { KaxChapterAtom *subChapter = FindChild(*atom); bool addThisChapter = true; // since QuickTime only supports linear chapter tracks (no nesting), only add chapter leaves if (subChapter && subChapter->GetSize() > 0) { while (subChapter && subChapter->GetSize() > 0) { KaxChapterFlagHidden &hideChapter = GetChild(*subChapter); if (!uint8_t(hideChapter)) { AddChapterAtom(subChapter); addThisChapter = false; } subChapter = &GetNextChild(*atom, *subChapter); } } if (addThisChapter) { // add the chapter to the track if it has no children KaxChapterTimeStart & startTime = GetChild(*atom); KaxChapterDisplay & chapDisplay = GetChild(*atom); KaxChapterString & chapString = GetChild(chapDisplay); MediaHandler mh = GetMediaHandler(GetTrackMedia(chapterTrack)); TimeValue start = UInt64(startTime) / timecodeScale; if (start > movieDuration) { Codecprintf(NULL, "MKV: Chapter time is beyond the end of the file\n"); return; } Rect bounds = {0, 0, 0, 0}; TimeValue inserted; OSErr err = TextMediaAddTextSample(mh, const_cast(UTFstring(chapString).GetUTF8().c_str()), UTFstring(chapString).GetUTF8().size(), 0, 0, 0, NULL, NULL, teCenter, &bounds, dfClipToTextBox, 0, 0, 0, NULL, 1, &inserted); if (err) Codecprintf(NULL, "MKV: Error adding text sample %d\n", err); else { InsertMediaIntoTrack(chapterTrack, start, inserted, 1, fixed1); } } } ComponentResult MatroskaImport::ReadAttachments(KaxAttachments &attachments) { KaxAttached *attachedFile = FindChild(attachments); while (attachedFile && attachedFile->GetSize() > 0) { string fileMimeType = GetChild(*attachedFile); string fileName = UTFstring(GetChild(*attachedFile)).GetUTF8(); /* The only attachments handled here are fonts, which currently can be truetype or opentype. application/x-* is probably not a permanent MIME type, but it is current practice... */ if ((fileMimeType == "application/x-truetype-font" || fileMimeType == "application/x-font-otf") && ShouldImportFontFileName(fileName.c_str())) { KaxFileData & fontData = GetChild(*attachedFile); if (fontData.GetSize()) { ATSFontContainerRef container; ATSFontActivateFromMemory(fontData.GetBuffer(), fontData.GetSize(), kATSFontContextLocal, kATSFontFormatUnspecified, NULL, kATSOptionFlagsDefault, &container); } } bool isCoverArt = false, isJPEG; if (fileName == "cover.jpg") { isCoverArt = isJPEG = true; } else if (fileName == "cover.png") { isCoverArt = true; isJPEG = false; } if (isCoverArt) { KaxFileData & fileData = GetChild(*attachedFile); FourCharCode key = 'covr'; //iTunes cover art tag QTMetaDataRef movieMetaData; QTCopyMovieMetaData(theMovie, &movieMetaData); OSErr err; err = QTMetaDataAddItem(movieMetaData, kQTMetaDataStorageFormatiTunes, kQTMetaDataKeyFormatiTunesShortForm, (UInt8 *)&key, sizeof(key), fileData.GetBuffer(), fileData.GetSize(), isJPEG ? kQTMetaDataTypeJPEGImage : kQTMetaDataTypePNGImage, NULL); if (err) Codecprintf(NULL, "MKV: Error adding cover art %d\n", (int)err); QTMetaDataRelease(movieMetaData); } attachedFile = &GetNextChild(attachments, *attachedFile); } return noErr; } ComponentResult MatroskaImport::ReadMetaSeek(KaxSeekHead &seekHead) { ComponentResult err = noErr; KaxSeek *seekEntry = FindChild(seekHead); // don't re-read a seek head that's already been read uint64_t currPos = seekHead.GetElementPosition(); vector::iterator itr = levelOneElements.begin(); for (; itr != levelOneElements.end(); itr++) { if (itr->GetID() == KaxSeekHead::ClassInfos.GlobalId && itr->segmentPos + segmentOffset == currPos) return noErr; } while (seekEntry && seekEntry->GetSize() > 0) { MatroskaSeek newSeekEntry; KaxSeekID & seekID = GetChild(*seekEntry); KaxSeekPosition & position = GetChild(*seekEntry); EbmlId elementID = EbmlId(seekID.GetBuffer(), seekID.GetSize()); newSeekEntry.ebmlID = elementID.Value; newSeekEntry.idLength = elementID.Length; newSeekEntry.segmentPos = position; // recursively read seek heads that are pointed to by the current one // as well as the level one elements we care about if (elementID == KaxInfo::ClassInfos.GlobalId || elementID == KaxTracks::ClassInfos.GlobalId || elementID == KaxChapters::ClassInfos.GlobalId || elementID == KaxAttachments::ClassInfos.GlobalId || elementID == KaxSeekHead::ClassInfos.GlobalId) { MatroskaSeekContext savedContext = SaveContext(); SetContext(newSeekEntry.GetSeekContext(segmentOffset)); if (NextLevel1Element()) err = ProcessLevel1Element(); SetContext(savedContext); if (err) return err; } levelOneElements.push_back(newSeekEntry); seekEntry = &GetNextChild(seekHead, *seekEntry); } sort(levelOneElements.begin(), levelOneElements.end()); return noErr; } void MatroskaImport::ImportCluster(KaxCluster &cluster, bool addToTrack) { KaxSegment & segment = *static_cast(el_l0); KaxClusterTimecode & clusterTime = GetChild(cluster); cluster.SetParent(segment); cluster.InitTimecode(uint64(clusterTime), timecodeScale); for (int i = 0; i < cluster.ListSize(); i++) { const EbmlId & elementID = EbmlId(*cluster[i]); KaxInternalBlock *block = NULL; uint32_t duration = 0; // set to track's default duration in AddBlock if 0 short flags = 0; if (elementID == KaxBlockGroup::ClassInfos.GlobalId) { KaxBlockGroup & blockGroup = *static_cast(cluster[i]); KaxBlockDuration & blkDuration = GetChild(blockGroup); block = &GetChild(blockGroup); if (blkDuration.ValueIsSet()) duration = uint32(blkDuration); flags = blockGroup.ReferenceCount() > 0 ? mediaSampleNotSync : 0; } else if (elementID == KaxSimpleBlock::ClassInfos.GlobalId) { KaxSimpleBlock & simpleBlock = *static_cast(cluster[i]); block = &simpleBlock; if (!simpleBlock.IsKeyframe()) flags |= mediaSampleNotSync; if (simpleBlock.IsDiscardable() && IsFrameDroppingEnabled()) flags |= mediaSampleDroppable; } if (block) { block->SetParent(cluster); for (int j = 0; j < tracks.size(); j++) { if (tracks[j].number == block->TrackNum()) { tracks[j].AddBlock(*block, duration, flags); break; } } } } if (addToTrack) { for (int i = 0; i < tracks.size(); i++) tracks[i].AddSamplesToTrack(); loadState = kMovieLoadStatePlayable; } } MatroskaSeekContext MatroskaImport::SaveContext() { MatroskaSeekContext ret = { el_l1, ioHandler->getFilePointer() }; el_l1 = NULL; return ret; } void MatroskaImport::SetContext(MatroskaSeekContext context) { if (el_l1) delete el_l1; el_l1 = context.el_l1; ioHandler->setFilePointer(context.position); } void MatroskaImport::PrerollSubtitleTracks() { if (!seenTracks) return; for (int i = 0; i < tracks.size(); i++) { MatroskaTrack *track = &tracks[i]; if (track->type == track_subtitle) { Handle subtitleDescriptionExt; OSErr err = GetImageDescriptionExtension((ImageDescriptionHandle)track->desc, &subtitleDescriptionExt, kSubFormatSSA, 1); if (err || !subtitleDescriptionExt) continue; SubRendererPrerollFromHeader(*subtitleDescriptionExt, GetHandleSize(subtitleDescriptionExt)); } } } MatroskaTrack::MatroskaTrack() { number = 0; type = -1; theTrack = NULL; theMedia = NULL; desc = NULL; sampleTable = NULL; qtSampleDesc = 0; timecodeScale = 1000000; maxLoadedTime = 0; seenFirstBlock = false; firstSample = -1; numSamples = 0; durationToAdd = 0; displayOffsetSum = 0; durationSinceZeroSum = 0; subtitleSerializer = new CXXSubSerializer; subDataRefHandler = NULL; is_vobsub = false; isEnabled = true; defaultDuration = 0; usesLacing = true; currentFrame = 0; } MatroskaTrack::MatroskaTrack(const MatroskaTrack ©) { number = copy.number; type = copy.type; theTrack = copy.theTrack; theMedia = copy.theMedia; if (copy.desc) { desc = (SampleDescriptionHandle) NewHandle((*copy.desc)->descSize); memcpy(*desc, *copy.desc, (*copy.desc)->descSize); } else desc = NULL; sampleTable = copy.sampleTable; if (sampleTable) QTSampleTableRetain(sampleTable); qtSampleDesc = copy.qtSampleDesc; timecodeScale = copy.timecodeScale; maxLoadedTime = copy.maxLoadedTime; for (int i = 0; i < copy.lastFrames.size(); i++) lastFrames.push_back(copy.lastFrames[i]); seenFirstBlock = copy.seenFirstBlock; firstSample = copy.firstSample; numSamples = copy.numSamples; durationToAdd = copy.durationToAdd; displayOffsetSum = copy.displayOffsetSum; durationSinceZeroSum = copy.durationSinceZeroSum; subtitleSerializer = copy.subtitleSerializer; subtitleSerializer->retain(); subDataRefHandler = copy.subDataRefHandler; is_vobsub = copy.is_vobsub; isEnabled = copy.isEnabled; defaultDuration = copy.defaultDuration; usesLacing = copy.usesLacing; currentFrame = copy.currentFrame; } MatroskaTrack::~MatroskaTrack() { if (desc) DisposeHandle((Handle) desc); if (sampleTable) QTSampleTableRelease(sampleTable); if (subtitleSerializer) subtitleSerializer->release(); } void MatroskaTrack::ParseFirstBlock(KaxInternalBlock &block) { AudioStreamBasicDescription asbd = {0}; AudioChannelLayout acl = {0}; bool replaceSoundDesc = false; lowestPTS = block.GlobalTimecode(); if (desc) { switch ((*desc)->dataFormat) { case kAudioFormatAC3: replaceSoundDesc = parse_ac3_bitstream(&asbd, &acl, block.GetBuffer(0).Buffer(), block.GetFrameSize(0)); break; } } if (replaceSoundDesc) { // successful in parsing, so the acl and asbd are more correct than what we generated in // AddAudioTrack() so replace our sound description SoundDescriptionHandle sndDesc = NULL; OSStatus err = QTSoundDescriptionCreate(&asbd, &acl, sizeof(AudioChannelLayout), NULL, 0, kQTSoundDescriptionKind_Movie_LowestPossibleVersion, &sndDesc); if (err == noErr) { DisposeHandle((Handle) desc); desc = (SampleDescriptionHandle) sndDesc; QTSampleTableAddSampleDescription(sampleTable, desc, 0, &qtSampleDesc); } } } void MatroskaTrack::AddBlock(KaxInternalBlock &block, uint32 frameDuration, short flags) { if (!seenFirstBlock) { ParseFirstBlock(block); seenFirstBlock = true; } // tracks w/ lacing can't have b-frames, and neither can any known audio codec if (usesLacing || type != track_video) { // don't add the blocks until we get one with a new timecode TimeValue64 duration = 0; if (lastFrames.size() > 0) duration = block.GlobalTimecode() / timecodeScale - lastFrames[0].pts; if (duration > 0) { for (int i = 0; i < lastFrames.size(); i++) { // since there can be multiple frames in one block, split the duration evenly between them // giving the remainder to the latter blocks int remainder = duration % lastFrames.size() >= lastFrames.size() - i ? 1 : 0; lastFrames[i].duration = duration / lastFrames.size() + remainder; AddFrame(lastFrames[i]); } lastFrames.clear(); } } else if (ptsReorder.size() - currentFrame > MAX_DECODE_DELAY + 1) { map::iterator duration; MatroskaFrame &curr = lastFrames[currentFrame]; MatroskaFrame &next = lastFrames[currentFrame+1]; currentFrame++; // pts -> dts works this way: we assume that the first frame has correct dts // (we start at a keyframe, so pts = dts), and then we fill up a buffer with // frames until we have the frame whose pts is equal to the next dts // Then, we sort this buffer, extract the pts as dts, and calculate the duration. ptsReorder.sort(); next.dts = *(++ptsReorder.begin()); ptsReorder.pop_front(); // Duration calculation has to be done between consecutive pts. Since we reorder // the pts into the dts, which have to be in order, we calculate the duration then // from the dts, then save it for the frame with the same pts. durationForPTS[curr.dts] = next.dts - curr.dts; duration = durationForPTS.find(lastFrames[0].pts); if (duration != durationForPTS.end()) { lastFrames[0].duration = duration->second; AddFrame(lastFrames[0]); lastFrames.erase(lastFrames.begin()); durationForPTS.erase(duration); currentFrame--; } } for (int i = 0; i < block.NumberFrames(); i++) { MatroskaFrame newFrame; newFrame.pts = block.GlobalTimecode() / timecodeScale; newFrame.dts = newFrame.pts; if (frameDuration > 0) newFrame.duration = frameDuration; else newFrame.duration = defaultDuration; newFrame.offset = block.GetDataPosition(i); newFrame.size = block.GetFrameSize(i); newFrame.flags = flags; if (type == track_subtitle) { newFrame.buffer = &block.GetBuffer(i); AddFrame(newFrame); } else { lastFrames.push_back(newFrame); if (!usesLacing && type == track_video) { //Codecprintf(NULL, "push_back pts %lld dts %lld\n", newFrame.pts, newFrame.dts); ptsReorder.push_back(newFrame.pts); } } newFrame.buffer = NULL; } } void MatroskaTrack::AddFrame(MatroskaFrame &frame) { ComponentResult err = noErr; TimeValue sampleTime = 0; TimeValue64 displayOffset = frame.pts - frame.dts; if (desc == NULL) return; if (frame.duration == 0 && type != track_subtitle) { // try to correct duplicate timestamps // not that we can be sure what the real duration is... frame.duration = MAX(defaultDuration, 1); } if (type != track_video) frame.flags &= ~mediaSampleDroppable; if (type == track_subtitle && !is_vobsub) { Handle packet=NULL; unsigned start=0, end=0; if (frame.size > 0 && frame.duration > 0) subtitleSerializer->pushLine((const char*)frame.buffer->Buffer(), frame.buffer->Size(), frame.pts, frame.pts + frame.duration); packet = subtitleSerializer->popPacket(&start, &end); if (packet) { err = AddMediaSample(theMedia, packet, 0, GetHandleSize(packet), end - start, desc, 1, 0, &sampleTime); if (err) { Codecprintf(NULL, "MKV: error adding subtitle sample %d\n", (int)err); return; } DisposeHandle(packet); frame.pts = start; frame.duration = end - start; } else return; } else if (sampleTable) { if(frame.duration) { SInt64 sampleNum; //assert(displayOffset >= 0); //Codecprintf(NULL, "Add frame offset %lld, size %lld, duration %ld, offset %lld\n", frame.offset, frame.size, frame.duration, displayOffset); err = QTSampleTableAddSampleReferences(sampleTable, frame.offset, frame.size, frame.duration, displayOffset, 1, frame.flags, qtSampleDesc, &sampleNum); if (err) { Codecprintf(NULL, "MKV: error adding sample reference to table %d\n", (int)err); return; } if (firstSample == -1) firstSample = sampleNum; numSamples++; } } else { SampleReference64Record sample; sample.dataOffset = SInt64ToWide(frame.offset); sample.dataSize = frame.size; sample.durationPerSample = frame.duration; sample.numberOfSamples = 1; sample.sampleFlags = frame.flags; err = AddMediaSampleReferences64(theMedia, desc, 1, &sample, &sampleTime); if (err) { Codecprintf(NULL, "MKV: error adding sample reference to media %d\n", (int)err); return; } } // add to track immediately if subtitle, otherwise we let it be added elsewhere when we can do several at once if (type == track_subtitle) { err = InsertMediaIntoTrack(theTrack, frame.pts, sampleTime, frame.duration, fixed1); if (err) { Codecprintf(NULL, "MKV: error adding subtitle media into track %d\n", (int)err); return; } } else { durationSinceZeroSum += frame.duration; displayOffsetSum += displayOffset; if (displayOffsetSum == 0) { durationToAdd += durationSinceZeroSum; durationSinceZeroSum = 0; } } } void MatroskaTrack::AddSamplesToTrack() { OSStatus err = noErr; if (type == track_subtitle) return; // handled in AddFrame() if (durationToAdd == 0 && numSamples == 0) // nothing to add return; if (sampleTable) { if (firstSample == -1) return; // nothing to add err = AddSampleTableToMedia(theMedia, sampleTable, firstSample, numSamples, NULL); if (err) { Codecprintf(NULL, "MKV: error adding sample table to the media %d\n", (int)err); durationToAdd = 0; firstSample = -1; numSamples = 0; return; } firstSample = -1; numSamples = 0; } err = InsertMediaIntoTrack(theTrack, -1, maxLoadedTime, durationToAdd, fixed1); if (err) Codecprintf(NULL, "MKV: error inserting media into track %d\n", (int)err); if (!err) { if (!maxLoadedTime && lowestPTS) SetTrackOffset(theTrack, lowestPTS / timecodeScale); maxLoadedTime += durationToAdd; } durationToAdd = 0; } void MatroskaTrack::FinishTrack() { if (type == track_subtitle && !is_vobsub) { subtitleSerializer->setFinished(); do { MatroskaFrame fr = {0}; AddFrame(fr); // add empty frames to flush the subtitle packet queue } while (!subtitleSerializer->empty()); EndMediaEdits(theMedia); } else { if (lastFrames.size()) { #if 0 // the values produced by this seem to be worse than before // also, the dts in lastFrames seems to be out of order? for (int i = 0; i < lastFrames.size()-1; i++) { MatroskaFrame &curr = lastFrames[i], &next = lastFrames[i+1]; printf("cur: pts %lld dts %lld dur %d, next: pts %lld dts %lld dur %d\n", curr.pts, curr.dts, (int)curr.duration, next.pts, next.dts, (int)next.duration); curr.duration = next.dts - curr.dts; //assert(curr.duration < 65536 && curr.duration > 0); } #endif for (int i = 0; i < lastFrames.size(); i++) AddFrame(lastFrames[i]); lastFrames.resize(0); } AddSamplesToTrack(); } }