Bug 1364866 - Part 2 - Set MAX_SKIPPED_BYTES dynamically depending on parsing state. r?esawin draft
authorJan Henning <jh+bugzilla@buttercookie.de>
Mon, 15 May 2017 22:49:40 +0200
changeset 591979 02711c41f363adc7e9590123824d438f8daea2be
parent 590402 0f5c5c46f50e8faa82c9ac3a03284f535092b80c
child 592139 2286c757017276f39463cd061f4519299eafd9b8
child 592143 11212fa37bec954cf3bf1e66aa2a126f92180b8b
push id63228
push usermozilla@buttercookie.de
push dateFri, 09 Jun 2017 18:52:28 +0000
reviewersesawin
bugs1364866
milestone55.0a1
Bug 1364866 - Part 2 - Set MAX_SKIPPED_BYTES dynamically depending on parsing state. r?esawin As long as we're undecided whether the data we're being fed is actually a valid MPEG stream, we only search a limited amount of data for a valid frame header before giving up. Once we have found a valid frame header however, we change our behavior and search until EOS for the next frame in order to cope with possibly corrupted streams. In practice it has turned out that the first MPEG frame we detect might in fact be a false positive. Therefore, we now turn any found frame into a candidate frame at first and only accept it as the real first frame if it is followed by at least two other matching frames in succession. As currently implemented, our MAX_SKIPPED_BYTES logic however doesn't know about this and turns itself off as soon as we've found anything that *looks* like a valid MP3 frame header. This means that if that frame was in fact a false positive, we can now go off on a wild goose chase through what might potentially be the *whole* file while looking for putative followup frames. To fix this, the parser now records not just whether it has found *any* frame, but also whether it has detected a *succession of valid frames* and then sets the amount of bytes to be searched for the next frame based on that state. MozReview-Commit-ID: 3WQTfLg88kV
dom/media/MP3Demuxer.cpp
dom/media/MP3Demuxer.h
--- a/dom/media/MP3Demuxer.cpp
+++ b/dom/media/MP3Demuxer.cpp
@@ -3,16 +3,17 @@
 /* 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 "MP3Demuxer.h"
 
 #include <inttypes.h>
 #include <algorithm>
+#include <limits>
 
 #include "mozilla/Assertions.h"
 #include "mozilla/EndianUtils.h"
 #include "mozilla/SizePrintfMacros.h"
 #include "nsAutoPtr.h"
 #include "VideoUtils.h"
 #include "TimeUnits.h"
 #include "prenv.h"
@@ -101,16 +102,17 @@ MP3Demuxer::NotifyDataRemoved()
   MP3LOGV("NotifyDataRemoved()");
 }
 
 
 // MP3TrackDemuxer
 
 MP3TrackDemuxer::MP3TrackDemuxer(MediaResource* aSource)
   : mSource(aSource)
+  , mFrameLock(false)
   , mOffset(0)
   , mFirstFrameOffset(0)
   , mNumParsedFrames(0)
   , mFrameIndex(0)
   , mTotalFrameLen(0)
   , mSamplesPerFrame(0)
   , mSamplesPerSecond(0)
   , mChannels(0)
@@ -420,16 +422,17 @@ MP3TrackDemuxer::Duration(int64_t aNumFr
 MediaByteRange
 MP3TrackDemuxer::FindFirstFrame()
 {
   // We attempt to find multiple successive frames to avoid locking onto a false
   // positive if we're fed a stream that has been cut mid-frame.
   // For compatibility reasons we have to use the same frame count as Chrome, since
   // some web sites actually use a file that short to test our playback capabilities.
   static const int MIN_SUCCESSIVE_FRAMES = 3;
+  mFrameLock = false;
 
   MediaByteRange candidateFrame = FindNextFrame();
   int numSuccFrames = candidateFrame.Length() > 0;
   MediaByteRange currentFrame = candidateFrame;
   MP3LOGV("FindFirst() first candidate frame: mOffset=%" PRIu64
           " Length()=%" PRIu64,
           candidateFrame.mStart, candidateFrame.Length());
 
@@ -460,16 +463,17 @@ MP3TrackDemuxer::FindFirstFrame()
               " Length()=%" PRIu64,
               candidateFrame.mStart, candidateFrame.Length());
     }
   }
 
   if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) {
     MP3LOG("FindFirst() accepting candidate frame: "
            "successiveFrames=%d", numSuccFrames);
+    mFrameLock = true;
   } else {
     MP3LOG("FindFirst() no suitable first frame found");
   }
   return candidateFrame;
 }
 
 static bool
 VerifyFrameConsistency(const FrameParser::Frame& aFrame1,
@@ -488,36 +492,63 @@ VerifyFrameConsistency(const FrameParser
          && h1.RawVersion() == h2.RawVersion()
          && h1.RawProtection() == h2.RawProtection();
 }
 
 MediaByteRange
 MP3TrackDemuxer::FindNextFrame()
 {
   static const int BUFFER_SIZE = 64;
-  static const int MAX_SKIPPED_BYTES = 1024 * BUFFER_SIZE;
+  static const uint32_t MAX_SKIPPABLE_BYTES = 1024 * BUFFER_SIZE;
 
   MP3LOGV("FindNext() Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
           " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
           " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
           mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
           mSamplesPerFrame, mSamplesPerSecond, mChannels);
 
   uint8_t buffer[BUFFER_SIZE];
   int32_t read = 0;
 
   bool foundFrame = false;
   int64_t frameHeaderOffset = 0;
+  int64_t startOffset = mOffset;
+  const bool searchingForID3 = !mParser.ID3Header().Size();
 
   // Check whether we've found a valid MPEG frame.
   while (!foundFrame) {
-    if ((!mParser.FirstFrame().Length()
-         && mOffset - mParser.ID3Header().TotalTagSize() > MAX_SKIPPED_BYTES)
+    // How many bytes we can go without finding a valid MPEG frame
+    // (effectively rounded up to the next full buffer size multiple, as we
+    // only check this before reading the next set of data into the buffer).
+
+    // This default value of 0 will be used during testing whether we're being
+    // fed a valid stream, which shouldn't have any gaps between frames.
+    uint32_t maxSkippableBytes = 0;
+
+    if (!mParser.FirstFrame().Length()) {
+      // We're looking for the first valid frame. A well-formed file should
+      // have its first frame header right at the start (skipping an ID3 tag
+      // if necessary), but in order to support files that might have been
+      // improperly cut, we search the first few kB for a frame header.
+      maxSkippableBytes = MAX_SKIPPABLE_BYTES;
+      // Since we're counting the skipped bytes from the offset we started
+      // this parsing session with, we need to discount the ID3 tag size only
+      // if we were looking for one during the current frame parsing session.
+      if (searchingForID3) {
+        maxSkippableBytes += mParser.ID3Header().TotalTagSize();
+      }
+    } else if (mFrameLock) {
+      // We've found a valid MPEG stream, so don't impose any limits
+      // to allow skipping corrupted data until we hit EOS.
+      maxSkippableBytes = std::numeric_limits<uint32_t>::max();
+    }
+
+    if ((mOffset - startOffset > maxSkippableBytes)
         || (read = Read(buffer, mOffset, BUFFER_SIZE)) == 0) {
-      MP3LOG("FindNext() EOS or exceeded MAX_SKIPPED_BYTES without a frame");
+      MP3LOG("FindNext() EOS or exceeded maxSkippeableBytes without a frame");
       // This is not a valid MPEG audio stream or we've reached EOS, give up.
       break;
     }
 
     ByteReader reader(buffer, read);
     uint32_t bytesToSkip = 0;
     foundFrame = mParser.Parse(&reader, &bytesToSkip);
     frameHeaderOffset =
--- a/dom/media/MP3Demuxer.h
+++ b/dom/media/MP3Demuxer.h
@@ -449,16 +449,19 @@ private:
   double AverageFrameLength() const;
 
   // The (hopefully) MPEG resource.
   MediaResourceIndex mSource;
 
   // MPEG frame parser used to detect frames and extract side info.
   FrameParser mParser;
 
+  // Whether we've locked onto a valid sequence of frames or not.
+  bool mFrameLock;
+
   // Current byte offset in the source stream.
   int64_t mOffset;
 
   // Byte offset of the begin of the first frame, or 0 if none parsed yet.
   int64_t mFirstFrameOffset;
 
   // Total parsed frames.
   uint64_t mNumParsedFrames;