diff --git a/dom/media/MP3Demuxer.cpp b/dom/media/MP3Demuxer.cpp index 2ec509978..760e32232 100644 --- a/dom/media/MP3Demuxer.cpp +++ b/dom/media/MP3Demuxer.cpp @@ -157,7 +157,6 @@ MP3TrackDemuxer::SeekPosition() const { return pos; } -#ifdef ENABLE_TESTS const FrameParser::Frame& MP3TrackDemuxer::LastFrame() const { return mParser.PrevFrame(); @@ -167,7 +166,6 @@ RefPtr MP3TrackDemuxer::DemuxSample() { return GetNextFrame(FindNextFrame()); } -#endif const ID3Parser::ID3Header& MP3TrackDemuxer::ID3Header() const { @@ -204,7 +202,7 @@ MP3TrackDemuxer::FastSeek(const TimeUnit& aTime) { if (!aTime.ToMicroseconds()) { // Quick seek to the beginning of the stream. mFrameIndex = 0; - } else if (vbr.IsTOCPresent()) { + } else if (vbr.IsTOCPresent() && Duration().ToMicroseconds() > 0) { // Use TOC for more precise seeking. const float durationFrac = static_cast(aTime.ToMicroseconds()) / Duration().ToMicroseconds(); @@ -347,7 +345,7 @@ MP3TrackDemuxer::Duration() const { int64_t numFrames = 0; const auto numAudioFrames = mParser.VBRInfo().NumAudioFrames(); - if (numAudioFrames) { + if (mParser.VBRInfo().IsValid() && numAudioFrames.valueOr(0) + 1 > 1) { // VBR headers don't include the VBR header frame. numFrames = numAudioFrames.value() + 1; } else { @@ -356,7 +354,9 @@ MP3TrackDemuxer::Duration() const { // Unknown length, we can't estimate duration. return TimeUnit::FromMicroseconds(-1); } - numFrames = (streamLen - mFirstFrameOffset) / AverageFrameLength(); + if (AverageFrameLength() > 0) { + numFrames = (streamLen - mFirstFrameOffset) / AverageFrameLength(); + } } return Duration(numFrames); } @@ -371,6 +371,55 @@ MP3TrackDemuxer::Duration(int64_t aNumFrames) const { return TimeUnit::FromMicroseconds(aNumFrames * usPerFrame); } +MediaByteRange +MP3TrackDemuxer::FindFirstFrame() { + // Get engough successive frames to avoid invalid frame from cut stream. + // However, some website use very short mp3 file so using the same value as Chrome. + static const int MIN_SUCCESSIVE_FRAMES = 3; + + MediaByteRange candidateFrame = FindNextFrame(); + int numSuccFrames = candidateFrame.Length() > 0; + MediaByteRange currentFrame = candidateFrame; + MP3LOGV("FindFirst() first candidate frame: mOffset=%" PRIu64 " Length()=%" PRIu64, + candidateFrame.mStart, candidateFrame.Length()); + + while (candidateFrame.Length() && numSuccFrames < MIN_SUCCESSIVE_FRAMES) { + mParser.EndFrameSession(); + mOffset = currentFrame.mEnd; + const MediaByteRange prevFrame = currentFrame; + + // FindNextFrame() here will only return frames consistent with our candidate frame. + currentFrame = FindNextFrame(); + numSuccFrames += currentFrame.Length() > 0; + // Multiple successive false positives, which wouldn't be caught by the consistency + // checks alone, can be detected by wrong alignment (non-zero gap between frames). + const int64_t frameSeparation = currentFrame.mStart - prevFrame.mEnd; + + if (!currentFrame.Length() || frameSeparation != 0) { + MP3LOGV("FindFirst() not enough successive frames detected, " + "rejecting candidate frame: successiveFrames=%d, last Length()=%" PRIu64 + ", last frameSeparation=%" PRId64, numSuccFrames, currentFrame.Length(), + frameSeparation); + + mParser.ResetFrameData(); + mOffset = candidateFrame.mStart + 1; + candidateFrame = FindNextFrame(); + numSuccFrames = candidateFrame.Length() > 0; + currentFrame = candidateFrame; + MP3LOGV("FindFirst() new candidate frame: mOffset=%" PRIu64 " Length()=%" PRIu64, + candidateFrame.mStart, candidateFrame.Length()); + } + } + + if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) { + MP3LOG("FindFirst() accepting candidate frame: " + "successiveFrames=%d", numSuccFrames); + } else { + MP3LOG("FindFirst() no suitable first frame found"); + } + return candidateFrame; +} + static bool VerifyFrameConsistency( const FrameParser::Frame& aFrame1, const FrameParser::Frame& aFrame2) { @@ -511,7 +560,6 @@ MP3TrackDemuxer::GetNextFrame(const MediaByteRange& aRange) { if (mNumParsedFrames == 1) { // First frame parsed, let's read VBR info if available. - // TODO: read info that helps with seeking (bug 1163667). ByteReader reader(frame->Data(), frame->Size()); mParser.ParseVBRHeader(&reader); reader.DiscardRemaining(); @@ -532,7 +580,7 @@ MP3TrackDemuxer::OffsetFromFrameIndex(int64_t aFrameIndex) const { int64_t offset = 0; const auto& vbr = mParser.VBRInfo(); - if (vbr.NumBytes() && vbr.NumAudioFrames()) { + if (vbr.IsComplete()) { offset = mFirstFrameOffset + aFrameIndex * vbr.NumBytes().value() / vbr.NumAudioFrames().value(); } else if (AverageFrameLength() > 0) { @@ -548,7 +596,7 @@ MP3TrackDemuxer::FrameIndexFromOffset(int64_t aOffset) const { int64_t frameIndex = 0; const auto& vbr = mParser.VBRInfo(); - if (vbr.NumBytes() && vbr.NumAudioFrames()) { + if (vbr.IsComplete()) { frameIndex = static_cast(aOffset - mFirstFrameOffset) / vbr.NumBytes().value() * vbr.NumAudioFrames().value(); frameIndex = std::min(vbr.NumAudioFrames().value(), frameIndex); @@ -624,7 +672,7 @@ MP3TrackDemuxer::AverageFrameLength() const { return static_cast(mTotalFrameLen) / mNumParsedFrames; } const auto& vbr = mParser.VBRInfo(); - if (vbr.NumBytes() && vbr.NumAudioFrames()) { + if (vbr.IsComplete() && vbr.NumAudioFrames().value() + 1) { return static_cast(vbr.NumBytes().value()) / (vbr.NumAudioFrames().value() + 1); } @@ -651,15 +699,20 @@ FrameParser::Reset() { mFrame.Reset(); } +void +FrameParser::ResetFrameData() { + mFrame.Reset(); + mFirstFrame.Reset(); + mPrevFrame.Reset(); +} + void FrameParser::EndFrameSession() { if (!mID3Parser.Header().IsValid()) { // Reset ID3 tags only if we have not parsed a valid ID3 header yet. mID3Parser.Reset(); } -#ifdef ENABLE_TESTS mPrevFrame = mFrame; -#endif mFrame.Reset(); } @@ -668,12 +721,10 @@ FrameParser::CurrentFrame() const { return mFrame; } -#ifdef ENABLE_TESTS const FrameParser::Frame& FrameParser::PrevFrame() const { return mPrevFrame; } -#endif const FrameParser::Frame& FrameParser::FirstFrame() const { @@ -960,6 +1011,21 @@ FrameParser::VBRHeader::IsTOCPresent() const { return mTOC.size() == vbr_header::TOC_SIZE; } +bool +FrameParser::VBRHeader::IsValid() const { + return mType != NONE; +} + +bool +FrameParser::VBRHeader::IsComplete() const { + return IsValid() && + mNumAudioFrames.valueOr(0) > 0 && + mNumBytes.valueOr(0) > 0 && + // We don't care about the scale for any computations here. + // mScale < 101 && + true; +} + int64_t FrameParser::VBRHeader::Offset(float aDurationFac) const { if (!IsTOCPresent()) { @@ -1047,7 +1113,7 @@ FrameParser::VBRHeader::ParseVBRI(ByteReader* aReader) { MOZ_ASSERT(aReader); // ParseVBRI assumes that the ByteReader offset points to the beginning of a frame, // therefore as a simple check, we look for the presence of a frame sync at that position. - MOZ_ASSERT(aReader->PeekU16() & 0xFFE0); + MOZ_ASSERT((aReader->PeekU16() & 0xFFE0) == 0xFFE0); const size_t prevReaderOffset = aReader->Offset(); // VBRI have a fixed relative position, so let's check for it there. diff --git a/dom/media/MP3Demuxer.h b/dom/media/MP3Demuxer.h index f42126d76..a5346a195 100644 --- a/dom/media/MP3Demuxer.h +++ b/dom/media/MP3Demuxer.h @@ -232,6 +232,12 @@ public: // Returns true iff Xing/Info TOC (table of contents) is present. bool IsTOCPresent() const; + // Returns whether the header is valid (type XING or VBRI). + bool IsValid() const; + + // Returns whether the header is valid and contains reasonable non-zero field values. + bool IsComplete() const; + // Returns the byte offset for the given duration percentage as a factor // (0: begin, 1.0: end). int64_t Offset(float aDurationFac) const; @@ -298,10 +304,8 @@ public: // Returns the currently parsed frame. Reset via Reset or EndFrameSession. const Frame& CurrentFrame() const; -#ifdef ENABLE_TESTS // Returns the previously parsed frame. Reset via Reset. const Frame& PrevFrame() const; -#endif // Returns the first parsed frame. Reset via Reset. const Frame& FirstFrame() const; @@ -312,9 +316,13 @@ public: // Returns the parsed VBR header info. Note: check for validity by type. const VBRHeader& VBRInfo() const; - // Resets the parser. Don't use between frames as first frame data is reset. + // Resets the parser. void Reset(); + // Resets all frame data, but not the ID3Header. + // Don't use between frames as first frame data is reset. + void ResetFrameData(); + // Clear the last parsed frame to allow for next frame parsing, i.e.: // - sets PrevFrame to CurrentFrame // - resets the CurrentFrame @@ -344,9 +352,7 @@ private: // previously parsed frame for debugging and the currently parsed frame. Frame mFirstFrame; Frame mFrame; -#ifdef ENABLE_TESTS Frame mPrevFrame; -#endif }; // The MP3 demuxer used to extract MPEG frames and side information out of @@ -373,10 +379,8 @@ public: // Returns the estimated current seek position time. media::TimeUnit SeekPosition() const; -#ifdef ENABLE_TESTS const FrameParser::Frame& LastFrame() const; RefPtr DemuxSample(); -#endif const ID3Parser::ID3Header& ID3Header() const; const FrameParser::VBRHeader& VBRInfo() const; @@ -401,7 +405,12 @@ private: // Seeks by scanning the stream up to the given time for more accurate results. media::TimeUnit ScanUntil(const media::TimeUnit& aTime); - // Finds the next valid frame and returns its byte range. + // Finds the first valid frame and returns its byte range if found + // or a null-byte range otherwise. + MediaByteRange FindFirstFrame(); + + // Finds the next valid frame and returns its byte range if found + // or a null-byte range otherwise. MediaByteRange FindNextFrame(); // Skips the next frame given the provided byte range.