summaryrefslogtreecommitdiffstats
path: root/dom/media/mp3
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/mp3
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/media/mp3/MP3Decoder.cpp45
-rw-r--r--dom/media/mp3/MP3Decoder.h29
-rw-r--r--dom/media/mp3/MP3Demuxer.cpp890
-rw-r--r--dom/media/mp3/MP3Demuxer.h187
-rw-r--r--dom/media/mp3/MP3FrameParser.cpp817
-rw-r--r--dom/media/mp3/MP3FrameParser.h374
-rw-r--r--dom/media/mp3/moz.build22
7 files changed, 2364 insertions, 0 deletions
diff --git a/dom/media/mp3/MP3Decoder.cpp b/dom/media/mp3/MP3Decoder.cpp
new file mode 100644
index 0000000000..efbe96cf4b
--- /dev/null
+++ b/dom/media/mp3/MP3Decoder.cpp
@@ -0,0 +1,45 @@
+/* -*- 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 "MP3Decoder.h"
+#include "MediaContainerType.h"
+#include "PDMFactory.h"
+
+namespace mozilla {
+
+/* static */
+bool MP3Decoder::IsEnabled() {
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ return platform->SupportsMimeType("audio/mpeg"_ns) !=
+ media::DecodeSupport::Unsupported;
+}
+
+/* static */
+bool MP3Decoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (aContainerType.Type() == MEDIAMIMETYPE("audio/mp3") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/mpeg")) {
+ return IsEnabled() && (aContainerType.ExtendedType().Codecs().IsEmpty() ||
+ aContainerType.ExtendedType().Codecs() == "mp3");
+ }
+ return false;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> MP3Decoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mpeg"_ns, aType));
+
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp3/MP3Decoder.h b/dom/media/mp3/MP3Decoder.h
new file mode 100644
index 0000000000..eab5387b51
--- /dev/null
+++ b/dom/media/mp3/MP3Decoder.h
@@ -0,0 +1,29 @@
+/* -*- 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/. */
+#ifndef MP3Decoder_h_
+#define MP3Decoder_h_
+
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class MP3Decoder {
+ public:
+ // Returns true if the MP3 backend is preffed on, and we're running on a
+ // platform that is likely to have decoders for the format.
+ static bool IsEnabled();
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/MP3Demuxer.cpp b/dom/media/mp3/MP3Demuxer.cpp
new file mode 100644
index 0000000000..25d878b3be
--- /dev/null
+++ b/dom/media/mp3/MP3Demuxer.cpp
@@ -0,0 +1,890 @@
+/* -*- 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 "MP3Demuxer.h"
+
+#include <algorithm>
+#include <inttypes.h>
+#include <limits>
+
+#include "ByteWriter.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "mozilla/Assertions.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define MP3LOG(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, msg, ##__VA_ARGS__)
+#define MP3LOGV(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, msg, ##__VA_ARGS__)
+
+using mozilla::media::TimeInterval;
+using mozilla::media::TimeIntervals;
+using mozilla::media::TimeUnit;
+
+namespace mozilla {
+
+// MP3Demuxer
+
+MP3Demuxer::MP3Demuxer(MediaResource* aSource) : mSource(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+bool MP3Demuxer::InitInternal() {
+ if (!mTrackDemuxer) {
+ mTrackDemuxer = new MP3TrackDemuxer(mSource);
+ DDLINKCHILD("track demuxer", mTrackDemuxer.get());
+ }
+ return mTrackDemuxer->Init();
+}
+
+RefPtr<MP3Demuxer::InitPromise> MP3Demuxer::Init() {
+ if (!InitInternal()) {
+ MP3LOG("MP3Demuxer::Init() failure: waiting for data");
+
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ MP3LOG("MP3Demuxer::Init() successful");
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+uint32_t MP3Demuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ return aType == TrackInfo::kAudioTrack ? 1u : 0u;
+}
+
+already_AddRefed<MediaTrackDemuxer> MP3Demuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (!mTrackDemuxer) {
+ return nullptr;
+ }
+ return RefPtr<MP3TrackDemuxer>(mTrackDemuxer).forget();
+}
+
+bool MP3Demuxer::IsSeekable() const { return true; }
+
+void MP3Demuxer::NotifyDataArrived() {
+ // TODO: bug 1169485.
+ NS_WARNING("Unimplemented function NotifyDataArrived");
+ MP3LOGV("NotifyDataArrived()");
+}
+
+void MP3Demuxer::NotifyDataRemoved() {
+ // TODO: bug 1169485.
+ NS_WARNING("Unimplemented function 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) {
+ DDLINKCHILD("source", aSource);
+ Reset();
+}
+
+bool MP3TrackDemuxer::Init() {
+ Reset();
+ FastSeek(TimeUnit());
+ // Read the first frame to fetch sample rate and other meta data.
+ RefPtr<MediaRawData> frame(GetNextFrame(FindFirstFrame()));
+
+ MP3LOG("Init StreamLength()=%" PRId64 " first-frame-found=%d", StreamLength(),
+ !!frame);
+
+ if (!frame) {
+ return false;
+ }
+
+ // Rewind back to the stream begin to avoid dropping the first frame.
+ FastSeek(TimeUnit());
+
+ if (!mInfo) {
+ mInfo = MakeUnique<AudioInfo>();
+ }
+
+ mInfo->mRate = mSamplesPerSecond;
+ mInfo->mChannels = mChannels;
+ mInfo->mBitDepth = 16;
+ mInfo->mMimeType = "audio/mpeg";
+ mInfo->mDuration = Duration().valueOr(TimeUnit::FromInfinity());
+ Mp3CodecSpecificData mp3CodecData{};
+ if (mEncoderDelay) {
+ mp3CodecData.mEncoderDelayFrames = mEncoderDelay;
+ mp3CodecData.mEncoderPaddingFrames = mEncoderPadding;
+ }
+ mInfo->mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(mp3CodecData)};
+
+ MP3LOG("Init mInfo={mRate=%d mChannels=%d mBitDepth=%d mDuration=%s (%lfs)}",
+ mInfo->mRate, mInfo->mChannels, mInfo->mBitDepth,
+ mInfo->mDuration.ToString().get(), mInfo->mDuration.ToSeconds());
+
+ return mSamplesPerSecond && mChannels;
+}
+
+media::TimeUnit MP3TrackDemuxer::SeekPosition() const {
+ TimeUnit pos = Duration(mFrameIndex);
+ auto duration = Duration();
+ if (duration) {
+ pos = std::min(*duration, pos);
+ }
+ return pos;
+}
+
+const FrameParser::Frame& MP3TrackDemuxer::LastFrame() const {
+ return mParser.PrevFrame();
+}
+
+RefPtr<MediaRawData> MP3TrackDemuxer::DemuxSample() {
+ return GetNextFrame(FindNextFrame());
+}
+
+const ID3Parser::ID3Header& MP3TrackDemuxer::ID3Header() const {
+ return mParser.ID3Header();
+}
+
+const FrameParser::VBRHeader& MP3TrackDemuxer::VBRInfo() const {
+ return mParser.VBRInfo();
+}
+
+UniquePtr<TrackInfo> MP3TrackDemuxer::GetInfo() const { return mInfo->Clone(); }
+
+RefPtr<MP3TrackDemuxer::SeekPromise> MP3TrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ mRemainingEncoderPadding = AssertedCast<int32_t>(mEncoderPadding);
+ // Efficiently seek to the position.
+ FastSeek(aTime);
+ // Correct seek position by scanning the next frames.
+ const TimeUnit seekTime = ScanUntil(aTime);
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+TimeUnit MP3TrackDemuxer::FastSeek(const TimeUnit& aTime) {
+ MP3LOG("FastSeek(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ const auto& vbr = mParser.VBRInfo();
+ if (aTime.IsZero()) {
+ // Quick seek to the beginning of the stream.
+ mFrameIndex = 0;
+ } else if (vbr.IsTOCPresent() && Duration() &&
+ *Duration() != TimeUnit::Zero()) {
+ // Use TOC for more precise seeking.
+ mFrameIndex = FrameIndexFromOffset(vbr.Offset(aTime, Duration().value()));
+ } else if (AverageFrameLength() > 0) {
+ mFrameIndex = FrameIndexFromTime(aTime);
+ }
+
+ mOffset = OffsetFromFrameIndex(mFrameIndex);
+
+ if (mOffset > mFirstFrameOffset && StreamLength() > 0) {
+ mOffset = std::min(StreamLength() - 1, mOffset);
+ }
+
+ mParser.EndFrameSession();
+
+ MP3LOG("FastSeek End TOC=%d avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mFirstFrameOffset=%" PRId64
+ " mOffset=%" PRIu64 " SL=%" PRId64 " NumBytes=%u",
+ vbr.IsTOCPresent(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mFirstFrameOffset, mOffset, StreamLength(),
+ vbr.NumBytes().valueOr(0));
+
+ return Duration(mFrameIndex);
+}
+
+TimeUnit MP3TrackDemuxer::ScanUntil(const TimeUnit& aTime) {
+ MP3LOG("ScanUntil(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ if (aTime.IsZero()) {
+ return FastSeek(aTime);
+ }
+
+ if (Duration(mFrameIndex) > aTime) {
+ // We've seeked past the target time, rewind back a little to correct it.
+ const int64_t rewind = aTime.ToMicroseconds() / 100;
+ FastSeek(aTime - TimeUnit::FromMicroseconds(rewind));
+ }
+
+ if (Duration(mFrameIndex + 1) > aTime) {
+ return SeekPosition();
+ }
+
+ MediaByteRange nextRange = FindNextFrame();
+ while (SkipNextFrame(nextRange) && Duration(mFrameIndex + 1) < aTime) {
+ nextRange = FindNextFrame();
+ MP3LOGV("ScanUntil* avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64 " Duration=%" PRId64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset,
+ Duration(mFrameIndex + 1).ToMicroseconds());
+ }
+
+ MP3LOG("ScanUntil End avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset);
+
+ return SeekPosition();
+}
+
+RefPtr<MP3TrackDemuxer::SamplesPromise> MP3TrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ MP3LOGV("GetSamples(%d) Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ aNumSamples, mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ RefPtr<SamplesHolder> frames = new SamplesHolder();
+
+ while (aNumSamples--) {
+ RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame()));
+ if (!frame) {
+ break;
+ }
+ if (!frame->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ frames->AppendSample(frame);
+ }
+
+ MP3LOGV("GetSamples() End mSamples.Size()=%zu aNumSamples=%d mOffset=%" PRIu64
+ " mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d "
+ "mChannels=%d",
+ frames->GetSamples().Length(), aNumSamples, mOffset, mNumParsedFrames,
+ mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond,
+ mChannels);
+
+ if (frames->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ return SamplesPromise::CreateAndResolve(frames, __func__);
+}
+
+void MP3TrackDemuxer::Reset() {
+ MP3LOG("Reset()");
+
+ FastSeek(TimeUnit());
+ mParser.Reset();
+}
+
+RefPtr<MP3TrackDemuxer::SkipAccessPointPromise>
+MP3TrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ // Will not be called for audio-only resources.
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
+}
+
+int64_t MP3TrackDemuxer::GetResourceOffset() const { return mOffset; }
+
+TimeIntervals MP3TrackDemuxer::GetBuffered() {
+ AutoPinned<MediaResource> stream(mSource.GetResource());
+ TimeIntervals buffered;
+
+ if (Duration() && stream->IsDataCachedToEndOfResource(0)) {
+ // Special case completely cached files. This also handles local files.
+ buffered += TimeInterval(TimeUnit(), *Duration());
+ MP3LOGV("buffered = [[%" PRId64 ", %" PRId64 "]]",
+ TimeUnit().ToMicroseconds(), Duration()->ToMicroseconds());
+ return buffered;
+ }
+
+ MediaByteRangeSet ranges;
+ nsresult rv = stream->GetCachedRanges(ranges);
+ NS_ENSURE_SUCCESS(rv, buffered);
+
+ for (const auto& range : ranges) {
+ if (range.IsEmpty()) {
+ continue;
+ }
+ TimeUnit start = Duration(FrameIndexFromOffset(range.mStart));
+ TimeUnit end = Duration(FrameIndexFromOffset(range.mEnd));
+ MP3LOGV("buffered += [%" PRId64 ", %" PRId64 "]", start.ToMicroseconds(),
+ end.ToMicroseconds());
+ buffered += TimeInterval(start, end);
+ }
+
+ // If the number of frames reported by the header is valid,
+ // the duration calculated from it is the maximal duration.
+ if (ValidNumAudioFrames() && Duration()) {
+ TimeInterval duration = TimeInterval(TimeUnit(), *Duration());
+ return buffered.Intersection(duration);
+ }
+
+ return buffered;
+}
+
+int64_t MP3TrackDemuxer::StreamLength() const { return mSource.GetLength(); }
+
+media::NullableTimeUnit NothingIfNegative(TimeUnit aDuration) {
+ if (aDuration.IsNegative()) {
+ return Nothing();
+ }
+ return Some(aDuration);
+}
+
+media::NullableTimeUnit MP3TrackDemuxer::Duration() const {
+ if (!mNumParsedFrames) {
+ return Nothing();
+ }
+
+ int64_t numFrames = 0;
+ const auto numAudioFrames = ValidNumAudioFrames();
+ if (numAudioFrames) {
+ // VBR headers don't include the VBR header frame.
+ numFrames = numAudioFrames.value() + 1;
+ return NothingIfNegative(Duration(numFrames) -
+ (EncoderDelay() + Padding()));
+ }
+
+ const int64_t streamLen = StreamLength();
+ if (streamLen < 0) { // Live streams.
+ // Unknown length, we can't estimate duration.
+ return Nothing();
+ }
+ // We can't early return when streamLen < 0 before checking numAudioFrames
+ // since some live radio will give an opening remark before playing music
+ // and the duration of the opening talk can be calculated by numAudioFrames.
+
+ int64_t size = streamLen - mFirstFrameOffset;
+ MOZ_ASSERT(size);
+
+ if (mParser.ID3v1MetadataFound() && size > 128) {
+ size -= 128;
+ }
+
+ // If it's CBR, calculate the duration by bitrate.
+ if (!mParser.VBRInfo().IsValid()) {
+ const uint32_t bitrate = mParser.CurrentFrame().Header().Bitrate();
+ return NothingIfNegative(
+ media::TimeUnit::FromSeconds(static_cast<double>(size) * 8 / bitrate));
+ }
+
+ if (AverageFrameLength() > 0) {
+ numFrames = std::lround(AssertedCast<double>(size) / AverageFrameLength());
+ }
+
+ return NothingIfNegative(Duration(numFrames) - (EncoderDelay() + Padding()));
+}
+
+TimeUnit MP3TrackDemuxer::Duration(int64_t aNumFrames) const {
+ if (!mSamplesPerSecond) {
+ return TimeUnit::Invalid();
+ }
+
+ const int64_t frameCount = aNumFrames * mSamplesPerFrame;
+ return TimeUnit(frameCount, mSamplesPerSecond);
+}
+
+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());
+
+ while (candidateFrame.Length()) {
+ 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());
+ } else if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) {
+ MP3LOG(
+ "FindFirst() accepting candidate frame: "
+ "successiveFrames=%d",
+ numSuccFrames);
+ mFrameLock = true;
+ return candidateFrame;
+ } else if (prevFrame.mStart == mParser.TotalID3HeaderSize() &&
+ currentFrame.mEnd == StreamLength()) {
+ // We accept streams with only two frames if both frames are valid. This
+ // is to handle very short files and provide parity with Chrome. See
+ // bug 1432195 for more information. This will not handle short files
+ // with a trailing tag, but as of writing we lack infrastructure to
+ // handle such tags.
+ MP3LOG(
+ "FindFirst() accepting candidate frame for short stream: "
+ "successiveFrames=%d",
+ numSuccFrames);
+ mFrameLock = true;
+ return candidateFrame;
+ }
+ }
+
+ MP3LOG("FindFirst() no suitable first frame found");
+ return candidateFrame;
+}
+
+static bool VerifyFrameConsistency(const FrameParser::Frame& aFrame1,
+ const FrameParser::Frame& aFrame2) {
+ const auto& h1 = aFrame1.Header();
+ const auto& h2 = aFrame2.Header();
+
+ return h1.IsValid() && h2.IsValid() && h1.Layer() == h2.Layer() &&
+ h1.SlotSize() == h2.SlotSize() &&
+ h1.SamplesPerFrame() == h2.SamplesPerFrame() &&
+ h1.Channels() == h2.Channels() && h1.SampleRate() == h2.SampleRate() &&
+ h1.RawVersion() == h2.RawVersion() &&
+ h1.RawProtection() == h2.RawProtection();
+}
+
+MediaByteRange MP3TrackDemuxer::FindNextFrame() {
+ static const int BUFFER_SIZE = 64;
+ 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];
+ uint32_t read = 0;
+
+ bool foundFrame = false;
+ int64_t frameHeaderOffset = 0;
+ int64_t startOffset = mOffset;
+ const bool searchingForID3 = !mParser.ID3Header().HasSizeBeenSet();
+
+ // Check whether we've found a valid MPEG frame.
+ while (!foundFrame) {
+ // 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.TotalID3HeaderSize();
+ }
+ } 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 maxSkippeableBytes without a frame "
+ "(read: %d)",
+ read);
+ // This is not a valid MPEG audio stream or we've reached EOS, give up.
+ break;
+ }
+
+ BufferReader reader(buffer, read);
+ uint32_t bytesToSkip = 0;
+ auto res = mParser.Parse(&reader, &bytesToSkip);
+ foundFrame = res.unwrapOr(false);
+ int64_t readerOffset = static_cast<int64_t>(reader.Offset());
+ frameHeaderOffset = mOffset + readerOffset - FrameParser::FrameHeader::SIZE;
+
+ // If we've found neither an MPEG frame header nor an ID3v2 tag,
+ // the reader shouldn't have any bytes remaining.
+ MOZ_ASSERT(foundFrame || bytesToSkip || !reader.Remaining());
+
+ if (foundFrame && mParser.FirstFrame().Length() &&
+ !VerifyFrameConsistency(mParser.FirstFrame(), mParser.CurrentFrame())) {
+ MP3LOG("Skipping frame");
+ // We've likely hit a false-positive, ignore it and proceed with the
+ // search for the next valid frame.
+ foundFrame = false;
+ mOffset = frameHeaderOffset + 1;
+ mParser.EndFrameSession();
+ } else {
+ // Advance mOffset by the amount of bytes read and if necessary,
+ // skip an ID3v2 tag which stretches beyond the current buffer.
+ NS_ENSURE_TRUE(mOffset + read + bytesToSkip > mOffset,
+ MediaByteRange(0, 0));
+ mOffset += static_cast<int64_t>(read + bytesToSkip);
+ }
+ }
+
+ if (StreamLength() != -1) {
+ mEOS = frameHeaderOffset + mParser.CurrentFrame().Length() + BUFFER_SIZE >
+ StreamLength();
+ }
+
+ if (!foundFrame || !mParser.CurrentFrame().Length()) {
+ MP3LOG("FindNext() Exit foundFrame=%d mParser.CurrentFrame().Length()=%d ",
+ foundFrame, mParser.CurrentFrame().Length());
+ return {0, 0};
+ }
+
+ MP3LOGV("FindNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " frameHeaderOffset=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d"
+ " mChannels=%d, mEOS=%s",
+ mOffset, mNumParsedFrames, mFrameIndex, frameHeaderOffset,
+ mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels,
+ mEOS ? "true" : "false");
+
+ return {frameHeaderOffset,
+ frameHeaderOffset + mParser.CurrentFrame().Length()};
+}
+
+bool MP3TrackDemuxer::SkipNextFrame(const MediaByteRange& aRange) {
+ if (!mNumParsedFrames || !aRange.Length()) {
+ // We can't skip the first frame, since it could contain VBR headers.
+ RefPtr<MediaRawData> frame(GetNextFrame(aRange));
+ return frame;
+ }
+
+ UpdateState(aRange);
+
+ MP3LOGV("SkipNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ return true;
+}
+
+media::TimeUnit MP3TrackDemuxer::EncoderDelay() const {
+ return media::TimeUnit(mEncoderDelay, mSamplesPerSecond);
+}
+
+uint32_t MP3TrackDemuxer::EncoderDelayFrames() const { return mEncoderDelay; }
+
+media::TimeUnit MP3TrackDemuxer::Padding() const {
+ return media::TimeUnit(mEncoderPadding, mSamplesPerSecond);
+}
+
+uint32_t MP3TrackDemuxer::PaddingFrames() const { return mEncoderPadding; }
+
+already_AddRefed<MediaRawData> MP3TrackDemuxer::GetNextFrame(
+ const MediaByteRange& aRange) {
+ MP3LOG("GetNext() Begin({mStart=%" PRId64 " Length()=%" PRId64 "})",
+ aRange.mStart, aRange.Length());
+ if (!aRange.Length()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> frame = new MediaRawData();
+ frame->mOffset = aRange.mStart;
+
+ UniquePtr<MediaRawDataWriter> frameWriter(frame->CreateWriter());
+ if (!frameWriter->SetSize(static_cast<size_t>(aRange.Length()))) {
+ MP3LOG("GetNext() Exit failed to allocated media buffer");
+ return nullptr;
+ }
+
+ const uint32_t read =
+ Read(frameWriter->Data(), frame->mOffset, frame->Size());
+
+ if (read != aRange.Length()) {
+ MP3LOG("GetNext() Exit read=%u frame->Size()=%zu", read, frame->Size());
+ return nullptr;
+ }
+
+ UpdateState(aRange);
+
+ if (mNumParsedFrames == 1) {
+ // First frame parsed, let's read VBR info if available.
+ BufferReader reader(frame->Data(), frame->Size());
+ mFirstFrameOffset = frame->mOffset;
+
+ if (mParser.ParseVBRHeader(&reader)) {
+ // Parsing was successful
+ if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::XING) {
+ MP3LOG("XING header present, skipping encoder delay (%u frames)",
+ mParser.VBRInfo().EncoderDelay());
+ mEncoderDelay = mParser.VBRInfo().EncoderDelay();
+ mEncoderPadding = mParser.VBRInfo().EncoderPadding();
+ // Padding is encoded as a 12-bit unsigned number so this is fine.
+ mRemainingEncoderPadding = AssertedCast<int32_t>(mEncoderPadding);
+ if (mEncoderDelay == 0) {
+ // Skip the VBR frame + the decoder delay, that is always 529 frames
+ // in practice for the decoder we're using.
+ mEncoderDelay = mSamplesPerFrame + 529;
+ MP3LOG(
+ "No explicit delay present in vbr header, delay is assumed to be "
+ "%u frames\n",
+ mEncoderDelay);
+ }
+ } else if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::VBRI) {
+ MP3LOG("VBRI header present, skipping encoder delay (%u frames)",
+ mParser.VBRInfo().EncoderDelay());
+ mEncoderDelay = mParser.VBRInfo().EncoderDelay();
+ }
+ }
+ }
+
+ TimeUnit rawPts = Duration(mFrameIndex - 1) - EncoderDelay();
+ TimeUnit rawDuration = Duration(1);
+ TimeUnit rawEnd = rawPts + rawDuration;
+
+ frame->mTime = std::max(TimeUnit::Zero(mSamplesPerSecond), rawPts);
+
+ frame->mDuration = Duration(1);
+ frame->mTimecode = frame->mTime;
+ frame->mKeyframe = true;
+ frame->mEOS = mEOS;
+
+ // Handle decoder delay. A packet must be trimmed if its pts, adjusted for
+ // decoder delay, is negative. A packet can be trimmed entirely.
+ if (rawPts.IsNegative()) {
+ frame->mDuration =
+ std::max(TimeUnit::Zero(mSamplesPerSecond), rawEnd - frame->mTime);
+ }
+
+ // It's possible to create an mp3 file that has a padding value that somehow
+ // spans multiple packets. In that case the duration is probably known,
+ // because it's probably a VBR file with a XING header (that has a duration
+ // field). Use the duration to be able to set the correct duration on
+ // packets that aren't the last one.
+ // For most files, the padding is less than a packet, it's simply substracted.
+ if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::XING &&
+ mRemainingEncoderPadding > 0 &&
+ frame->GetEndTime() > Duration().valueOr(TimeUnit::FromInfinity())) {
+ TimeUnit duration = Duration().value();
+ TimeUnit inPaddingZone = frame->GetEndTime() - duration;
+ TimeUnit originalEnd = frame->GetEndTime();
+ TimeUnit originalPts = frame->mTime;
+ frame->mDuration -= inPaddingZone;
+ // Packet is entirely padding and will be completely discarded by the
+ // decoder.
+ if (frame->mDuration.IsNegative()) {
+ frame->mDuration = TimeUnit::Zero(mSamplesPerSecond);
+ }
+ int32_t paddingFrames =
+ AssertedCast<int32_t>(inPaddingZone.ToTicksAtRate(mSamplesPerSecond));
+ if (mRemainingEncoderPadding >= paddingFrames) {
+ mRemainingEncoderPadding -= paddingFrames;
+ } else {
+ mRemainingEncoderPadding = 0;
+ }
+ MP3LOG("Trimming [%s, %s] to [%s,%s] (padding) (stream duration: %s)",
+ originalPts.ToString().get(), originalEnd.ToString().get(),
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get(),
+ duration.ToString().get());
+ } else if (frame->mEOS &&
+ mRemainingEncoderPadding <=
+ frame->mDuration.ToTicksAtRate(mSamplesPerSecond)) {
+ frame->mDuration -= TimeUnit(mRemainingEncoderPadding, mSamplesPerSecond);
+ MOZ_ASSERT(frame->mDuration.IsPositiveOrZero());
+ MP3LOG("Trimming last packet %s to [%s,%s]", Padding().ToString().get(),
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get());
+ }
+
+ MP3LOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d, mEOS=%s",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels,
+ mEOS ? "true" : "false");
+
+ // It's possible for the duration of a frame to be zero if the frame is to be
+ // trimmed entirely because it's fully comprised of decoder delay samples.
+ // This is common at the beginning of an stream.
+ MOZ_ASSERT(frame->mDuration.IsPositiveOrZero());
+
+ MP3LOG("Packet demuxed: pts [%s, %s] (duration: %s)",
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get(),
+ frame->mDuration.ToString().get());
+
+ // Indicate original packet information to trim after decoding.
+ if (frame->mDuration != rawDuration) {
+ frame->mOriginalPresentationWindow = Some(TimeInterval{rawPts, rawEnd});
+ MP3LOG("Total packet time excluding trimming: [%s, %s]",
+ rawPts.ToString().get(), rawEnd.ToString().get());
+ }
+
+ return frame.forget();
+}
+
+int64_t MP3TrackDemuxer::OffsetFromFrameIndex(int64_t aFrameIndex) const {
+ int64_t offset = 0;
+ const auto& vbr = mParser.VBRInfo();
+
+ if (vbr.IsComplete()) {
+ offset = mFirstFrameOffset + aFrameIndex * vbr.NumBytes().value() /
+ vbr.NumAudioFrames().value();
+ } else if (AverageFrameLength() > 0) {
+ offset = mFirstFrameOffset +
+ AssertedCast<int64_t>(static_cast<float>(aFrameIndex) *
+ AverageFrameLength());
+ }
+
+ MP3LOGV("OffsetFromFrameIndex(%" PRId64 ") -> %" PRId64, aFrameIndex, offset);
+ return std::max<int64_t>(mFirstFrameOffset, offset);
+}
+
+int64_t MP3TrackDemuxer::FrameIndexFromOffset(int64_t aOffset) const {
+ int64_t frameIndex = 0;
+ const auto& vbr = mParser.VBRInfo();
+
+ if (vbr.IsComplete()) {
+ frameIndex =
+ AssertedCast<int64_t>(static_cast<float>(aOffset - mFirstFrameOffset) /
+ static_cast<float>(vbr.NumBytes().value()) *
+ static_cast<float>(vbr.NumAudioFrames().value()));
+ frameIndex = std::min<int64_t>(vbr.NumAudioFrames().value(), frameIndex);
+ } else if (AverageFrameLength() > 0) {
+ frameIndex = AssertedCast<int64_t>(
+ static_cast<float>(aOffset - mFirstFrameOffset) / AverageFrameLength());
+ }
+
+ MP3LOGV("FrameIndexFromOffset(%" PRId64 ") -> %" PRId64, aOffset, frameIndex);
+ return std::max<int64_t>(0, frameIndex);
+}
+
+int64_t MP3TrackDemuxer::FrameIndexFromTime(
+ const media::TimeUnit& aTime) const {
+ int64_t frameIndex = 0;
+ if (mSamplesPerSecond > 0 && mSamplesPerFrame > 0) {
+ frameIndex = AssertedCast<int64_t>(
+ aTime.ToSeconds() * mSamplesPerSecond / mSamplesPerFrame - 1);
+ }
+
+ MP3LOGV("FrameIndexFromOffset(%fs) -> %" PRId64, aTime.ToSeconds(),
+ frameIndex);
+ return std::max<int64_t>(0, frameIndex);
+}
+
+void MP3TrackDemuxer::UpdateState(const MediaByteRange& aRange) {
+ // Prevent overflow.
+ if (mTotalFrameLen + aRange.Length() < mTotalFrameLen) {
+ // These variables have a linear dependency and are only used to derive the
+ // average frame length.
+ mTotalFrameLen /= 2;
+ mNumParsedFrames /= 2;
+ }
+
+ // Full frame parsed, move offset to its end.
+ mOffset = aRange.mEnd;
+
+ mTotalFrameLen += aRange.Length();
+
+ if (!mSamplesPerFrame) {
+ mSamplesPerFrame = mParser.CurrentFrame().Header().SamplesPerFrame();
+ mSamplesPerSecond = mParser.CurrentFrame().Header().SampleRate();
+ mChannels = mParser.CurrentFrame().Header().Channels();
+ }
+
+ ++mNumParsedFrames;
+ ++mFrameIndex;
+ MOZ_ASSERT(mFrameIndex > 0);
+
+ // Prepare the parser for the next frame parsing session.
+ mParser.EndFrameSession();
+}
+
+uint32_t MP3TrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset,
+ uint32_t aSize) {
+ MP3LOGV("MP3TrackDemuxer::Read(%p %" PRId64 " %d)", aBuffer, aOffset, aSize);
+
+ const int64_t streamLen = StreamLength();
+ if (mInfo && streamLen > 0) {
+ // Prevent blocking reads after successful initialization.
+ int64_t max = streamLen > aOffset ? streamLen - aOffset : 0;
+ aSize = std::min<int64_t>(aSize, max);
+ }
+
+ uint32_t read = 0;
+ MP3LOGV("MP3TrackDemuxer::Read -> ReadAt(%u)", aSize);
+ const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
+ static_cast<uint32_t>(aSize), &read);
+ NS_ENSURE_SUCCESS(rv, 0);
+ return read;
+}
+
+double MP3TrackDemuxer::AverageFrameLength() const {
+ if (mNumParsedFrames) {
+ return static_cast<double>(mTotalFrameLen) /
+ static_cast<double>(mNumParsedFrames);
+ }
+ const auto& vbr = mParser.VBRInfo();
+ if (vbr.IsComplete() && vbr.NumAudioFrames().value() + 1) {
+ return static_cast<double>(vbr.NumBytes().value()) /
+ (vbr.NumAudioFrames().value() + 1);
+ }
+ return 0.0;
+}
+
+Maybe<uint32_t> MP3TrackDemuxer::ValidNumAudioFrames() const {
+ return mParser.VBRInfo().IsValid() &&
+ mParser.VBRInfo().NumAudioFrames().valueOr(0) + 1 > 1
+ ? mParser.VBRInfo().NumAudioFrames()
+ : Nothing();
+}
+
+} // namespace mozilla
+
+#undef MP3LOG
+#undef MP3LOGV
diff --git a/dom/media/mp3/MP3Demuxer.h b/dom/media/mp3/MP3Demuxer.h
new file mode 100644
index 0000000000..5189e82acf
--- /dev/null
+++ b/dom/media/mp3/MP3Demuxer.h
@@ -0,0 +1,187 @@
+/* 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/. */
+
+#ifndef MP3_DEMUXER_H_
+#define MP3_DEMUXER_H_
+
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+#include "MP3FrameParser.h"
+
+namespace mozilla {
+
+class MP3TrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(MP3Demuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(MP3TrackDemuxer, MediaTrackDemuxer);
+
+class MP3Demuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<MP3Demuxer> {
+ public:
+ // MediaDataDemuxer interface.
+ explicit MP3Demuxer(MediaResource* aSource);
+ RefPtr<InitPromise> Init() override;
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+ bool IsSeekable() const override;
+ void NotifyDataArrived() override;
+ void NotifyDataRemoved() override;
+
+ private:
+ // Synchronous initialization.
+ bool InitInternal();
+
+ RefPtr<MediaResource> mSource;
+ RefPtr<MP3TrackDemuxer> mTrackDemuxer;
+};
+
+// The MP3 demuxer used to extract MPEG frames and side information out of
+// MPEG streams.
+class MP3TrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<MP3TrackDemuxer> {
+ public:
+ // Constructor, expecting a valid media resource.
+ explicit MP3TrackDemuxer(MediaResource* aSource);
+
+ // Initializes the track demuxer by reading the first frame for meta data.
+ // Returns initialization success state.
+ bool Init();
+
+ // Returns the total stream length if known, -1 otherwise.
+ int64_t StreamLength() const;
+
+ // Returns the estimated stream duration, or a 0-duration if unknown.
+ media::NullableTimeUnit Duration() const;
+
+ // Returns the estimated duration up to the given frame number,
+ // or a 0-duration if unknown.
+ media::TimeUnit Duration(int64_t aNumFrames) const;
+
+ // Returns the estimated current seek position time.
+ media::TimeUnit SeekPosition() const;
+
+ const FrameParser::Frame& LastFrame() const;
+ RefPtr<MediaRawData> DemuxSample();
+
+ const ID3Parser::ID3Header& ID3Header() const;
+ const FrameParser::VBRHeader& VBRInfo() const;
+
+ // MediaTrackDemuxer interface.
+ UniquePtr<TrackInfo> GetInfo() const override;
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+ void Reset() override;
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+ int64_t GetResourceOffset() const override;
+ media::TimeIntervals GetBuffered() override;
+ // Return the duration in frames of the encoder delay.
+ uint32_t EncoderDelayFrames() const;
+ // Return the duration in frames of the padding.
+ uint32_t PaddingFrames() const;
+
+ private:
+ // Destructor.
+ ~MP3TrackDemuxer() = default;
+
+ // Fast approximate seeking to given time.
+ media::TimeUnit FastSeek(const media::TimeUnit& aTime);
+
+ // Seeks by scanning the stream up to the given time for more accurate
+ // results.
+ media::TimeUnit ScanUntil(const media::TimeUnit& aTime);
+
+ // 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.
+ bool SkipNextFrame(const MediaByteRange& aRange);
+
+ // Returns the next MPEG frame, if available.
+ already_AddRefed<MediaRawData> GetNextFrame(const MediaByteRange& aRange);
+
+ // Updates post-read meta data.
+ void UpdateState(const MediaByteRange& aRange);
+
+ // Returns the estimated offset for the given frame index.
+ int64_t OffsetFromFrameIndex(int64_t aFrameIndex) const;
+
+ // Returns the estimated frame index for the given offset.
+ int64_t FrameIndexFromOffset(int64_t aOffset) const;
+
+ // Returns the estimated frame index for the given time.
+ int64_t FrameIndexFromTime(const media::TimeUnit& aTime) const;
+
+ // Reads aSize bytes into aBuffer from the source starting at aOffset.
+ // Returns the actual size read.
+ uint32_t Read(uint8_t* aBuffer, int64_t aOffset, uint32_t aSize);
+
+ // Returns the average frame length derived from the previously parsed frames.
+ double AverageFrameLength() const;
+
+ // Returns the number of frames reported by the header if it's valid. Nothing
+ // otherwise.
+ Maybe<uint32_t> ValidNumAudioFrames() const;
+
+ // Return the duration of the encoder delay.
+ media::TimeUnit EncoderDelay() const;
+
+ // Return the duration of the padding.
+ media::TimeUnit Padding() 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;
+
+ // Current frame index.
+ int64_t mFrameIndex;
+
+ // Sum of parsed frames' lengths in bytes.
+ int64_t mTotalFrameLen;
+
+ // Samples per frame metric derived from frame headers or 0 if none available.
+ uint32_t mSamplesPerFrame;
+
+ // Samples per second metric derived from frame headers or 0 if none
+ // available.
+ uint32_t mSamplesPerSecond;
+
+ // Channel count derived from frame headers or 0 if none available.
+ uint32_t mChannels;
+
+ // Audio track config info.
+ UniquePtr<AudioInfo> mInfo;
+
+ // Number of frames to skip at the beginning
+ uint32_t mEncoderDelay = 0;
+ // Number of frames to skip at the end
+ uint32_t mEncoderPadding = 0;
+ int32_t mRemainingEncoderPadding = 0;
+ // End of stream has been found
+ bool mEOS = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/MP3FrameParser.cpp b/dom/media/mp3/MP3FrameParser.cpp
new file mode 100644
index 0000000000..9701aa2f8f
--- /dev/null
+++ b/dom/media/mp3/MP3FrameParser.cpp
@@ -0,0 +1,817 @@
+/* -*- 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 "MP3FrameParser.h"
+
+#include <algorithm>
+#include <inttypes.h>
+
+#include "TimeUnits.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "VideoUtils.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define MP3LOG(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, ("MP3Demuxer " msg, ##__VA_ARGS__))
+#define MP3LOGV(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, \
+ ("MP3Demuxer " msg, ##__VA_ARGS__))
+
+namespace mozilla {
+
+// FrameParser
+
+namespace frame_header {
+// FrameHeader mRaw byte offsets.
+static const int SYNC1 = 0;
+static const int SYNC2_VERSION_LAYER_PROTECTION = 1;
+static const int BITRATE_SAMPLERATE_PADDING_PRIVATE = 2;
+static const int CHANNELMODE_MODEEXT_COPY_ORIG_EMPH = 3;
+} // namespace frame_header
+
+FrameParser::FrameParser() = default;
+
+void FrameParser::Reset() {
+ mID3Parser.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();
+ }
+ mPrevFrame = mFrame;
+ mFrame.Reset();
+}
+
+const FrameParser::Frame& FrameParser::CurrentFrame() const { return mFrame; }
+
+const FrameParser::Frame& FrameParser::PrevFrame() const { return mPrevFrame; }
+
+const FrameParser::Frame& FrameParser::FirstFrame() const {
+ return mFirstFrame;
+}
+
+const ID3Parser::ID3Header& FrameParser::ID3Header() const {
+ return mID3Parser.Header();
+}
+
+uint32_t FrameParser::TotalID3HeaderSize() const {
+ uint32_t ID3v1Size = 0;
+ if (mID3v1MetadataFound) {
+ ID3v1Size = 128;
+ }
+ return ID3v1Size + mID3Parser.TotalHeadersSize();
+}
+
+const FrameParser::VBRHeader& FrameParser::VBRInfo() const {
+ return mVBRHeader;
+}
+
+Result<bool, nsresult> FrameParser::Parse(BufferReader* aReader,
+ uint32_t* aBytesToSkip) {
+ MOZ_ASSERT(aReader && aBytesToSkip);
+ *aBytesToSkip = 0;
+
+ if (ID3Parser::IsBufferStartingWithID3v1Tag(aReader)) {
+ // This is usually at the end of the file, and is always 128 bytes, that
+ // can simply be skipped.
+ aReader->Read(128);
+ *aBytesToSkip = 128;
+ mID3v1MetadataFound = true;
+ MP3LOGV("ID3v1 tag detected, skipping 128 bytes past the current buffer");
+ return false;
+ }
+
+ if (ID3Parser::IsBufferStartingWithID3Tag(aReader) && !mFirstFrame.Length()) {
+ // No MP3 frames have been parsed yet, look for ID3v2 headers at file begin.
+ // ID3v1 tags may only be at file end.
+ const size_t prevReaderOffset = aReader->Offset();
+ const uint32_t tagSize = mID3Parser.Parse(aReader);
+ if (!!tagSize) {
+ // ID3 tag found, skip past it.
+ const uint32_t skipSize = tagSize - ID3Parser::ID3Header::SIZE;
+
+ if (skipSize > aReader->Remaining()) {
+ // Skipping across the ID3v2 tag would take us past the end of the
+ // buffer, therefore we return immediately and let the calling function
+ // handle skipping the rest of the tag.
+ MP3LOGV(
+ "ID3v2 tag detected, size=%d,"
+ " needing to skip %zu bytes past the current buffer",
+ tagSize, skipSize - aReader->Remaining());
+ *aBytesToSkip = skipSize - aReader->Remaining();
+ return false;
+ }
+ MP3LOGV("ID3v2 tag detected, size=%d", tagSize);
+ aReader->Read(skipSize);
+ } else {
+ // No ID3v2 tag found, rewinding reader in order to search for a MPEG
+ // frame header.
+ aReader->Seek(prevReaderOffset);
+ }
+ }
+
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mFrame.ParseNext(res.unwrap()); res = aReader->ReadU8()) {
+ }
+
+ if (mFrame.Length()) {
+ // MP3 frame found.
+ if (!mFirstFrame.Length()) {
+ mFirstFrame = mFrame;
+ }
+ // Indicate success.
+ return true;
+ }
+ return false;
+}
+
+// FrameParser::Header
+
+FrameParser::FrameHeader::FrameHeader() { Reset(); }
+
+uint8_t FrameParser::FrameHeader::Sync1() const {
+ return mRaw[frame_header::SYNC1];
+}
+
+uint8_t FrameParser::FrameHeader::Sync2() const {
+ return 0x7 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 5;
+}
+
+uint8_t FrameParser::FrameHeader::RawVersion() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 3;
+}
+
+uint8_t FrameParser::FrameHeader::RawLayer() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::RawProtection() const {
+ return 0x1 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 6;
+}
+
+uint8_t FrameParser::FrameHeader::RawBitrate() const {
+ return 0xF & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 4;
+}
+
+uint8_t FrameParser::FrameHeader::RawSampleRate() const {
+ return 0x3 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 2;
+}
+
+uint8_t FrameParser::FrameHeader::Padding() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::Private() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE];
+}
+
+uint8_t FrameParser::FrameHeader::RawChannelMode() const {
+ return 0x3 & mRaw[frame_header::CHANNELMODE_MODEEXT_COPY_ORIG_EMPH] >> 6;
+}
+
+uint32_t FrameParser::FrameHeader::Layer() const {
+ static const uint8_t LAYERS[4] = {0, 3, 2, 1};
+
+ return LAYERS[RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::SampleRate() const {
+ // Sample rates - use [version][srate]
+ static const uint16_t SAMPLE_RATE[4][4] = {
+ // clang-format off
+ { 11025, 12000, 8000, 0 }, // MPEG 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 22050, 24000, 16000, 0 }, // MPEG 2
+ { 44100, 48000, 32000, 0 } // MPEG 1
+ // clang-format on
+ };
+
+ return SAMPLE_RATE[RawVersion()][RawSampleRate()];
+}
+
+uint32_t FrameParser::FrameHeader::Channels() const {
+ // 3 is single channel (mono), any other value is some variant of dual
+ // channel.
+ return RawChannelMode() == 3 ? 1 : 2;
+}
+
+uint32_t FrameParser::FrameHeader::SamplesPerFrame() const {
+ // Samples per frame - use [version][layer]
+ static const uint16_t FRAME_SAMPLE[4][4] = {
+ // clang-format off
+ // Layer 3 2 1 Version
+ { 0, 576, 1152, 384 }, // 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 0, 576, 1152, 384 }, // 2
+ { 0, 1152, 1152, 384 } // 1
+ // clang-format on
+ };
+
+ return FRAME_SAMPLE[RawVersion()][RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::Bitrate() const {
+ // Bitrates - use [version][layer][bitrate]
+ static const uint16_t BITRATE[4][4][16] = {
+ // clang-format off
+ { // Version 2.5
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Reserved
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // Invalid
+ },
+ { // Version 2
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Version 1
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }, // Layer 3
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, // Layer 2
+ { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, // Layer 1
+ }
+ // clang-format on
+ };
+
+ return 1000 * BITRATE[RawVersion()][RawLayer()][RawBitrate()];
+}
+
+uint32_t FrameParser::FrameHeader::SlotSize() const {
+ // Slot size (MPEG unit of measurement) - use [layer]
+ static const uint8_t SLOT_SIZE[4] = {0, 1, 1, 4}; // Rsvd, 3, 2, 1
+
+ return SLOT_SIZE[RawLayer()];
+}
+
+bool FrameParser::FrameHeader::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool FrameParser::ID3v1MetadataFound() const { return mID3v1MetadataFound; }
+
+bool FrameParser::FrameHeader::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ if (aPos == frame_header::SYNC1) {
+ return Sync1() == 0xFF;
+ }
+ if (aPos == frame_header::SYNC2_VERSION_LAYER_PROTECTION) {
+ return Sync2() == 7 && RawVersion() != 1 && Layer() == 3;
+ }
+ if (aPos == frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE) {
+ return RawBitrate() != 0xF && RawBitrate() != 0 && RawSampleRate() != 3;
+ }
+ return true;
+}
+
+bool FrameParser::FrameHeader::IsValid() const { return mPos >= SIZE; }
+
+void FrameParser::FrameHeader::Reset() { mPos = 0; }
+
+bool FrameParser::FrameHeader::Update(uint8_t c) {
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+// FrameParser::VBRHeader
+
+namespace vbr_header {
+static const char* TYPE_STR[3] = {"NONE", "XING", "VBRI"};
+static const uint32_t TOC_SIZE = 100;
+} // namespace vbr_header
+
+FrameParser::VBRHeader::VBRHeader() : mType(NONE) {}
+
+FrameParser::VBRHeader::VBRHeaderType FrameParser::VBRHeader::Type() const {
+ return mType;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumAudioFrames() const {
+ return mNumAudioFrames;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumBytes() const {
+ return mNumBytes;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::Scale() const { return mScale; }
+
+bool FrameParser::VBRHeader::IsTOCPresent() const {
+ // This doesn't use VBRI TOC
+ return !mTOC.empty() && mType != VBRI;
+}
+
+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
+ ;
+}
+
+int64_t FrameParser::VBRHeader::Offset(media::TimeUnit aTime,
+ media::TimeUnit aDuration) const {
+ if (!IsTOCPresent()) {
+ return -1;
+ }
+
+ int64_t offset = -1;
+ if (mType == XING) {
+ // Constrain the duration percentage to [0, 99].
+ double percent = 100. * aTime.ToSeconds() / aDuration.ToSeconds();
+ const double durationPer = std::clamp(percent, 0., 99.);
+ double integer;
+ const double fractional = modf(durationPer, &integer);
+ size_t integerPer = AssertedCast<size_t>(integer);
+
+ MOZ_ASSERT(integerPer < mTOC.size());
+ offset = mTOC.at(integerPer);
+ if (fractional > 0.0 && integerPer + 1 < mTOC.size()) {
+ offset += AssertedCast<int64_t>(fractional) *
+ (mTOC.at(integerPer + 1) - offset);
+ }
+ }
+ // TODO: VBRI TOC seeking
+ MP3LOG("VBRHeader::Offset (%s): %f is at byte %" PRId64 "",
+ mType == XING ? "XING" : "VBRI", aTime.ToSeconds(), offset);
+
+ return offset;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseXing(BufferReader* aReader,
+ size_t aFrameSize) {
+ static const uint32_t XING_TAG = BigEndian::readUint32("Xing");
+ static const uint32_t INFO_TAG = BigEndian::readUint32("Info");
+ static const uint32_t LAME_TAG = BigEndian::readUint32("LAME");
+ static const uint32_t LAVC_TAG = BigEndian::readUint32("Lavc");
+
+ enum Flags {
+ NUM_FRAMES = 0x01,
+ NUM_BYTES = 0x02,
+ TOC = 0x04,
+ VBR_SCALE = 0x08
+ };
+
+ MOZ_ASSERT(aReader);
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // We have to search for the Xing header as its position can change.
+ for (auto res = aReader->PeekU32();
+ res.isOk() && res.unwrap() != XING_TAG && res.unwrap() != INFO_TAG;) {
+ aReader->Read(1);
+ res = aReader->PeekU32();
+ }
+
+ // Skip across the VBR header ID tag.
+ MOZ_TRY(aReader->ReadU32());
+ mType = XING;
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, aReader->ReadU32());
+
+ if (flags & NUM_FRAMES) {
+ uint32_t frames;
+ MOZ_TRY_VAR(frames, aReader->ReadU32());
+ mNumAudioFrames = Some(frames);
+ }
+ if (flags & NUM_BYTES) {
+ uint32_t bytes;
+ MOZ_TRY_VAR(bytes, aReader->ReadU32());
+ mNumBytes = Some(bytes);
+ }
+ if (flags & TOC && aReader->Remaining() >= vbr_header::TOC_SIZE) {
+ if (!mNumBytes) {
+ // We don't have the stream size to calculate offsets, skip the TOC.
+ aReader->Read(vbr_header::TOC_SIZE);
+ } else {
+ mTOC.clear();
+ mTOC.reserve(vbr_header::TOC_SIZE);
+ uint8_t data;
+ for (size_t i = 0; i < vbr_header::TOC_SIZE; ++i) {
+ MOZ_TRY_VAR(data, aReader->ReadU8());
+ mTOC.push_back(
+ AssertedCast<uint32_t>(1.0f / 256.0f * data * mNumBytes.value()));
+ }
+ }
+ }
+ if (flags & VBR_SCALE) {
+ uint32_t scale;
+ MOZ_TRY_VAR(scale, aReader->ReadU32());
+ mScale = Some(scale);
+ }
+
+ uint32_t lameOrLavcTag;
+ MOZ_TRY_VAR(lameOrLavcTag, aReader->ReadU32());
+
+ if (lameOrLavcTag == LAME_TAG || lameOrLavcTag == LAVC_TAG) {
+ // Skip 17 bytes after the LAME tag:
+ // - http://gabriel.mp3-tech.org/mp3infotag.html
+ // - 5 bytes for the encoder short version string
+ // - 1 byte for the info tag revision + VBR method
+ // - 1 byte for the lowpass filter value
+ // - 8 bytes for the ReplayGain information
+ // - 1 byte for the encoding flags + ATH Type
+ // - 1 byte for the specified bitrate if ABR, else the minimal bitrate
+ if (!aReader->Read(17)) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ // The encoder delay is three bytes, for two 12-bits integers are the
+ // encoder delay and the padding.
+ const uint8_t* delayPadding = aReader->Read(3);
+ if (!delayPadding) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ mEncoderDelay =
+ uint32_t(delayPadding[0]) << 4 | (delayPadding[1] & 0xf0) >> 4;
+ mEncoderPadding = uint32_t(delayPadding[1] & 0x0f) << 8 | delayPadding[2];
+
+ constexpr uint16_t DEFAULT_DECODER_DELAY = 529;
+ mEncoderDelay += DEFAULT_DECODER_DELAY + aFrameSize; // ignore first frame.
+ mEncoderPadding -= std::min(mEncoderPadding, DEFAULT_DECODER_DELAY);
+
+ MP3LOG("VBRHeader::ParseXing: LAME encoder delay section: delay: %" PRIu16
+ " frames, padding: %" PRIu16 " frames",
+ mEncoderDelay, mEncoderPadding);
+ }
+
+ return mType == XING;
+}
+
+template <typename T>
+int readAndConvertToInt(BufferReader* aReader) {
+ int value = AssertedCast<int>(aReader->ReadType<T>());
+ return value;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseVBRI(
+ BufferReader* aReader) {
+ static const uint32_t TAG = BigEndian::readUint32("VBRI");
+ static const uint32_t OFFSET = 32 + FrameParser::FrameHeader::SIZE;
+ static const uint32_t MIN_FRAME_SIZE = OFFSET + 26;
+
+ 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.
+ auto sync = aReader->PeekU16();
+ if (sync.isOk()) { // To avoid compiler complains 'set but unused'.
+ MOZ_ASSERT((sync.unwrap() & 0xFFE0) == 0xFFE0);
+ }
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // VBRI have a fixed relative position, so let's check for it there.
+ if (aReader->Remaining() > MIN_FRAME_SIZE) {
+ aReader->Seek(prevReaderOffset + OFFSET);
+ uint32_t tag;
+ MOZ_TRY_VAR(tag, aReader->ReadU32());
+ if (tag == TAG) {
+ uint16_t vbriEncoderVersion, vbriEncoderDelay, vbriQuality;
+ uint32_t vbriBytes, vbriFrames;
+ uint16_t vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry;
+ MOZ_TRY_VAR(vbriEncoderVersion, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriEncoderDelay, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriQuality, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriBytes, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriFrames, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsTableSize, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsScaleFactor, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsBytesPerEntry, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsFramesPerEntry, aReader->ReadU16());
+
+ mTOC.reserve(vbriSeekOffsetsTableSize + 1);
+
+ int (*readFunc)(BufferReader*) = nullptr;
+ switch (vbriSeekOffsetsBytesPerEntry) {
+ case 1:
+ readFunc = &readAndConvertToInt<uint8_t>;
+ break;
+ case 2:
+ readFunc = &readAndConvertToInt<int16_t>;
+ break;
+ case 4:
+ readFunc = &readAndConvertToInt<int32_t>;
+ break;
+ case 8:
+ readFunc = &readAndConvertToInt<int64_t>;
+ break;
+ default:
+ MP3LOG("Unhandled vbriSeekOffsetsBytesPerEntry size of %hd",
+ vbriSeekOffsetsBytesPerEntry);
+ break;
+ }
+ for (uint32_t i = 0; readFunc && i < vbriSeekOffsetsTableSize; i++) {
+ int entry = readFunc(aReader);
+ mTOC.push_back(entry * vbriSeekOffsetsScaleFactor);
+ }
+ MP3LOG(
+ "Header::Parse found valid header: EncoderVersion=%hu "
+ "EncoderDelay=%hu "
+ "Quality=%hu "
+ "Bytes=%u "
+ "Frames=%u "
+ "SeekOffsetsTableSize=%u "
+ "SeekOffsetsScaleFactor=%hu "
+ "SeekOffsetsBytesPerEntry=%hu "
+ "SeekOffsetsFramesPerEntry=%hu",
+ vbriEncoderVersion, vbriEncoderDelay, vbriQuality, vbriBytes,
+ vbriFrames, vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry);
+ // Adjust the number of frames so it's counted the same way as in the XING
+ // header
+ if (vbriFrames < 1) {
+ return false;
+ }
+ mNumAudioFrames = Some(vbriFrames - 1);
+ mNumBytes = Some(vbriBytes);
+ mEncoderDelay = vbriEncoderDelay;
+ mVBRISeekOffsetsFramesPerEntry = vbriSeekOffsetsFramesPerEntry;
+ MP3LOG("TOC:");
+ for (auto entry : mTOC) {
+ MP3LOG("%" PRId64, entry);
+ }
+
+ mType = VBRI;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool FrameParser::VBRHeader::Parse(BufferReader* aReader, size_t aFrameSize) {
+ auto res = std::make_pair(ParseVBRI(aReader), ParseXing(aReader, aFrameSize));
+ const bool rv = (res.first.isOk() && res.first.unwrap()) ||
+ (res.second.isOk() && res.second.unwrap());
+ if (rv) {
+ MP3LOG(
+ "VBRHeader::Parse found valid VBR/CBR header: type=%s"
+ " NumAudioFrames=%u NumBytes=%u Scale=%u TOC-size=%zu Delay=%u",
+ vbr_header::TYPE_STR[Type()], NumAudioFrames().valueOr(0),
+ NumBytes().valueOr(0), Scale().valueOr(0), mTOC.size(), mEncoderDelay);
+ }
+ return rv;
+}
+
+// FrameParser::Frame
+
+void FrameParser::Frame::Reset() { mHeader.Reset(); }
+
+uint32_t FrameParser::Frame::Length() const {
+ if (!mHeader.IsValid() || !mHeader.SampleRate()) {
+ return 0;
+ }
+
+ const uint32_t bitsPerSample = mHeader.SamplesPerFrame() / 8;
+ const uint32_t frameLen =
+ bitsPerSample * mHeader.Bitrate() / mHeader.SampleRate() +
+ mHeader.Padding() * mHeader.SlotSize();
+ return frameLen;
+}
+
+bool FrameParser::Frame::ParseNext(uint8_t c) { return mHeader.ParseNext(c); }
+
+const FrameParser::FrameHeader& FrameParser::Frame::Header() const {
+ return mHeader;
+}
+
+bool FrameParser::ParseVBRHeader(BufferReader* aReader) {
+ return mVBRHeader.Parse(aReader, CurrentFrame().Header().SamplesPerFrame());
+}
+
+// ID3Parser
+
+// Constants
+namespace id3_header {
+static const int ID_LEN = 3;
+static const int VERSION_LEN = 2;
+static const int FLAGS_LEN = 1;
+static const int SIZE_LEN = 4;
+
+static const int ID_END = ID_LEN;
+static const int VERSION_END = ID_END + VERSION_LEN;
+static const int FLAGS_END = VERSION_END + FLAGS_LEN;
+static const int SIZE_END = FLAGS_END + SIZE_LEN;
+
+static const uint8_t ID[ID_LEN] = {'I', 'D', '3'};
+static const uint8_t IDv1[ID_LEN] = {'T', 'A', 'G'};
+
+static const uint8_t MIN_MAJOR_VER = 2;
+static const uint8_t MAX_MAJOR_VER = 4;
+} // namespace id3_header
+
+bool ID3Parser::IsBufferStartingWithID3v1Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v1 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::IDv1[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+/* static */
+bool ID3Parser::IsBufferStartingWithID3Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v2 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::ID[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+uint32_t ID3Parser::Parse(BufferReader* aReader) {
+ MOZ_ASSERT(aReader);
+ MOZ_ASSERT(ID3Parser::IsBufferStartingWithID3Tag(aReader));
+
+ if (!mHeader.HasSizeBeenSet()) {
+ return ParseInternal(aReader);
+ }
+
+ // Encounter another possible ID3 header, if that is valid then we would use
+ // it and save the size of previous one in order to report the size of all ID3
+ // headers together in `TotalHeadersSize()`.
+ ID3Header prevHeader = mHeader;
+ mHeader.Reset();
+ uint32_t size = ParseInternal(aReader);
+ if (!size) {
+ // next ID3 is invalid, so revert the header.
+ mHeader = prevHeader;
+ return size;
+ }
+
+ mFormerID3Size += prevHeader.TotalTagSize();
+ return size;
+}
+
+uint32_t ID3Parser::ParseInternal(BufferReader* aReader) {
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mHeader.ParseNext(res.unwrap());
+ res = aReader->ReadU8()) {
+ }
+ return mHeader.TotalTagSize();
+}
+
+void ID3Parser::Reset() {
+ mHeader.Reset();
+ mFormerID3Size = 0;
+}
+
+uint32_t ID3Parser::TotalHeadersSize() const {
+ return mHeader.TotalTagSize() + mFormerID3Size;
+}
+
+const ID3Parser::ID3Header& ID3Parser::Header() const { return mHeader; }
+
+// ID3Parser::Header
+
+ID3Parser::ID3Header::ID3Header() { Reset(); }
+
+void ID3Parser::ID3Header::Reset() {
+ mSize.reset();
+ mPos = 0;
+}
+
+uint8_t ID3Parser::ID3Header::MajorVersion() const {
+ return mRaw[id3_header::ID_END];
+}
+
+uint8_t ID3Parser::ID3Header::MinorVersion() const {
+ return mRaw[id3_header::ID_END + 1];
+}
+
+uint8_t ID3Parser::ID3Header::Flags() const {
+ return mRaw[id3_header::FLAGS_END - id3_header::FLAGS_LEN];
+}
+
+uint32_t ID3Parser::ID3Header::Size() const {
+ if (!IsValid() || !mSize) {
+ return 0;
+ }
+ return *mSize;
+}
+
+bool ID3Parser::ID3Header::HasSizeBeenSet() const { return !!mSize; }
+
+uint8_t ID3Parser::ID3Header::FooterSize() const {
+ if (Flags() & (1 << 4)) {
+ return SIZE;
+ }
+ return 0;
+}
+
+uint32_t ID3Parser::ID3Header::TotalTagSize() const {
+ if (IsValid()) {
+ // Header found, return total tag size.
+ return ID3Header::SIZE + Size() + FooterSize();
+ }
+ return 0;
+}
+
+bool ID3Parser::ID3Header::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool ID3Parser::ID3Header::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ const uint8_t c = mRaw[aPos];
+ switch (aPos) {
+ case 0:
+ case 1:
+ case 2:
+ // Expecting "ID3".
+ return id3_header::ID[aPos] == c;
+ case 3:
+ return MajorVersion() >= id3_header::MIN_MAJOR_VER &&
+ MajorVersion() <= id3_header::MAX_MAJOR_VER;
+ case 4:
+ return MinorVersion() < 0xFF;
+ case 5:
+ // Validate flags for supported versions, see bug 949036.
+ return ((0xFF >> MajorVersion()) & c) == 0;
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ return c < 0x80;
+ }
+ return true;
+}
+
+bool ID3Parser::ID3Header::IsValid() const { return mPos >= SIZE; }
+
+bool ID3Parser::ID3Header::Update(uint8_t c) {
+ if (mPos >= id3_header::SIZE_END - id3_header::SIZE_LEN &&
+ mPos < id3_header::SIZE_END) {
+ uint32_t tmp = mSize.valueOr(0) << 7;
+ mSize = Some(tmp | c);
+ }
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp3/MP3FrameParser.h b/dom/media/mp3/MP3FrameParser.h
new file mode 100644
index 0000000000..d0d7a372d2
--- /dev/null
+++ b/dom/media/mp3/MP3FrameParser.h
@@ -0,0 +1,374 @@
+/* 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/. */
+
+#ifndef MP3_FRAME_PARSER_H_
+#define MP3_FRAME_PARSER_H_
+
+#include <vector>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "BufferReader.h"
+
+namespace mozilla {
+
+// ID3 header parser state machine used by FrameParser.
+// The header contains the following format (one byte per term):
+// 'I' 'D' '3' MajorVersion MinorVersion Flags Size1 Size2 Size3 Size4
+// For more details see https://id3.org/id3v2.4.0-structure
+class ID3Parser {
+ public:
+ // Holds the ID3 header and its parsing state.
+ class ID3Header {
+ public:
+ // The header size is static, see class comment.
+ static const int SIZE = 10;
+ static const int ID3v1_SIZE = 128;
+
+ // Constructor.
+ ID3Header();
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ // The ID3 tags are versioned like this: ID3vMajorVersion.MinorVersion.
+ uint8_t MajorVersion() const;
+ uint8_t MinorVersion() const;
+
+ // The ID3 flags field.
+ uint8_t Flags() const;
+
+ // The derived size based on the provided size fields.
+ uint32_t Size() const;
+
+ // To see whether we have parsed the value of the size from header.
+ bool HasSizeBeenSet() const;
+
+ // Returns the size of an ID3v2.4 footer if present and zero otherwise.
+ uint8_t FooterSize() const;
+
+ // The total size of the ID3 tag including header/footer, or zero if
+ // none has been found.
+ uint32_t TotalTagSize() const;
+
+ // Returns whether the parsed data is a valid ID3 header up to the given
+ // byte position.
+ bool IsValid(int aPos) const;
+
+ // Returns whether the parsed data is a complete and valid ID3 header.
+ bool IsValid() const;
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(uint8_t c);
+
+ private:
+ // Updates the parser state machine with the provided next byte.
+ // Returns whether the provided byte is a valid next byte in the sequence.
+ bool Update(uint8_t c);
+
+ // The currently parsed byte sequence.
+ uint8_t mRaw[SIZE] = {};
+
+ // The derived size as provided by the size fields.
+ // The header size fields holds a 4 byte sequence with each MSB set to 0,
+ // this bits need to be ignored when deriving the actual size.
+ Maybe<uint32_t> mSize;
+
+ // The current byte position in the parsed sequence. Reset via Reset and
+ // incremented via Update.
+ int mPos = 0;
+ };
+
+ // Check if the buffer is starting with ID3v2 tag.
+ static bool IsBufferStartingWithID3Tag(BufferReader* aReader);
+ // Similarly, if the buffer is starting with ID3v1 tag.
+ static bool IsBufferStartingWithID3v1Tag(BufferReader* aReader);
+
+ // Returns the parsed ID3 header. Note: check for validity.
+ const ID3Header& Header() const;
+
+ // Returns the size of all parsed ID3 headers.
+ uint32_t TotalHeadersSize() const;
+
+ // Parses contents of given BufferReader for a valid ID3v2 header.
+ // Returns the parsed ID3v2 tag size if successful and zero otherwise.
+ uint32_t Parse(BufferReader* aReader);
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ private:
+ uint32_t ParseInternal(BufferReader* aReader);
+
+ // The currently parsed ID3 header. Reset via Reset, updated via Parse.
+ ID3Header mHeader;
+ // If a file contains multiple ID3 headers, then we would only select the
+ // latest one, but keep the size of former abandoned in order to return the
+ // correct size offset.
+ uint32_t mFormerID3Size = 0;
+};
+
+// MPEG audio frame parser.
+// The MPEG frame header has the following format (one bit per character):
+// 11111111 111VVLLC BBBBSSPR MMEETOHH
+// { sync } - 11 sync bits
+// VV - MPEG audio version ID (0->2.5, 1->reserved, 2->2, 3->1)
+// LL - Layer description (0->reserved, 1->III, 2->II, 3->I)
+// C - CRC protection bit (0->protected, 1->not protected)
+// BBBB - Bitrate index (see table in implementation)
+// SS - Sampling rate index (see table in implementation)
+// P - Padding bit (0->not padded, 1->padded by 1 slot size)
+// R - Private bit (ignored)
+// MM - Channel mode (0->stereo, 1->joint stereo, 2->dual channel,
+// 3->single channel)
+// EE - Mode extension for joint stereo (ignored)
+// T - Copyright (0->disabled, 1->enabled)
+// O - Original (0->copy, 1->original)
+// HH - Emphasis (0->none, 1->50/15 ms, 2->reserved, 3->CCIT J.17)
+class FrameParser {
+ public:
+ // Holds the frame header and its parsing state.
+ class FrameHeader {
+ public:
+ // The header size is static, see class comments.
+ static const int SIZE = 4;
+
+ // Constructor.
+ FrameHeader();
+
+ // Raw field access, see class comments for details.
+ uint8_t Sync1() const;
+ uint8_t Sync2() const;
+ uint8_t RawVersion() const;
+ uint8_t RawLayer() const;
+ uint8_t RawProtection() const;
+ uint8_t RawBitrate() const;
+ uint8_t RawSampleRate() const;
+ uint8_t Padding() const;
+ uint8_t Private() const;
+ uint8_t RawChannelMode() const;
+
+ // Sampling rate frequency in Hz.
+ uint32_t SampleRate() const;
+
+ // Number of audio channels.
+ uint32_t Channels() const;
+
+ // Samples per frames, static depending on MPEG version and layer.
+ uint32_t SamplesPerFrame() const;
+
+ // Slot size used for padding, static depending on MPEG layer.
+ uint32_t SlotSize() const;
+
+ // Bitrate in kbps, can vary between frames.
+ uint32_t Bitrate() const;
+
+ // MPEG layer (0->invalid, 1->I, 2->II, 3->III).
+ uint32_t Layer() const;
+
+ // Returns whether the parsed data is a valid frame header up to the given
+ // byte position.
+ bool IsValid(const int aPos) const;
+
+ // Returns whether the parsed data is a complete and valid frame header.
+ bool IsValid() const;
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(const uint8_t c);
+
+ private:
+ // Updates the parser state machine with the provided next byte.
+ // Returns whether the provided byte is a valid next byte in the sequence.
+ bool Update(const uint8_t c);
+
+ // The currently parsed byte sequence.
+ uint8_t mRaw[SIZE] = {};
+
+ // The current byte position in the parsed sequence. Reset via Reset and
+ // incremented via Update.
+ int mPos = 0;
+ };
+
+ // VBR frames may contain Xing or VBRI headers for additional info, we use
+ // this class to parse them and access this info.
+ class VBRHeader {
+ public:
+ // Synchronize with vbr_header TYPE_STR on change.
+ enum VBRHeaderType { NONE = 0, XING, VBRI };
+
+ // Constructor.
+ VBRHeader();
+
+ // Returns the parsed VBR header type, or NONE if no valid header found.
+ VBRHeaderType Type() const;
+
+ // Returns the total number of audio frames (excluding the VBR header frame)
+ // expected in the stream/file.
+ const Maybe<uint32_t>& NumAudioFrames() const;
+
+ // Returns the expected size of the stream.
+ const Maybe<uint32_t>& NumBytes() const;
+
+ // Returns the VBR scale factor (0: best quality, 100: lowest quality).
+ const Maybe<uint32_t>& Scale() const;
+
+ // 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(media::TimeUnit aTime, media::TimeUnit aDuration) const;
+
+ // Parses contents of given ByteReader for a valid VBR header.
+ // The offset of the passed ByteReader needs to point to an MPEG frame
+ // begin, as a VBRI-style header is searched at a fixed offset relative to
+ // frame begin. Returns whether a valid VBR header was found in the range.
+ bool Parse(BufferReader* aReader, size_t aFrameSize);
+
+ uint32_t EncoderDelay() const { return mEncoderDelay; }
+ uint32_t EncoderPadding() const { return mEncoderPadding; }
+
+ private:
+ // Parses contents of given ByteReader for a valid Xing header.
+ // The initial ByteReader offset will be preserved.
+ // Returns whether a valid Xing header was found in the range.
+ Result<bool, nsresult> ParseXing(BufferReader* aReader, size_t aFrameSize);
+
+ // Parses contents of given ByteReader for a valid VBRI header.
+ // The initial ByteReader offset will be preserved. It also needs to point
+ // to the beginning of a valid MPEG frame, as VBRI headers are searched
+ // at a fixed offset relative to frame begin.
+ // Returns whether a valid VBRI header was found in the range.
+ Result<bool, nsresult> ParseVBRI(BufferReader* aReader);
+
+ // The total number of frames expected as parsed from a VBR header.
+ Maybe<uint32_t> mNumAudioFrames;
+
+ // The total number of bytes expected in the stream.
+ Maybe<uint32_t> mNumBytes;
+
+ // The VBR scale factor.
+ Maybe<uint32_t> mScale;
+
+ // The TOC table mapping duration percentage to byte offset.
+ std::vector<int64_t> mTOC;
+
+ // The detected VBR header type.
+ VBRHeaderType mType;
+
+ uint16_t mVBRISeekOffsetsFramesPerEntry = 0;
+
+ // Delay and padding values found in the LAME header. The encoder delay is a
+ // number of frames that has to be skipped at the beginning of the stream,
+ // encoder padding is a number of frames that needs to be ignored in the
+ // last packet.
+ uint16_t mEncoderDelay = 0;
+ uint16_t mEncoderPadding = 0;
+ };
+
+ // Frame meta container used to parse and hold a frame header and side info.
+ class Frame {
+ public:
+ // Returns the length of the frame excluding the header in bytes.
+ uint32_t Length() const;
+
+ // Returns the parsed frame header.
+ const FrameHeader& Header() const;
+
+ // Resets the frame header and data.
+ void Reset();
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(uint8_t c);
+
+ private:
+ // The currently parsed frame header.
+ FrameHeader mHeader;
+ };
+
+ // Constructor.
+ FrameParser();
+
+ // Returns the currently parsed frame. Reset via Reset or EndFrameSession.
+ const Frame& CurrentFrame() const;
+
+ // Returns the previously parsed frame. Reset via Reset.
+ const Frame& PrevFrame() const;
+
+ // Returns the first parsed frame. Reset via Reset.
+ const Frame& FirstFrame() const;
+
+ // Returns the parsed ID3 header. Note: check for validity.
+ const ID3Parser::ID3Header& ID3Header() const;
+
+ // Returns whether ID3 metadata have been found, at the end of the file.
+ bool ID3v1MetadataFound() const;
+
+ // Returns the size of all parsed ID3 headers.
+ uint32_t TotalID3HeaderSize() const;
+
+ // Returns the parsed VBR header info. Note: check for validity by type.
+ const VBRHeader& VBRInfo() const;
+
+ // 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
+ // - resets ID3Header if no valid header was parsed yet
+ void EndFrameSession();
+
+ // Parses contents of given BufferReader for a valid frame header and returns
+ // true if one was found. After returning, the variable passed to
+ // 'aBytesToSkip' holds the amount of bytes to be skipped (if any) in order to
+ // jump across a large ID3v2 tag spanning multiple buffers.
+ Result<bool, nsresult> Parse(BufferReader* aReader, uint32_t* aBytesToSkip);
+
+ // Parses contents of given BufferReader for a valid VBR header.
+ // The offset of the passed BufferReader needs to point to an MPEG frame
+ // begin, as a VBRI-style header is searched at a fixed offset relative to
+ // frame begin. Returns whether a valid VBR header was found.
+ bool ParseVBRHeader(BufferReader* aReader);
+
+ private:
+ // ID3 header parser.
+ ID3Parser mID3Parser;
+
+ // VBR header parser.
+ VBRHeader mVBRHeader;
+
+ // We keep the first parsed frame around for static info access, the
+ // previously parsed frame for debugging and the currently parsed frame.
+ Frame mFirstFrame;
+ Frame mFrame;
+ Frame mPrevFrame;
+ // If this is true, ID3v1 metadata have been found at the end of the file, and
+ // must be sustracted from the stream size in order to compute the stream
+ // duration, when computing the duration of a CBR file based on its length in
+ // bytes. This means that the duration can change at the moment we reach the
+ // end of the file.
+ bool mID3v1MetadataFound = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/moz.build b/dom/media/mp3/moz.build
new file mode 100644
index 0000000000..70031cc2b4
--- /dev/null
+++ b/dom/media/mp3/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+EXPORTS += [
+ "MP3Decoder.h",
+ "MP3Demuxer.h",
+ "MP3FrameParser.h",
+]
+
+UNIFIED_SOURCES += [
+ "MP3Decoder.cpp",
+ "MP3Demuxer.cpp",
+ "MP3FrameParser.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")