/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sw=2 sts=2 et cindent: */ /* 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 "WebMBufferedParser.h" #include #include "mozilla/CheckedInt.h" #include "nsAlgorithm.h" #include "nsThreadUtils.h" extern mozilla::LazyLogModule gMediaDemuxerLog; #define WEBM_DEBUG(arg, ...) \ MOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, \ ("WebMBufferedParser(%p)::%s: " arg, this, __func__, ##__VA_ARGS__)) namespace mozilla { static uint32_t VIntLength(unsigned char aFirstByte, uint32_t* aMask) { uint32_t count = 1; uint32_t mask = 1 << 7; while (count < 8) { if ((aFirstByte & mask) != 0) { break; } mask >>= 1; count += 1; } if (aMask) { *aMask = mask; } NS_ASSERTION(count >= 1 && count <= 8, "Insane VInt length."); return count; } constexpr uint8_t EBML_MAX_ID_LENGTH_DEFAULT = 4; constexpr uint8_t EBML_MAX_SIZE_LENGTH_DEFAULT = 8; WebMBufferedParser::WebMBufferedParser(int64_t aOffset) : mStartOffset(aOffset), mCurrentOffset(aOffset), mInitEndOffset(-1), mBlockEndOffset(-1), mState(READ_ELEMENT_ID), mNextState(READ_ELEMENT_ID), mVIntRaw(false), mLastInitStartOffset(-1), mLastInitSize(0), mEBMLMaxIdLength(EBML_MAX_ID_LENGTH_DEFAULT), mEBMLMaxSizeLength(EBML_MAX_SIZE_LENGTH_DEFAULT), mClusterSyncPos(0), mVIntLeft(0), mBlockSize(0), mClusterTimecode(0), mClusterOffset(-1), mClusterEndOffset(-1), mBlockOffset(0), mBlockTimecode(0), mBlockTimecodeLength(0), mSkipBytes(0), mTimecodeScale(1000000), mGotTimecodeScale(false), mGotClusterTimecode(false) { if (mStartOffset != 0) { mState = FIND_CLUSTER_SYNC; } } MediaResult WebMBufferedParser::Append(const unsigned char* aBuffer, uint32_t aLength, nsTArray& aMapping) { static const uint32_t EBML_ID = 0x1a45dfa3; static const uint32_t SEGMENT_ID = 0x18538067; static const uint32_t SEGINFO_ID = 0x1549a966; static const uint32_t TRACKS_ID = 0x1654AE6B; static const uint32_t CLUSTER_ID = 0x1f43b675; static const uint32_t TIMECODESCALE_ID = 0x2ad7b1; static const unsigned char TIMECODE_ID = 0xe7; static const unsigned char BLOCKGROUP_ID = 0xa0; static const unsigned char BLOCK_ID = 0xa1; static const unsigned char SIMPLEBLOCK_ID = 0xa3; static const uint16_t EBML_MAX_ID_LENGTH_ID = 0x42f2; static const uint16_t EBML_MAX_SIZE_LENGTH_ID = 0x42f3; static const uint32_t BLOCK_TIMECODE_LENGTH = 2; static const unsigned char CLUSTER_SYNC_ID[] = {0x1f, 0x43, 0xb6, 0x75}; const unsigned char* p = aBuffer; // Parse each byte in aBuffer one-by-one, producing timecodes and updating // aMapping as we go. Parser pauses at end of stream (which may be at any // point within the parse) and resumes parsing the next time Append is // called with new data. while (p < aBuffer + aLength) { switch (mState) { case READ_ELEMENT_ID: mVIntRaw = true; mState = READ_VINT; mNextState = READ_ELEMENT_SIZE; break; case READ_ELEMENT_SIZE: if (mVInt.mLength > mEBMLMaxIdLength) { nsPrintfCString detail("Invalid element id of length %" PRIu64, mVInt.mLength); WEBM_DEBUG("%s", detail.get()); return MediaResult(NS_ERROR_FAILURE, detail); } mVIntRaw = false; mElement.mID = mVInt; mState = READ_VINT; mNextState = PARSE_ELEMENT; break; case FIND_CLUSTER_SYNC: if (*p++ == CLUSTER_SYNC_ID[mClusterSyncPos]) { mClusterSyncPos += 1; } else { mClusterSyncPos = 0; } if (mClusterSyncPos == sizeof(CLUSTER_SYNC_ID)) { mVInt.mValue = CLUSTER_ID; mVInt.mLength = sizeof(CLUSTER_SYNC_ID); mState = READ_ELEMENT_SIZE; } break; case PARSE_ELEMENT: if (mVInt.mLength > mEBMLMaxSizeLength) { nsPrintfCString detail("Invalid element size of length %" PRIu64, mVInt.mLength); WEBM_DEBUG("%s", detail.get()); return MediaResult(NS_ERROR_FAILURE, detail); } mElement.mSize = mVInt; switch (mElement.mID.mValue) { case SEGMENT_ID: mState = READ_ELEMENT_ID; break; case SEGINFO_ID: mGotTimecodeScale = true; mState = READ_ELEMENT_ID; break; case TIMECODE_ID: mVInt = VInt(); mVIntLeft = mElement.mSize.mValue; mState = READ_VINT_REST; mNextState = READ_CLUSTER_TIMECODE; break; case TIMECODESCALE_ID: mVInt = VInt(); mVIntLeft = mElement.mSize.mValue; mState = READ_VINT_REST; mNextState = READ_TIMECODESCALE; break; case CLUSTER_ID: mClusterOffset = mCurrentOffset + (p - aBuffer) - (mElement.mID.mLength + mElement.mSize.mLength); // Handle "unknown" length; if (mElement.mSize.mValue + 1 != uint64_t(1) << (mElement.mSize.mLength * 7)) { mClusterEndOffset = mClusterOffset + mElement.mID.mLength + mElement.mSize.mLength + mElement.mSize.mValue; } else { mClusterEndOffset = -1; } mGotClusterTimecode = false; mState = READ_ELEMENT_ID; break; case BLOCKGROUP_ID: mState = READ_ELEMENT_ID; break; case SIMPLEBLOCK_ID: /* FALLTHROUGH */ case BLOCK_ID: if (!mGotClusterTimecode) { WEBM_DEBUG( "The Timecode element must appear before any Block or " "SimpleBlock elements in a Cluster"); return MediaResult( NS_ERROR_FAILURE, "The Timecode element must appear before any Block or " "SimpleBlock elements in a Cluster"); } mBlockSize = mElement.mSize.mValue; mBlockTimecode = 0; mBlockTimecodeLength = BLOCK_TIMECODE_LENGTH; mBlockOffset = mCurrentOffset + (p - aBuffer) - (mElement.mID.mLength + mElement.mSize.mLength); mState = READ_VINT; mNextState = READ_BLOCK_TIMECODE; break; case TRACKS_ID: mSkipBytes = mElement.mSize.mValue; mState = CHECK_INIT_FOUND; break; case EBML_MAX_ID_LENGTH_ID: case EBML_MAX_SIZE_LENGTH_ID: if (int64_t currentOffset = mCurrentOffset + (p - aBuffer); currentOffset < mLastInitStartOffset || currentOffset >= mLastInitStartOffset + mLastInitSize) { nsPrintfCString str("Unexpected %s outside init segment", mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID ? "EBMLMaxIdLength" : "EBMLMaxSizeLength"); WEBM_DEBUG("%s", str.get()); return MediaResult(NS_ERROR_FAILURE, str); } if (mElement.mSize.mValue > 8) { // https://www.rfc-editor.org/rfc/rfc8794.html (EBML): // An Unsigned Integer Element MUST declare a length from zero // to eight octets. nsPrintfCString str("Bad length of %s size", mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID ? "EBMLMaxIdLength" : "EBMLMaxSizeLength"); WEBM_DEBUG("%s", str.get()); return MediaResult(NS_ERROR_FAILURE, str); } mVInt = VInt(); mVIntLeft = mElement.mSize.mValue; mState = READ_VINT_REST; mNextState = mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID ? READ_EBML_MAX_ID_LENGTH : READ_EBML_MAX_SIZE_LENGTH; break; case EBML_ID: mLastInitStartOffset = mCurrentOffset + (p - aBuffer) - (mElement.mID.mLength + mElement.mSize.mLength); mLastInitSize = mElement.mSize.mValue; mEBMLMaxIdLength = EBML_MAX_ID_LENGTH_DEFAULT; mEBMLMaxSizeLength = EBML_MAX_SIZE_LENGTH_DEFAULT; mState = READ_ELEMENT_ID; break; default: mSkipBytes = mElement.mSize.mValue; mState = SKIP_DATA; mNextState = READ_ELEMENT_ID; break; } break; case READ_VINT: { unsigned char c = *p++; uint32_t mask; mVInt.mLength = VIntLength(c, &mask); mVIntLeft = mVInt.mLength - 1; mVInt.mValue = mVIntRaw ? c : c & ~mask; mState = READ_VINT_REST; break; } case READ_VINT_REST: if (mVIntLeft) { mVInt.mValue <<= 8; mVInt.mValue |= *p++; mVIntLeft -= 1; } else { mState = mNextState; } break; case READ_TIMECODESCALE: if (!mGotTimecodeScale) { WEBM_DEBUG("Should get the SegmentInfo first"); return MediaResult(NS_ERROR_FAILURE, "TimecodeScale appeared before SegmentInfo"); } mTimecodeScale = mVInt.mValue; mState = READ_ELEMENT_ID; break; case READ_CLUSTER_TIMECODE: mClusterTimecode = mVInt.mValue; mGotClusterTimecode = true; mState = READ_ELEMENT_ID; break; case READ_BLOCK_TIMECODE: if (mBlockTimecodeLength) { mBlockTimecode <<= 8; mBlockTimecode |= *p++; mBlockTimecodeLength -= 1; } else { // It's possible we've parsed this data before, so avoid inserting // duplicate WebMTimeDataOffset entries. { int64_t endOffset = mBlockOffset + mBlockSize + mElement.mID.mLength + mElement.mSize.mLength; uint32_t idx = aMapping.IndexOfFirstElementGt(endOffset); if (idx == 0 || aMapping[idx - 1] != endOffset) { // Don't insert invalid negative timecodes. if (mBlockTimecode >= 0 || mClusterTimecode >= uint16_t(abs(mBlockTimecode))) { if (!mGotTimecodeScale) { WEBM_DEBUG("Should get the TimecodeScale first"); return MediaResult(NS_ERROR_FAILURE, "Timecode appeared before SegmentInfo"); } uint64_t absTimecode = mClusterTimecode + mBlockTimecode; absTimecode *= mTimecodeScale; // Avoid creating an entry if the timecode is out of order // (invalid according to the WebM specification) so that // ordering invariants of aMapping are not violated. if (idx == 0 || aMapping[idx - 1].mTimecode <= absTimecode || (idx + 1 < aMapping.Length() && aMapping[idx + 1].mTimecode >= absTimecode)) { WebMTimeDataOffset entry(endOffset, absTimecode, mLastInitStartOffset, mClusterOffset, mClusterEndOffset); aMapping.InsertElementAt(idx, entry); } else { WEBM_DEBUG("Out of order timecode %" PRIu64 " in Cluster at %" PRId64 " ignored", absTimecode, mClusterOffset); } } } } // Skip rest of block header and the block's payload. mBlockSize -= mVInt.mLength; mBlockSize -= BLOCK_TIMECODE_LENGTH; mSkipBytes = uint32_t(mBlockSize); mState = SKIP_DATA; mNextState = READ_ELEMENT_ID; } break; case READ_EBML_MAX_ID_LENGTH: if (mElement.mSize.mLength == 0) { // https://www.rfc-editor.org/rfc/rfc8794.html (EBML): // If an Empty Element has a default value declared, then the EBML // Reader MUST interpret the value of the Empty Element as the // default value. mVInt.mValue = EBML_MAX_ID_LENGTH_DEFAULT; } if (mVInt.mValue < 4 || mVInt.mValue > 5) { // https://www.ietf.org/archive/id/draft-ietf-cellar-matroska-13.html // (Matroska): // The EBMLMaxIDLength of the EBML Header MUST be "4". // // Also Matroska: // Element IDs are encoded using the VINT mechanism described in // Section 4 of [RFC8794] and can be between one and five octets // long. Five-octet-long Element IDs are possible only if declared // in the EBML header. nsPrintfCString detail("Invalid EMBLMaxIdLength %" PRIu64, mVInt.mValue); WEBM_DEBUG("%s", detail.get()); return MediaResult(NS_ERROR_FAILURE, detail); } mEBMLMaxIdLength = mVInt.mValue; mState = READ_ELEMENT_ID; break; case READ_EBML_MAX_SIZE_LENGTH: if (mElement.mSize.mLength == 0) { // https://www.rfc-editor.org/rfc/rfc8794.html (EBML): // If an Empty Element has a default value declared, then the EBML // Reader MUST interpret the value of the Empty Element as the // default value. mVInt.mValue = EBML_MAX_SIZE_LENGTH_DEFAULT; } if (mVInt.mValue < 1 || mVInt.mValue > 8) { // https://www.ietf.org/archive/id/draft-ietf-cellar-matroska-13.html // (Matroska): // The EBMLMaxSizeLength of the EBML Header MUST be between "1" and // "8" inclusive. nsPrintfCString detail("Invalid EMBLMaxSizeLength %" PRIu64, mVInt.mValue); WEBM_DEBUG("%s", detail.get()); return MediaResult(NS_ERROR_FAILURE, detail); } mEBMLMaxSizeLength = mVInt.mValue; mState = READ_ELEMENT_ID; break; case SKIP_DATA: if (mSkipBytes) { uint32_t left = aLength - (p - aBuffer); left = std::min(left, mSkipBytes); p += left; mSkipBytes -= left; } if (!mSkipBytes) { mBlockEndOffset = mCurrentOffset + (p - aBuffer); mState = mNextState; } break; case CHECK_INIT_FOUND: if (mSkipBytes) { uint32_t left = aLength - (p - aBuffer); left = std::min(left, mSkipBytes); p += left; mSkipBytes -= left; } if (!mSkipBytes) { if (mInitEndOffset < 0) { mInitEndOffset = mCurrentOffset + (p - aBuffer); mBlockEndOffset = mCurrentOffset + (p - aBuffer); } mState = READ_ELEMENT_ID; } break; } } NS_ASSERTION(p == aBuffer + aLength, "Must have parsed to end of data."); mCurrentOffset += aLength; return NS_OK; } int64_t WebMBufferedParser::EndSegmentOffset(int64_t aOffset) { if (mLastInitStartOffset > aOffset || mClusterOffset > aOffset) { return std::min( mLastInitStartOffset >= 0 ? mLastInitStartOffset : INT64_MAX, mClusterOffset >= 0 ? mClusterOffset : INT64_MAX); } return mBlockEndOffset; } int64_t WebMBufferedParser::GetClusterOffset() const { return mClusterOffset; } // SyncOffsetComparator and TimeComparator are slightly confusing, in that // the nsTArray they're used with (mTimeMapping) is sorted by mEndOffset and // these comparators are used on the other fields of WebMTimeDataOffset. // This is only valid because timecodes are required to be monotonically // increasing within a file (thus establishing an ordering relationship with // mTimecode), and mEndOffset is derived from mSyncOffset. struct SyncOffsetComparator { bool Equals(const WebMTimeDataOffset& a, const int64_t& b) const { return a.mSyncOffset == b; } bool LessThan(const WebMTimeDataOffset& a, const int64_t& b) const { return a.mSyncOffset < b; } }; struct TimeComparator { bool Equals(const WebMTimeDataOffset& a, const uint64_t& b) const { return a.mTimecode == b; } bool LessThan(const WebMTimeDataOffset& a, const uint64_t& b) const { return a.mTimecode < b; } }; bool WebMBufferedState::CalculateBufferedForRange(int64_t aStartOffset, int64_t aEndOffset, uint64_t* aStartTime, uint64_t* aEndTime) { MutexAutoLock lock(mMutex); // Find the first WebMTimeDataOffset at or after aStartOffset. uint32_t start = mTimeMapping.IndexOfFirstElementGt(aStartOffset - 1, SyncOffsetComparator()); if (start == mTimeMapping.Length()) { return false; } // Find the first WebMTimeDataOffset at or before aEndOffset. uint32_t end = mTimeMapping.IndexOfFirstElementGt(aEndOffset); if (end > 0) { end -= 1; } // Range is empty. if (end <= start) { return false; } NS_ASSERTION(mTimeMapping[start].mSyncOffset >= aStartOffset && mTimeMapping[end].mEndOffset <= aEndOffset, "Computed time range must lie within data range."); if (start > 0) { NS_ASSERTION(mTimeMapping[start - 1].mSyncOffset < aStartOffset, "Must have found least WebMTimeDataOffset for start"); } if (end < mTimeMapping.Length() - 1) { NS_ASSERTION(mTimeMapping[end + 1].mEndOffset > aEndOffset, "Must have found greatest WebMTimeDataOffset for end"); } MOZ_ASSERT(mTimeMapping[end].mTimecode >= mTimeMapping[end - 1].mTimecode); uint64_t frameDuration = mTimeMapping[end].mTimecode - mTimeMapping[end - 1].mTimecode; *aStartTime = mTimeMapping[start].mTimecode; CheckedUint64 endTime{mTimeMapping[end].mTimecode}; endTime += frameDuration; if (!endTime.isValid()) { WEBM_DEBUG("End time overflow during CalculateBufferedForRange."); return false; } *aEndTime = endTime.value(); return true; } bool WebMBufferedState::GetOffsetForTime(uint64_t aTime, int64_t* aOffset) { MutexAutoLock lock(mMutex); if (mTimeMapping.IsEmpty()) { return false; } uint64_t time = aTime; if (time > 0) { time = time - 1; } uint32_t idx = mTimeMapping.IndexOfFirstElementGt(time, TimeComparator()); if (idx == mTimeMapping.Length()) { // Clamp to end *aOffset = mTimeMapping[mTimeMapping.Length() - 1].mSyncOffset; } else { // Idx is within array or has been clamped to start *aOffset = mTimeMapping[idx].mSyncOffset; } return true; } void WebMBufferedState::NotifyDataArrived(const unsigned char* aBuffer, uint32_t aLength, int64_t aOffset) { uint32_t idx = mRangeParsers.IndexOfFirstElementGt(aOffset - 1); if (idx == 0 || !(mRangeParsers[idx - 1] == aOffset)) { // If the incoming data overlaps an already parsed range, adjust the // buffer so that we only reparse the new data. It's also possible to // have an overlap where the end of the incoming data is within an // already parsed range, but we don't bother handling that other than by // avoiding storing duplicate timecodes when the parser runs. if (idx != mRangeParsers.Length() && mRangeParsers[idx].mStartOffset <= aOffset) { // Complete overlap, skip parsing. if (aOffset + aLength <= mRangeParsers[idx].mCurrentOffset) { return; } // Partial overlap, adjust the buffer to parse only the new data. int64_t adjust = mRangeParsers[idx].mCurrentOffset - aOffset; NS_ASSERTION(adjust >= 0, "Overlap detection bug."); aBuffer += adjust; aLength -= uint32_t(adjust); } else { mRangeParsers.InsertElementAt(idx, WebMBufferedParser(aOffset)); if (idx != 0) { mRangeParsers[idx].SetTimecodeScale( mRangeParsers[0].GetTimecodeScale()); } } } { MutexAutoLock lock(mMutex); mRangeParsers[idx].Append(aBuffer, aLength, mTimeMapping); } // Merge parsers with overlapping regions and clean up the remnants. uint32_t i = 0; while (i + 1 < mRangeParsers.Length()) { if (mRangeParsers[i].mCurrentOffset >= mRangeParsers[i + 1].mStartOffset) { mRangeParsers[i + 1].mStartOffset = mRangeParsers[i].mStartOffset; mRangeParsers[i + 1].mInitEndOffset = mRangeParsers[i].mInitEndOffset; mRangeParsers.RemoveElementAt(i); } else { i += 1; } } if (mRangeParsers.IsEmpty()) { return; } MutexAutoLock lock(mMutex); mLastBlockOffset = mRangeParsers.LastElement().mBlockEndOffset; } void WebMBufferedState::Reset() { MutexAutoLock lock(mMutex); mRangeParsers.Clear(); mTimeMapping.Clear(); } void WebMBufferedState::UpdateIndex(const MediaByteRangeSet& aRanges, MediaResource* aResource) { for (uint32_t index = 0; index < aRanges.Length(); index++) { const MediaByteRange& range = aRanges[index]; int64_t offset = range.mStart; uint32_t length = range.mEnd - range.mStart; uint32_t idx = mRangeParsers.IndexOfFirstElementGt(offset - 1); if (!idx || !(mRangeParsers[idx - 1] == offset)) { // If the incoming data overlaps an already parsed range, adjust the // buffer so that we only reparse the new data. It's also possible to // have an overlap where the end of the incoming data is within an // already parsed range, but we don't bother handling that other than by // avoiding storing duplicate timecodes when the parser runs. if (idx != mRangeParsers.Length() && mRangeParsers[idx].mStartOffset <= offset) { // Complete overlap, skip parsing. if (offset + length <= mRangeParsers[idx].mCurrentOffset) { continue; } // Partial overlap, adjust the buffer to parse only the new data. int64_t adjust = mRangeParsers[idx].mCurrentOffset - offset; NS_ASSERTION(adjust >= 0, "Overlap detection bug."); offset += adjust; length -= uint32_t(adjust); } else { mRangeParsers.InsertElementAt(idx, WebMBufferedParser(offset)); if (idx) { mRangeParsers[idx].SetTimecodeScale( mRangeParsers[0].GetTimecodeScale()); } } } MediaResourceIndex res(aResource); while (length > 0) { static const uint32_t BLOCK_SIZE = 1048576; uint32_t block = std::min(length, BLOCK_SIZE); RefPtr bytes = res.CachedMediaReadAt(offset, block); if (!bytes) { break; } NotifyDataArrived(bytes->Elements(), bytes->Length(), offset); length -= bytes->Length(); offset += bytes->Length(); } } } int64_t WebMBufferedState::GetInitEndOffset() { if (mRangeParsers.IsEmpty()) { return -1; } return mRangeParsers[0].mInitEndOffset; } int64_t WebMBufferedState::GetLastBlockOffset() { MutexAutoLock lock(mMutex); return mLastBlockOffset; } bool WebMBufferedState::GetStartTime(uint64_t* aTime) { MutexAutoLock lock(mMutex); if (mTimeMapping.IsEmpty()) { return false; } uint32_t idx = mTimeMapping.IndexOfFirstElementGt(0, SyncOffsetComparator()); if (idx == mTimeMapping.Length()) { return false; } *aTime = mTimeMapping[idx].mTimecode; return true; } bool WebMBufferedState::GetNextKeyframeTime(uint64_t aTime, uint64_t* aKeyframeTime) { MutexAutoLock lock(mMutex); int64_t offset = 0; bool rv = GetOffsetForTime(aTime, &offset); if (!rv) { return false; } uint32_t idx = mTimeMapping.IndexOfFirstElementGt(offset, SyncOffsetComparator()); if (idx == mTimeMapping.Length()) { return false; } *aKeyframeTime = mTimeMapping[idx].mTimecode; return true; } } // namespace mozilla #undef WEBM_DEBUG