/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "MoofParser.h" #include "Box.h" #include "SinfParser.h" #include #include "MP4Interval.h" #include "mozilla/CheckedInt.h" #include "mozilla/HelperMacros.h" #include "mozilla/Logging.h" #if defined(MOZ_FMP4) extern mozilla::LogModule* GetDemuxerLog(); # define LOG_ERROR(name, arg, ...) \ MOZ_LOG( \ GetDemuxerLog(), mozilla::LogLevel::Error, \ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__)) # define LOG_WARN(name, arg, ...) \ MOZ_LOG( \ GetDemuxerLog(), mozilla::LogLevel::Warning, \ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__)) # define LOG_DEBUG(name, arg, ...) \ MOZ_LOG( \ GetDemuxerLog(), mozilla::LogLevel::Debug, \ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__)) #else # define LOG_ERROR(...) # define LOG_WARN(...) # define LOG_DEBUG(...) #endif namespace mozilla { const uint32_t kKeyIdSize = 16; // We ensure there are no gaps in samples' CTS between the last sample in a // Moof, and the first sample in the next Moof, if they're within these many // Microseconds of each other. const Microseconds CROSS_MOOF_CTS_MERGE_THRESHOLD = 1; bool MoofParser::RebuildFragmentedIndex(const MediaByteRangeSet& aByteRanges) { BoxContext context(mSource, aByteRanges); return RebuildFragmentedIndex(context); } bool MoofParser::RebuildFragmentedIndex(const MediaByteRangeSet& aByteRanges, bool* aCanEvict) { MOZ_ASSERT(aCanEvict); if (*aCanEvict && mMoofs.Length() > 1) { MOZ_ASSERT(mMoofs.Length() == mMediaRanges.Length()); mMoofs.RemoveElementsAt(0, mMoofs.Length() - 1); mMediaRanges.RemoveElementsAt(0, mMediaRanges.Length() - 1); *aCanEvict = true; } else { *aCanEvict = false; } return RebuildFragmentedIndex(aByteRanges); } bool MoofParser::RebuildFragmentedIndex(BoxContext& aContext) { LOG_DEBUG( Moof, "Starting, mTrackParseMode=%s, track#=%" PRIu32 " (ignore if multitrack).", mTrackParseMode.is() ? "multitrack" : "single track", mTrackParseMode.is() ? 0 : mTrackParseMode.as()); bool foundValidMoof = false; for (Box box(&aContext, mOffset); box.IsAvailable(); box = box.Next()) { if (box.IsType("moov") && mInitRange.IsEmpty()) { mInitRange = MediaByteRange(0, box.Range().mEnd); ParseMoov(box); } else if (box.IsType("moof")) { Moof moof(box, mTrackParseMode, mTrex, mMvhd, mMdhd, mEdts, mSinf, &mLastDecodeTime, mIsAudio, mTracksEndCts); if (!moof.IsValid() && !box.Next().IsAvailable()) { // Moof isn't valid abort search for now. LOG_WARN(Moof, "Could not find valid moof, moof may not be complete yet."); break; } if (!mMoofs.IsEmpty()) { // Stitch time ranges together in the case of a (hopefully small) time // range gap between moofs. mMoofs.LastElement().FixRounding(moof); } mMoofs.AppendElement(moof); mMediaRanges.AppendElement(moof.mRange); foundValidMoof = true; } else if (box.IsType("mdat") && !Moofs().IsEmpty()) { // Check if we have all our data from last moof. Moof& moof = Moofs().LastElement(); media::Interval datarange(moof.mMdatRange.mStart, moof.mMdatRange.mEnd, 0); media::Interval mdat(box.Range().mStart, box.Range().mEnd, 0); if (datarange.Intersects(mdat)) { mMediaRanges.LastElement() = mMediaRanges.LastElement().Span(box.Range()); } } mOffset = box.NextOffset(); } MOZ_ASSERT(mTrackParseMode.is() || mTrex.mTrackId == mTrackParseMode.as(), "If not parsing all tracks, mTrex should have the same track id " "as the track being parsed."); LOG_DEBUG(Moof, "Done, foundValidMoof=%s.", foundValidMoof ? "true" : "false"); return foundValidMoof; } MediaByteRange MoofParser::FirstCompleteMediaHeader() { if (Moofs().IsEmpty()) { return MediaByteRange(); } return Moofs()[0].mRange; } MediaByteRange MoofParser::FirstCompleteMediaSegment() { for (uint32_t i = 0; i < mMediaRanges.Length(); i++) { if (mMediaRanges[i].Contains(Moofs()[i].mMdatRange)) { return mMediaRanges[i]; } } return MediaByteRange(); } DDLoggedTypeDeclNameAndBase(BlockingStream, ByteStream); class BlockingStream : public ByteStream, public DecoderDoctorLifeLogger { public: explicit BlockingStream(ByteStream* aStream) : mStream(aStream) { DDLINKCHILD("stream", aStream); } bool ReadAt(int64_t offset, void* data, size_t size, size_t* bytes_read) override { return mStream->ReadAt(offset, data, size, bytes_read); } bool CachedReadAt(int64_t offset, void* data, size_t size, size_t* bytes_read) override { return mStream->ReadAt(offset, data, size, bytes_read); } virtual bool Length(int64_t* size) override { return mStream->Length(size); } private: RefPtr mStream; }; bool MoofParser::BlockingReadNextMoof() { LOG_DEBUG(Moof, "Starting."); int64_t length = std::numeric_limits::max(); mSource->Length(&length); RefPtr stream = new BlockingStream(mSource); MediaByteRangeSet byteRanges(MediaByteRange(0, length)); BoxContext context(stream, byteRanges); for (Box box(&context, mOffset); box.IsAvailable(); box = box.Next()) { if (box.IsType("moof")) { MediaByteRangeSet parseByteRanges( MediaByteRange(mOffset, box.Range().mEnd)); BoxContext parseContext(stream, parseByteRanges); if (RebuildFragmentedIndex(parseContext)) { LOG_DEBUG(Moof, "Succeeded on RebuildFragmentedIndex, returning true."); return true; } } } LOG_DEBUG(Moof, "Couldn't read next moof, returning false."); return false; } void MoofParser::ScanForMetadata(mozilla::MediaByteRange& aMoov) { LOG_DEBUG(Moof, "Starting."); int64_t length = std::numeric_limits::max(); mSource->Length(&length); MediaByteRangeSet byteRanges; byteRanges += MediaByteRange(0, length); RefPtr stream = new BlockingStream(mSource); BoxContext context(stream, byteRanges); for (Box box(&context, mOffset); box.IsAvailable(); box = box.Next()) { if (box.IsType("moov")) { aMoov = box.Range(); break; } } mInitRange = aMoov; LOG_DEBUG(Moof, "Done, mInitRange.mStart=%" PRIi64 ", mInitRange.mEnd=%" PRIi64, mInitRange.mStart, mInitRange.mEnd); } already_AddRefed MoofParser::Metadata() { LOG_DEBUG(Moof, "Starting."); MediaByteRange moov; ScanForMetadata(moov); CheckedInt moovLength = moov.Length(); if (!moovLength.isValid() || !moovLength.value()) { // No moov, or cannot be used as array size. LOG_WARN(Moof, "Did not get usable moov length while trying to parse Metadata."); return nullptr; } RefPtr metadata = new MediaByteBuffer(); if (!metadata->SetLength(moovLength.value(), fallible)) { LOG_ERROR(Moof, "OOM"); return nullptr; } RefPtr stream = new BlockingStream(mSource); size_t read; bool rv = stream->ReadAt(moov.mStart, metadata->Elements(), moovLength.value(), &read); if (!rv || read != moovLength.value()) { LOG_WARN(Moof, "Failed to read moov while trying to parse Metadata."); return nullptr; } LOG_DEBUG(Moof, "Done, found metadata."); return metadata.forget(); } MP4Interval MoofParser::GetCompositionRange( const MediaByteRangeSet& aByteRanges) { LOG_DEBUG(Moof, "Starting."); MP4Interval compositionRange; BoxContext context(mSource, aByteRanges); for (size_t i = 0; i < mMoofs.Length(); i++) { Moof& moof = mMoofs[i]; Box box(&context, moof.mRange.mStart); if (box.IsAvailable()) { compositionRange = compositionRange.Extents(moof.mTimeRange); } } LOG_DEBUG(Moof, "Done, compositionRange.start=%" PRIi64 ", compositionRange.end=%" PRIi64 ".", compositionRange.start, compositionRange.end); return compositionRange; } bool MoofParser::ReachedEnd() { int64_t length; return mSource->Length(&length) && mOffset == length; } void MoofParser::ParseMoov(Box& aBox) { LOG_DEBUG(Moof, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("mvhd")) { mMvhd = Mvhd(box); } else if (box.IsType("trak")) { ParseTrak(box); } else if (box.IsType("mvex")) { ParseMvex(box); } } LOG_DEBUG(Moof, "Done."); } void MoofParser::ParseTrak(Box& aBox) { LOG_DEBUG(Trak, "Starting."); Tkhd tkhd; for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("tkhd")) { tkhd = Tkhd(box); } else if (box.IsType("mdia")) { if (mTrackParseMode.is() || tkhd.mTrackId == mTrackParseMode.as()) { ParseMdia(box); } } else if (box.IsType("edts") && (mTrackParseMode.is() || tkhd.mTrackId == mTrackParseMode.as())) { mEdts = Edts(box); } } LOG_DEBUG(Trak, "Done."); } void MoofParser::ParseMdia(Box& aBox) { LOG_DEBUG(Mdia, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("mdhd")) { mMdhd = Mdhd(box); } else if (box.IsType("minf")) { ParseMinf(box); } } LOG_DEBUG(Mdia, "Done."); } void MoofParser::ParseMvex(Box& aBox) { LOG_DEBUG(Mvex, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("trex")) { Trex trex = Trex(box); if (mTrackParseMode.is() || trex.mTrackId == mTrackParseMode.as()) { mTrex = trex; } } } LOG_DEBUG(Mvex, "Done."); } void MoofParser::ParseMinf(Box& aBox) { LOG_DEBUG(Minf, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("stbl")) { ParseStbl(box); } } LOG_DEBUG(Minf, "Done."); } void MoofParser::ParseStbl(Box& aBox) { LOG_DEBUG(Stbl, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("stsd")) { ParseStsd(box); } else if (box.IsType("sgpd")) { Sgpd sgpd(box); if (sgpd.IsValid() && sgpd.mGroupingType == "seig") { mTrackSampleEncryptionInfoEntries.Clear(); if (!mTrackSampleEncryptionInfoEntries.AppendElements( sgpd.mEntries, mozilla::fallible)) { LOG_ERROR(Stbl, "OOM"); return; } } } else if (box.IsType("sbgp")) { Sbgp sbgp(box); if (sbgp.IsValid() && sbgp.mGroupingType == "seig") { mTrackSampleToGroupEntries.Clear(); if (!mTrackSampleToGroupEntries.AppendElements(sbgp.mEntries, mozilla::fallible)) { LOG_ERROR(Stbl, "OOM"); return; } } } } LOG_DEBUG(Stbl, "Done."); } void MoofParser::ParseStsd(Box& aBox) { LOG_DEBUG(Stsd, "Starting."); if (mTrackParseMode.is()) { // It is not a sane operation to try and map sample description boxes from // multiple tracks onto the parser, which is modeled around storing metadata // for a single track. LOG_DEBUG(Stsd, "Early return due to multitrack parser."); return; } MOZ_ASSERT( mSampleDescriptions.IsEmpty(), "Shouldn't have any sample descriptions yet when starting to parse stsd"); uint32_t numberEncryptedEntries = 0; for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { SampleDescriptionEntry sampleDescriptionEntry{false}; if (box.IsType("encv") || box.IsType("enca")) { ParseEncrypted(box); sampleDescriptionEntry.mIsEncryptedEntry = true; numberEncryptedEntries++; } if (!mSampleDescriptions.AppendElement(sampleDescriptionEntry, mozilla::fallible)) { LOG_ERROR(Stsd, "OOM"); return; } } if (mSampleDescriptions.IsEmpty()) { LOG_WARN(Stsd, "No sample description entries found while parsing Stsd! This " "shouldn't happen, as the spec requires one for each track!"); } if (numberEncryptedEntries > 1) { LOG_WARN(Stsd, "More than one encrypted sample description entry found while " "parsing track! We don't expect this, and it will likely break " "during fragment look up!"); } LOG_DEBUG(Stsd, "Done, numberEncryptedEntries=%" PRIu32 ", mSampleDescriptions.Length=%zu", numberEncryptedEntries, mSampleDescriptions.Length()); } void MoofParser::ParseEncrypted(Box& aBox) { LOG_DEBUG(Moof, "Starting."); for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { // Some MP4 files have been found to have multiple sinf boxes in the same // enc* box. This does not match spec anyway, so just choose the first // one that parses properly. if (box.IsType("sinf")) { mSinf = Sinf(box); if (mSinf.IsValid()) { break; } } } LOG_DEBUG(Moof, "Done."); } class CtsComparator { public: bool Equals(Sample* const aA, Sample* const aB) const { return aA->mCompositionRange.start == aB->mCompositionRange.start; } bool LessThan(Sample* const aA, Sample* const aB) const { return aA->mCompositionRange.start < aB->mCompositionRange.start; } }; Moof::Moof(Box& aBox, const TrackParseMode& aTrackParseMode, Trex& aTrex, Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, Sinf& aSinf, uint64_t* aDecodeTime, bool aIsAudio, nsTArray& aTracksEndCts) : mRange(aBox.Range()), mTfhd(aTrex), mMaxRoundingError(35000) { LOG_DEBUG( Moof, "Starting, aTrackParseMode=%s, track#=%" PRIu32 " (ignore if multitrack).", aTrackParseMode.is() ? "multitrack" : "single track", aTrackParseMode.is() ? 0 : aTrackParseMode.as()); MOZ_ASSERT(aTrackParseMode.is() || aTrex.mTrackId == aTrackParseMode.as(), "If not parsing all tracks, aTrex should have the same track id " "as the track being parsed."); nsTArray psshBoxes; for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("traf")) { ParseTraf(box, aTrackParseMode, aTrex, aMvhd, aMdhd, aEdts, aSinf, aDecodeTime, aIsAudio); } if (box.IsType("pssh")) { psshBoxes.AppendElement(box); } } // The EME spec requires that PSSH boxes which are contiguous in the // file are dispatched to the media element in a single "encrypted" event. // So append contiguous boxes here. for (size_t i = 0; i < psshBoxes.Length(); ++i) { Box box = psshBoxes[i]; if (i == 0 || box.Offset() != psshBoxes[i - 1].NextOffset()) { mPsshes.AppendElement(); } nsTArray& pssh = mPsshes.LastElement(); pssh.AppendElements(std::move(box.ReadCompleteBox())); } if (IsValid()) { if (mIndex.Length()) { // Ensure the samples are contiguous with no gaps. nsTArray ctsOrder; for (auto& sample : mIndex) { ctsOrder.AppendElement(&sample); } ctsOrder.Sort(CtsComparator()); for (size_t i = 1; i < ctsOrder.Length(); i++) { ctsOrder[i - 1]->mCompositionRange.end = ctsOrder[i]->mCompositionRange.start; } // Ensure that there are no gaps between the first sample in this // Moof and the preceeding Moof. if (!ctsOrder.IsEmpty()) { bool found = false; // Track ID of the track we're parsing. const uint32_t trackId = aTrex.mTrackId; // Find the previous CTS end time of Moof preceeding the Moofs we just // parsed, for the track we're parsing. for (auto& prevCts : aTracksEndCts) { if (prevCts.mTrackId == trackId) { // We have previously parsed a Moof for this track. Smooth the gap // between samples for this track across the Moof bounary. if (ctsOrder[0]->mCompositionRange.start > prevCts.mCtsEndTime && ctsOrder[0]->mCompositionRange.start - prevCts.mCtsEndTime <= CROSS_MOOF_CTS_MERGE_THRESHOLD) { ctsOrder[0]->mCompositionRange.start = prevCts.mCtsEndTime; } prevCts.mCtsEndTime = ctsOrder.LastElement()->mCompositionRange.end; found = true; break; } } if (!found) { // We've not parsed a Moof for this track yet. Save its CTS end // time for the next Moof we parse. aTracksEndCts.AppendElement(TrackEndCts( trackId, ctsOrder.LastElement()->mCompositionRange.end)); } } // In MP4, the duration of a sample is defined as the delta between two // decode timestamps. The operation above has updated the duration of each // sample as a Sample's duration is mCompositionRange.end - // mCompositionRange.start MSE's TrackBuffersManager expects dts that // increased by the sample's duration, so we rewrite the dts accordingly. int64_t presentationDuration = ctsOrder.LastElement()->mCompositionRange.end - ctsOrder[0]->mCompositionRange.start; auto decodeOffset = aMdhd.ToMicroseconds((int64_t)*aDecodeTime - aEdts.mMediaStart); auto offsetOffset = aMvhd.ToMicroseconds(aEdts.mEmptyOffset); int64_t endDecodeTime = (decodeOffset.isOk() && offsetOffset.isOk()) ? decodeOffset.unwrap() + offsetOffset.unwrap() : 0; int64_t decodeDuration = endDecodeTime - mIndex[0].mDecodeTime; double adjust = !!presentationDuration ? (double)decodeDuration / presentationDuration : 0; int64_t dtsOffset = mIndex[0].mDecodeTime; int64_t compositionDuration = 0; // Adjust the dts, ensuring that the new adjusted dts will never be // greater than decodeTime (the next moof's decode start time). for (auto& sample : mIndex) { sample.mDecodeTime = dtsOffset + int64_t(compositionDuration * adjust); compositionDuration += sample.mCompositionRange.Length(); } mTimeRange = MP4Interval( ctsOrder[0]->mCompositionRange.start, ctsOrder.LastElement()->mCompositionRange.end); } ProcessCencAuxInfo(aSinf.mDefaultEncryptionType); } LOG_DEBUG(Moof, "Done."); } bool Moof::GetAuxInfo(AtomType aType, FallibleTArray* aByteRanges) { LOG_DEBUG(Moof, "Starting."); aByteRanges->Clear(); Saiz* saiz = nullptr; for (int i = 0;; i++) { if (i == mSaizs.Length()) { LOG_DEBUG(Moof, "Could not find saiz matching aType. Returning false."); return false; } if (mSaizs[i].mAuxInfoType == aType) { saiz = &mSaizs[i]; break; } } Saio* saio = nullptr; for (int i = 0;; i++) { if (i == mSaios.Length()) { LOG_DEBUG(Moof, "Could not find saio matching aType. Returning false."); return false; } if (mSaios[i].mAuxInfoType == aType) { saio = &mSaios[i]; break; } } if (saio->mOffsets.Length() == 1) { if (!aByteRanges->SetCapacity(saiz->mSampleInfoSize.Length(), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return false; } uint64_t offset = mRange.mStart + saio->mOffsets[0]; for (size_t i = 0; i < saiz->mSampleInfoSize.Length(); i++) { if (!aByteRanges->AppendElement( MediaByteRange(offset, offset + saiz->mSampleInfoSize[i]), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return false; } offset += saiz->mSampleInfoSize[i]; } LOG_DEBUG( Moof, "Saio has 1 entry. aByteRanges populated accordingly. Returning true."); return true; } if (saio->mOffsets.Length() == saiz->mSampleInfoSize.Length()) { if (!aByteRanges->SetCapacity(saiz->mSampleInfoSize.Length(), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return false; } for (size_t i = 0; i < saio->mOffsets.Length(); i++) { uint64_t offset = mRange.mStart + saio->mOffsets[i]; if (!aByteRanges->AppendElement( MediaByteRange(offset, offset + saiz->mSampleInfoSize[i]), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return false; } } LOG_DEBUG( Moof, "Saio and saiz have same number of entries. aByteRanges populated " "accordingly. Returning true."); return true; } LOG_DEBUG(Moof, "Moof::GetAuxInfo could not find any Aux info, returning false."); return false; } bool Moof::ProcessCencAuxInfo(AtomType aScheme) { LOG_DEBUG(Moof, "Starting."); FallibleTArray cencRanges; if (!GetAuxInfo(aScheme, &cencRanges) || cencRanges.Length() != mIndex.Length()) { LOG_DEBUG(Moof, "Couldn't find cenc aux info."); return false; } for (int i = 0; i < cencRanges.Length(); i++) { mIndex[i].mCencRange = cencRanges[i]; } LOG_DEBUG(Moof, "Found cenc aux info and stored on index."); return true; } void Moof::ParseTraf(Box& aBox, const TrackParseMode& aTrackParseMode, Trex& aTrex, Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, Sinf& aSinf, uint64_t* aDecodeTime, bool aIsAudio) { LOG_DEBUG( Traf, "Starting, aTrackParseMode=%s, track#=%" PRIu32 " (ignore if multitrack).", aTrackParseMode.is() ? "multitrack" : "single track", aTrackParseMode.is() ? 0 : aTrackParseMode.as()); MOZ_ASSERT(aDecodeTime); MOZ_ASSERT(aTrackParseMode.is() || aTrex.mTrackId == aTrackParseMode.as(), "If not parsing all tracks, aTrex should have the same track id " "as the track being parsed."); Tfdt tfdt; for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("tfhd")) { mTfhd = Tfhd(box, aTrex); } else if (aTrackParseMode.is() || mTfhd.mTrackId == aTrackParseMode.as()) { if (box.IsType("tfdt")) { tfdt = Tfdt(box); } else if (box.IsType("sgpd")) { Sgpd sgpd(box); if (sgpd.IsValid() && sgpd.mGroupingType == "seig") { mFragmentSampleEncryptionInfoEntries.Clear(); if (!mFragmentSampleEncryptionInfoEntries.AppendElements( sgpd.mEntries, mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return; } } } else if (box.IsType("sbgp")) { Sbgp sbgp(box); if (sbgp.IsValid() && sbgp.mGroupingType == "seig") { mFragmentSampleToGroupEntries.Clear(); if (!mFragmentSampleToGroupEntries.AppendElements( sbgp.mEntries, mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return; } } } else if (box.IsType("saiz")) { if (!mSaizs.AppendElement(Saiz(box, aSinf.mDefaultEncryptionType), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return; } } else if (box.IsType("saio")) { if (!mSaios.AppendElement(Saio(box, aSinf.mDefaultEncryptionType), mozilla::fallible)) { LOG_ERROR(Moof, "OOM"); return; } } } } if (aTrackParseMode.is() && mTfhd.mTrackId != aTrackParseMode.as()) { LOG_DEBUG(Traf, "Early return as not multitrack parser and track id didn't match " "mTfhd.mTrackId=%" PRIu32, mTfhd.mTrackId); return; } // Now search for TRUN boxes. uint64_t decodeTime = tfdt.IsValid() ? tfdt.mBaseMediaDecodeTime : *aDecodeTime; for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) { if (box.IsType("trun")) { if (ParseTrun(box, aMvhd, aMdhd, aEdts, &decodeTime, aIsAudio).isOk()) { mValid = true; } else { LOG_WARN(Moof, "ParseTrun failed"); mValid = false; break; } } } *aDecodeTime = decodeTime; LOG_DEBUG(Traf, "Done, setting aDecodeTime=%." PRIu64 ".", decodeTime); } void Moof::FixRounding(const Moof& aMoof) { Microseconds gap = aMoof.mTimeRange.start - mTimeRange.end; if (gap > 0 && gap <= mMaxRoundingError) { mTimeRange.end = aMoof.mTimeRange.start; } } Result Moof::ParseTrun(Box& aBox, Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, uint64_t* aDecodeTime, bool aIsAudio) { LOG_DEBUG(Trun, "Starting."); if (!mTfhd.IsValid() || !aMvhd.IsValid() || !aMdhd.IsValid() || !aEdts.IsValid()) { LOG_WARN( Moof, "Invalid dependencies: mTfhd(%d) aMvhd(%d) aMdhd(%d) aEdts(%d)", mTfhd.IsValid(), aMvhd.IsValid(), aMdhd.IsValid(), !aEdts.IsValid()); return Err(NS_ERROR_FAILURE); } BoxReader reader(aBox); if (!reader->CanReadType()) { LOG_WARN(Moof, "Incomplete Box (missing flags)"); return Err(NS_ERROR_FAILURE); } uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); if (!reader->CanReadType()) { LOG_WARN(Moof, "Incomplete Box (missing sampleCount)"); return Err(NS_ERROR_FAILURE); } uint32_t sampleCount; MOZ_TRY_VAR(sampleCount, reader->ReadU32()); if (sampleCount == 0) { LOG_DEBUG(Trun, "Trun with no samples, returning."); return Ok(); } uint64_t offset = mTfhd.mBaseDataOffset; if (flags & 0x01) { uint32_t tmp; MOZ_TRY_VAR(tmp, reader->ReadU32()); offset += tmp; } uint32_t firstSampleFlags = mTfhd.mDefaultSampleFlags; if (flags & 0x04) { MOZ_TRY_VAR(firstSampleFlags, reader->ReadU32()); } uint64_t decodeTime = *aDecodeTime; nsTArray> timeRanges; if (!mIndex.SetCapacity(sampleCount, fallible)) { LOG_ERROR(Moof, "Out of Memory"); return Err(NS_ERROR_FAILURE); } for (size_t i = 0; i < sampleCount; i++) { uint32_t sampleDuration = mTfhd.mDefaultSampleDuration; if (flags & 0x100) { MOZ_TRY_VAR(sampleDuration, reader->ReadU32()); } uint32_t sampleSize = mTfhd.mDefaultSampleSize; if (flags & 0x200) { MOZ_TRY_VAR(sampleSize, reader->ReadU32()); } uint32_t sampleFlags = i ? mTfhd.mDefaultSampleFlags : firstSampleFlags; if (flags & 0x400) { MOZ_TRY_VAR(sampleFlags, reader->ReadU32()); } int32_t ctsOffset = 0; if (flags & 0x800) { MOZ_TRY_VAR(ctsOffset, reader->Read32()); } if (sampleSize) { Sample sample; sample.mByteRange = MediaByteRange(offset, offset + sampleSize); offset += sampleSize; Microseconds decodeOffset, emptyOffset, startCts, endCts; MOZ_TRY_VAR(decodeOffset, aMdhd.ToMicroseconds((int64_t)decodeTime - aEdts.mMediaStart)); MOZ_TRY_VAR(emptyOffset, aMvhd.ToMicroseconds(aEdts.mEmptyOffset)); sample.mDecodeTime = decodeOffset + emptyOffset; MOZ_TRY_VAR(startCts, aMdhd.ToMicroseconds((int64_t)decodeTime + ctsOffset - aEdts.mMediaStart)); MOZ_TRY_VAR(endCts, aMdhd.ToMicroseconds((int64_t)decodeTime + ctsOffset + sampleDuration - aEdts.mMediaStart)); sample.mCompositionRange = MP4Interval( startCts + emptyOffset, endCts + emptyOffset); // Sometimes audio streams don't properly mark their samples as keyframes, // because every audio sample is a keyframe. sample.mSync = !(sampleFlags & 0x1010000) || aIsAudio; // FIXME: Make this infallible after bug 968520 is done. MOZ_ALWAYS_TRUE(mIndex.AppendElement(sample, fallible)); mMdatRange = mMdatRange.Span(sample.mByteRange); } decodeTime += sampleDuration; } Microseconds roundTime; MOZ_TRY_VAR(roundTime, aMdhd.ToMicroseconds(sampleCount)); mMaxRoundingError += roundTime; *aDecodeTime = decodeTime; LOG_DEBUG(Trun, "Done."); return Ok(); } Tkhd::Tkhd(Box& aBox) : mTrackId(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Tkhd, "Parse failed"); } } Result Tkhd::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); uint8_t version = flags >> 24; if (version == 0) { uint32_t creationTime, modificationTime, reserved, duration; MOZ_TRY_VAR(creationTime, reader->ReadU32()); MOZ_TRY_VAR(modificationTime, reader->ReadU32()); MOZ_TRY_VAR(mTrackId, reader->ReadU32()); MOZ_TRY_VAR(reserved, reader->ReadU32()); MOZ_TRY_VAR(duration, reader->ReadU32()); NS_ASSERTION(!reserved, "reserved should be 0"); mCreationTime = creationTime; mModificationTime = modificationTime; mDuration = duration; } else if (version == 1) { uint32_t reserved; MOZ_TRY_VAR(mCreationTime, reader->ReadU64()); MOZ_TRY_VAR(mModificationTime, reader->ReadU64()); MOZ_TRY_VAR(mTrackId, reader->ReadU32()); MOZ_TRY_VAR(reserved, reader->ReadU32()); NS_ASSERTION(!reserved, "reserved should be 0"); MOZ_TRY_VAR(mDuration, reader->ReadU64()); } return Ok(); } Mvhd::Mvhd(Box& aBox) : mCreationTime(0), mModificationTime(0), mTimescale(0), mDuration(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Mvhd, "Parse failed"); } } Result Mvhd::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); uint8_t version = flags >> 24; if (version == 0) { uint32_t creationTime, modificationTime, duration; MOZ_TRY_VAR(creationTime, reader->ReadU32()); MOZ_TRY_VAR(modificationTime, reader->ReadU32()); MOZ_TRY_VAR(mTimescale, reader->ReadU32()); MOZ_TRY_VAR(duration, reader->ReadU32()); mCreationTime = creationTime; mModificationTime = modificationTime; mDuration = duration; } else if (version == 1) { MOZ_TRY_VAR(mCreationTime, reader->ReadU64()); MOZ_TRY_VAR(mModificationTime, reader->ReadU64()); MOZ_TRY_VAR(mTimescale, reader->ReadU32()); MOZ_TRY_VAR(mDuration, reader->ReadU64()); } else { return Err(NS_ERROR_FAILURE); } return Ok(); } Mdhd::Mdhd(Box& aBox) : Mvhd(aBox) {} Trex::Trex(Box& aBox) : mFlags(0), mTrackId(0), mDefaultSampleDescriptionIndex(0), mDefaultSampleDuration(0), mDefaultSampleSize(0), mDefaultSampleFlags(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Trex, "Parse failed"); } } Result Trex::Parse(Box& aBox) { BoxReader reader(aBox); MOZ_TRY_VAR(mFlags, reader->ReadU32()); MOZ_TRY_VAR(mTrackId, reader->ReadU32()); MOZ_TRY_VAR(mDefaultSampleDescriptionIndex, reader->ReadU32()); MOZ_TRY_VAR(mDefaultSampleDuration, reader->ReadU32()); MOZ_TRY_VAR(mDefaultSampleSize, reader->ReadU32()); MOZ_TRY_VAR(mDefaultSampleFlags, reader->ReadU32()); return Ok(); } Tfhd::Tfhd(Box& aBox, Trex& aTrex) : Trex(aTrex), mBaseDataOffset(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Tfhd, "Parse failed"); } } Result Tfhd::Parse(Box& aBox) { MOZ_ASSERT(aBox.IsType("tfhd")); MOZ_ASSERT(aBox.Parent()->IsType("traf")); MOZ_ASSERT(aBox.Parent()->Parent()->IsType("moof")); BoxReader reader(aBox); MOZ_TRY_VAR(mFlags, reader->ReadU32()); MOZ_TRY_VAR(mTrackId, reader->ReadU32()); mBaseDataOffset = aBox.Parent()->Parent()->Offset(); if (mFlags & 0x01) { MOZ_TRY_VAR(mBaseDataOffset, reader->ReadU64()); } if (mFlags & 0x02) { MOZ_TRY_VAR(mDefaultSampleDescriptionIndex, reader->ReadU32()); } if (mFlags & 0x08) { MOZ_TRY_VAR(mDefaultSampleDuration, reader->ReadU32()); } if (mFlags & 0x10) { MOZ_TRY_VAR(mDefaultSampleSize, reader->ReadU32()); } if (mFlags & 0x20) { MOZ_TRY_VAR(mDefaultSampleFlags, reader->ReadU32()); } return Ok(); } Tfdt::Tfdt(Box& aBox) : mBaseMediaDecodeTime(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Tfdt, "Parse failed"); } } Result Tfdt::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); uint8_t version = flags >> 24; if (version == 0) { uint32_t tmp; MOZ_TRY_VAR(tmp, reader->ReadU32()); mBaseMediaDecodeTime = tmp; } else if (version == 1) { MOZ_TRY_VAR(mBaseMediaDecodeTime, reader->ReadU64()); } return Ok(); } Edts::Edts(Box& aBox) : mMediaStart(0), mEmptyOffset(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Edts, "Parse failed"); } } Result Edts::Parse(Box& aBox) { Box child = aBox.FirstChild(); if (!child.IsType("elst")) { return Err(NS_ERROR_FAILURE); } BoxReader reader(child); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); uint8_t version = flags >> 24; bool emptyEntry = false; uint32_t entryCount; MOZ_TRY_VAR(entryCount, reader->ReadU32()); for (uint32_t i = 0; i < entryCount; i++) { uint64_t segment_duration; int64_t media_time; if (version == 1) { MOZ_TRY_VAR(segment_duration, reader->ReadU64()); MOZ_TRY_VAR(media_time, reader->Read64()); } else { uint32_t tmp; MOZ_TRY_VAR(tmp, reader->ReadU32()); segment_duration = tmp; int32_t tmp2; MOZ_TRY_VAR(tmp2, reader->Read32()); media_time = tmp2; } if (media_time == -1 && i) { LOG_WARN(Edts, "Multiple empty edit, not handled"); } else if (media_time == -1) { mEmptyOffset = segment_duration; emptyEntry = true; } else if (i > 1 || (i > 0 && !emptyEntry)) { LOG_WARN(Edts, "More than one edit entry, not handled. A/V sync will be wrong"); break; } else { mMediaStart = media_time; } MOZ_TRY(reader->ReadU32()); // media_rate_integer and media_rate_fraction } return Ok(); } Saiz::Saiz(Box& aBox, AtomType aDefaultType) : mAuxInfoType(aDefaultType), mAuxInfoTypeParameter(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Saiz, "Parse failed"); } } Result Saiz::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); if (flags & 1) { MOZ_TRY_VAR(mAuxInfoType, reader->ReadU32()); MOZ_TRY_VAR(mAuxInfoTypeParameter, reader->ReadU32()); } uint8_t defaultSampleInfoSize; MOZ_TRY_VAR(defaultSampleInfoSize, reader->ReadU8()); uint32_t count; MOZ_TRY_VAR(count, reader->ReadU32()); if (defaultSampleInfoSize) { if (!mSampleInfoSize.SetLength(count, fallible)) { LOG_ERROR(Saiz, "OOM"); return Err(NS_ERROR_FAILURE); } memset(mSampleInfoSize.Elements(), defaultSampleInfoSize, mSampleInfoSize.Length()); } else { if (!reader->ReadArray(mSampleInfoSize, count)) { LOG_WARN(Saiz, "Incomplete Box (OOM or missing count:%u)", count); return Err(NS_ERROR_FAILURE); } } return Ok(); } Saio::Saio(Box& aBox, AtomType aDefaultType) : mAuxInfoType(aDefaultType), mAuxInfoTypeParameter(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Saio, "Parse failed"); } } Result Saio::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); uint8_t version = flags >> 24; if (flags & 1) { MOZ_TRY_VAR(mAuxInfoType, reader->ReadU32()); MOZ_TRY_VAR(mAuxInfoTypeParameter, reader->ReadU32()); } size_t count; MOZ_TRY_VAR(count, reader->ReadU32()); if (!mOffsets.SetCapacity(count, fallible)) { LOG_ERROR(Saiz, "OOM"); return Err(NS_ERROR_FAILURE); } if (version == 0) { uint32_t offset; for (size_t i = 0; i < count; i++) { MOZ_TRY_VAR(offset, reader->ReadU32()); MOZ_ALWAYS_TRUE(mOffsets.AppendElement(offset, fallible)); } } else { uint64_t offset; for (size_t i = 0; i < count; i++) { MOZ_TRY_VAR(offset, reader->ReadU64()); MOZ_ALWAYS_TRUE(mOffsets.AppendElement(offset, fallible)); } } return Ok(); } Sbgp::Sbgp(Box& aBox) : mGroupingTypeParam(0) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Sbgp, "Parse failed"); } } Result Sbgp::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); const uint8_t version = flags >> 24; uint32_t type; MOZ_TRY_VAR(type, reader->ReadU32()); mGroupingType = type; if (version == 1) { MOZ_TRY_VAR(mGroupingTypeParam, reader->ReadU32()); } uint32_t count; MOZ_TRY_VAR(count, reader->ReadU32()); for (uint32_t i = 0; i < count; i++) { uint32_t sampleCount; MOZ_TRY_VAR(sampleCount, reader->ReadU32()); uint32_t groupDescriptionIndex; MOZ_TRY_VAR(groupDescriptionIndex, reader->ReadU32()); SampleToGroupEntry entry(sampleCount, groupDescriptionIndex); if (!mEntries.AppendElement(entry, mozilla::fallible)) { LOG_ERROR(Sbgp, "OOM"); return Err(NS_ERROR_FAILURE); } } return Ok(); } Sgpd::Sgpd(Box& aBox) { mValid = Parse(aBox).isOk(); if (!mValid) { LOG_WARN(Sgpd, "Parse failed"); } } Result Sgpd::Parse(Box& aBox) { BoxReader reader(aBox); uint32_t flags; MOZ_TRY_VAR(flags, reader->ReadU32()); const uint8_t version = flags >> 24; flags = flags & 0xffffff; uint32_t type; MOZ_TRY_VAR(type, reader->ReadU32()); mGroupingType = type; const uint32_t entrySize = sizeof(uint32_t) + kKeyIdSize; uint32_t defaultLength = 0; if (version == 1) { MOZ_TRY_VAR(defaultLength, reader->ReadU32()); if (defaultLength < entrySize && defaultLength != 0) { return Err(NS_ERROR_FAILURE); } } uint32_t count; MOZ_TRY_VAR(count, reader->ReadU32()); for (uint32_t i = 0; i < count; ++i) { if (version == 1 && defaultLength == 0) { uint32_t descriptionLength; MOZ_TRY_VAR(descriptionLength, reader->ReadU32()); if (descriptionLength < entrySize) { return Err(NS_ERROR_FAILURE); } } CencSampleEncryptionInfoEntry entry; bool valid = entry.Init(reader).isOk(); if (!valid) { return Err(NS_ERROR_FAILURE); } if (!mEntries.AppendElement(entry, mozilla::fallible)) { LOG_ERROR(Sgpd, "OOM"); return Err(NS_ERROR_FAILURE); } } return Ok(); } Result CencSampleEncryptionInfoEntry::Init(BoxReader& aReader) { // Skip a reserved byte. MOZ_TRY(aReader->ReadU8()); uint8_t pattern; MOZ_TRY_VAR(pattern, aReader->ReadU8()); mCryptByteBlock = pattern >> 4; mSkipByteBlock = pattern & 0x0f; uint8_t isEncrypted; MOZ_TRY_VAR(isEncrypted, aReader->ReadU8()); mIsEncrypted = isEncrypted != 0; MOZ_TRY_VAR(mIVSize, aReader->ReadU8()); // Read the key id. if (!mKeyId.SetLength(kKeyIdSize, fallible)) { LOG_ERROR(CencSampleEncryptionInfoEntry, "OOM"); return Err(NS_ERROR_FAILURE); } for (uint32_t i = 0; i < kKeyIdSize; ++i) { MOZ_TRY_VAR(mKeyId.ElementAt(i), aReader->ReadU8()); } if (mIsEncrypted) { if (mIVSize != 8 && mIVSize != 16) { return Err(NS_ERROR_FAILURE); } } else if (mIVSize != 0) { // Protected content with 0 sized IV indicates a constant IV is present. // This is used for the cbcs scheme. uint8_t constantIVSize; MOZ_TRY_VAR(constantIVSize, aReader->ReadU8()); if (constantIVSize != 8 && constantIVSize != 16) { LOG_WARN(CencSampleEncryptionInfoEntry, "Unexpected constantIVSize: %" PRIu8, constantIVSize); return Err(NS_ERROR_FAILURE); } if (!mConsantIV.SetLength(constantIVSize, mozilla::fallible)) { LOG_ERROR(CencSampleEncryptionInfoEntry, "OOM"); return Err(NS_ERROR_FAILURE); } for (uint32_t i = 0; i < constantIVSize; ++i) { MOZ_TRY_VAR(mConsantIV.ElementAt(i), aReader->ReadU8()); } } return Ok(); } } // namespace mozilla #undef LOG_DEBUG #undef LOG_WARN #undef LOG_ERROR