diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:47:29 +0000 |
commit | 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch) | |
tree | a31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media/flac | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream/115.8.0esr.tar.xz firefox-esr-upstream/115.8.0esr.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/flac/FlacDecoder.cpp | 45 | ||||
-rw-r--r-- | dom/media/flac/FlacDecoder.h | 30 | ||||
-rw-r--r-- | dom/media/flac/FlacDemuxer.cpp | 1027 | ||||
-rw-r--r-- | dom/media/flac/FlacDemuxer.h | 112 | ||||
-rw-r--r-- | dom/media/flac/FlacFrameParser.cpp | 244 | ||||
-rw-r--r-- | dom/media/flac/FlacFrameParser.h | 72 | ||||
-rw-r--r-- | dom/media/flac/moz.build | 24 |
7 files changed, 1554 insertions, 0 deletions
diff --git a/dom/media/flac/FlacDecoder.cpp b/dom/media/flac/FlacDecoder.cpp new file mode 100644 index 0000000000..2f205c9aae --- /dev/null +++ b/dom/media/flac/FlacDecoder.cpp @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "FlacDecoder.h" +#include "MediaContainerType.h" +#include "mozilla/StaticPrefs_media.h" + +namespace mozilla { + +/* static */ +bool FlacDecoder::IsEnabled() { +#ifdef MOZ_FFVPX + return StaticPrefs::media_flac_enabled(); +#else + return false; +#endif +} + +/* static */ +bool FlacDecoder::IsSupportedType(const MediaContainerType& aContainerType) { + return IsEnabled() && + (aContainerType.Type() == MEDIAMIMETYPE("audio/flac") || + aContainerType.Type() == MEDIAMIMETYPE("audio/x-flac") || + aContainerType.Type() == MEDIAMIMETYPE("application/x-flac")); +} + +/* static */ +nsTArray<UniquePtr<TrackInfo>> FlacDecoder::GetTracksInfo( + const MediaContainerType& aType) { + nsTArray<UniquePtr<TrackInfo>> tracks; + if (!IsSupportedType(aType)) { + return tracks; + } + + tracks.AppendElement( + CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters( + "audio/flac"_ns, aType)); + + return tracks; +} + +} // namespace mozilla diff --git a/dom/media/flac/FlacDecoder.h b/dom/media/flac/FlacDecoder.h new file mode 100644 index 0000000000..f40ae6c31a --- /dev/null +++ b/dom/media/flac/FlacDecoder.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 FLAC_DECODER_H_ +#define FLAC_DECODER_H_ + +#include "mozilla/UniquePtr.h" +#include "nsTArray.h" + +namespace mozilla { + +class MediaContainerType; +class TrackInfo; + +class FlacDecoder { + public: + // Returns true if the Flac backend is pref'ed 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 // !FLAC_DECODER_H_ diff --git a/dom/media/flac/FlacDemuxer.cpp b/dom/media/flac/FlacDemuxer.cpp new file mode 100644 index 0000000000..fd6eea13c2 --- /dev/null +++ b/dom/media/flac/FlacDemuxer.cpp @@ -0,0 +1,1027 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "FlacDemuxer.h" + +#include "mozilla/Maybe.h" +#include "BitReader.h" +#include "prenv.h" +#include "FlacFrameParser.h" +#include "VideoUtils.h" +#include "TimeUnits.h" + +extern mozilla::LazyLogModule gMediaDemuxerLog; +#define LOG(msg, ...) \ + DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, msg, ##__VA_ARGS__) +#define LOGV(msg, ...) \ + DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, msg, ##__VA_ARGS__) + +using namespace mozilla::media; + +namespace mozilla { +namespace flac { + +// flac::FrameHeader - Holds the flac frame header and its parsing +// state. + +class FrameHeader { + public: + const AudioInfo& Info() const { return mInfo; } + + uint32_t Size() const { return mSize; } + + bool IsValid() const { return mValid; } + + // Return the index (in samples) from the beginning of the track. + int64_t Index() const { return mIndex; } + + // Parse the current packet and check that it made a valid flac frame header. + // From https://xiph.org/flac/format.html#frame_header + // A valid header is one that can be decoded without error and that has a + // valid CRC. + bool Parse(const uint8_t* aPacket, size_t aBytes) { + BitReader br(aPacket, aBytes * 8); + + // Frame sync code. + if ((br.ReadBits(15) & 0x7fff) != 0x7ffc) { + return false; + } + + // Variable block size stream code. + mVariableBlockSize = br.ReadBit(); + + // Block size and sample rate codes. + int bs_code = br.ReadBits(4); + int sr_code = br.ReadBits(4); + + // Channels and decorrelation. + uint32_t ch_mode = br.ReadBits(4); + if (ch_mode < FLAC_MAX_CHANNELS) { + mInfo.mChannels = ch_mode + 1; + } else if (ch_mode < FLAC_MAX_CHANNELS + FLAC_CHMODE_MID_SIDE) { + // This is a special flac channels, we can't handle those yet. Treat it + // as stereo. + mInfo.mChannels = 2; + } else { + // invalid channel mode + return false; + } + + // Bits per sample. + int bps_code = br.ReadBits(3); + if (bps_code == 3 || bps_code == 7) { + // Invalid sample size code. + return false; + } + mInfo.mBitDepth = FlacSampleSizeTable[bps_code]; + + // Reserved bit, must be 0. + if (br.ReadBit()) { + // Broken stream, invalid padding. + return false; + } + + // Sample or frame count. + uint64_t frame_or_sample_num = br.ReadUTF8(); + if (frame_or_sample_num == UINT64_MAX) { + // Sample/frame number invalid. + return false; + } + + // Blocksize + if (bs_code == 0) { + // reserved blocksize code + return false; + } else if (bs_code == 6) { + mBlocksize = br.ReadBits(8) + 1; + } else if (bs_code == 7) { + mBlocksize = br.ReadBits(16) + 1; + } else { + mBlocksize = FlacBlocksizeTable[bs_code]; + } + + // The sample index is either: + // 1- coded sample number if blocksize is variable or + // 2- coded frame number if blocksize is known. + // A frame is made of Blocksize sample. + mIndex = mVariableBlockSize ? frame_or_sample_num + : frame_or_sample_num * mBlocksize; + mFrameOrSampleNum = static_cast<uint64_t>(frame_or_sample_num); + + // Sample rate. + if (sr_code < 12) { + mInfo.mRate = FlacSampleRateTable[sr_code]; + } else if (sr_code == 12) { + mInfo.mRate = br.ReadBits(8) * 1000; + } else if (sr_code == 13) { + mInfo.mRate = br.ReadBits(16); + } else if (sr_code == 14) { + mInfo.mRate = br.ReadBits(16) * 10; + } else { + // Illegal sample rate code. + return false; + } + + // Header CRC-8 check. + uint8_t crc = 0; + for (uint32_t i = 0; i < br.BitCount() / 8; i++) { + crc = CRC8Table[crc ^ aPacket[i]]; + } + mValid = +#ifdef FUZZING + true; +#else + crc == br.ReadBits(8); +#endif + mSize = br.BitCount() / 8; + + if (mValid) { + // Set the mimetype to make it a valid AudioInfo. + mInfo.mMimeType = "audio/flac"; + // Set the codec specific data to flac, but leave it empty since we don't + // have METADATA_BLOCK_STREAMINFO in the frame. + mInfo.mCodecSpecificConfig = + AudioCodecSpecificVariant{FlacCodecSpecificData{}}; + } + + return mValid; + } + + private: + friend class Frame; + enum { + FLAC_CHMODE_INDEPENDENT = 0, + FLAC_CHMODE_LEFT_SIDE, + FLAC_CHMODE_RIGHT_SIDE, + FLAC_CHMODE_MID_SIDE, + }; + AudioInfo mInfo; + // mFrameOrSampleNum is either: + // 1- coded sample number if blocksize is variable or + // 2- coded frame number if blocksize is fixed. + // A frame is made of Blocksize sample. + uint64_t mFrameOrSampleNum = 0; + // Index in samples from start; + int64_t mIndex = 0; + bool mVariableBlockSize = false; + uint32_t mBlocksize = 0; + uint32_t mSize = 0; + bool mValid = false; + + static const uint32_t FlacSampleRateTable[16]; + static const uint32_t FlacBlocksizeTable[16]; + static const uint8_t FlacSampleSizeTable[8]; + static const uint8_t CRC8Table[256]; +}; + +const uint32_t FrameHeader::FlacSampleRateTable[16] = { + 0, 88200, 176400, 192000, 8000, 16000, 22050, 24000, + 32000, 44100, 48000, 96000, 0, 0, 0, 0}; + +const uint32_t FrameHeader::FlacBlocksizeTable[16] = { + 0, 192, 576 << 0, 576 << 1, 576 << 2, 576 << 3, + 0, 0, 256 << 0, 256 << 1, 256 << 2, 256 << 3, + 256 << 4, 256 << 5, 256 << 6, 256 << 7}; + +const uint8_t FrameHeader::FlacSampleSizeTable[8] = {0, 8, 12, 0, + 16, 20, 24, 0}; + +const uint8_t FrameHeader::CRC8Table[256] = { + 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31, + 0x24, 0x23, 0x2A, 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65, + 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9, + 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD, + 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1, + 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2, + 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE, + 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A, + 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16, + 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42, + 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80, + 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4, + 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8, + 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C, + 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10, + 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34, + 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F, + 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B, + 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, 0xA9, 0xA0, 0xA7, + 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83, + 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, + 0xFA, 0xFD, 0xF4, 0xF3}; + +// flac::Frame - Frame meta container used to parse and hold a frame +// header and side info. +class Frame { + public: + // The FLAC signature is made of 14 bits set to 1; however the 15th bit is + // mandatorily set to 0, so we need to find either of 0xfffc or 0xfffd 2-bytes + // signature. We first use a bitmask to see if 0xfc or 0xfd is present. And if + // so we check for the whole signature. + int64_t FindNext(const uint8_t* aData, const uint32_t aLength) { + // The non-variable size of a FLAC header is 32 bits followed by variable + // size data and a 8 bits CRC. + // There's no need to read the last 4 bytes, it can never make a complete + // header. + if (aLength < 4) { + return -1; + } + uint32_t modOffset = aLength % 4; + uint32_t i, j; + + for (i = 0; i < modOffset; i++) { + if ((BigEndian::readUint16(aData + i) & 0xfffe) == 0xfff8) { + if (mHeader.Parse(aData + i, aLength - i)) { + return i; + } + } + } + + for (; i < aLength - 4; i += 4) { + uint32_t x = BigEndian::readUint32(aData + i); + if (((x & ~(x + 0x01010101)) & 0x80808080)) { + for (j = 0; j < 4; j++) { + if ((BigEndian::readUint16(aData + i + j) & 0xfffe) == 0xfff8) { + if (mHeader.Parse(aData + i + j, aLength - i - j)) { + return i + j; + } + } + } + } + } + return -1; + } + + // Find the next frame start in the current resource. + // On exit return true, offset is set and resource points to the frame found. + bool FindNext(MediaResourceIndex& aResource) { + static const int BUFFER_SIZE = 4096; + + Reset(); + + nsTArray<char> buffer; + uint64_t originalOffset = static_cast<uint64_t>(aResource.Tell()); + uint64_t offset = originalOffset; + uint32_t innerOffset = 0; + + do { + uint32_t read = 0; + buffer.SetLength(BUFFER_SIZE + innerOffset); + nsresult rv = + aResource.Read(buffer.Elements() + innerOffset, BUFFER_SIZE, &read); + if (NS_FAILED(rv)) { + return false; + } + + const size_t bufSize = read + innerOffset; + int64_t foundOffset = + FindNext(reinterpret_cast<uint8_t*>(buffer.Elements()), bufSize); + + if (foundOffset >= 0) { + SetOffset(aResource, static_cast<uint64_t>(foundOffset) + offset); + return true; + } + + if (read < BUFFER_SIZE) { + // Nothing more to try on as we had reached EOS during the previous + // read. + mEOS = true; + return false; + } + + // Scan the next block; + // We rewind a bit to re-try what could have been an incomplete packet. + // The maximum size of a FLAC header being FLAC_MAX_FRAME_HEADER_SIZE so + // we need to retry just after that amount. + offset += bufSize - (FLAC_MAX_FRAME_HEADER_SIZE + 1); + buffer.RemoveElementsAt(0, bufSize - (FLAC_MAX_FRAME_HEADER_SIZE + 1)); + innerOffset = buffer.Length(); + } while (offset - originalOffset < FLAC_MAX_FRAME_SIZE); + + return false; + } + + uint64_t Offset() const { return mOffset; } + + const AudioInfo& Info() const { return Header().Info(); } + + void SetEndOffset(uint64_t aOffset) { mSize = aOffset - mOffset; } + + void SetEndTime(int64_t aIndex) { + if (aIndex > Header().mIndex) { + mDuration = aIndex - Header().mIndex; + } + } + + void ResetStartTimeIfNeeded(const Frame& aReferenceFrame) { + if (Header().mVariableBlockSize || + aReferenceFrame.Header().mVariableBlockSize || + aReferenceFrame.Header().mBlocksize <= Header().mBlocksize) { + // Not a fixed size frame, or nothing to adjust. + return; + } + mHeader.mIndex = + Header().mFrameOrSampleNum * aReferenceFrame.Header().mBlocksize; + } + + uint32_t Size() const { return mSize; } + + TimeUnit Time() const { + if (!IsValid()) { + return TimeUnit::Invalid(); + } + MOZ_ASSERT(Header().Info().mRate, "Invalid Frame. Need Header"); + return media::TimeUnit(Header().mIndex, Header().Info().mRate); + } + + TimeUnit Duration() const { + if (!IsValid()) { + return TimeUnit(); + } + MOZ_ASSERT(Header().Info().mRate, "Invalid Frame. Need Header"); + return media::TimeUnit(mDuration, Header().Info().mRate); + } + + // Returns the parsed frame header. + const FrameHeader& Header() const { return mHeader; } + + bool IsValid() const { return mHeader.IsValid(); } + + bool EOS() const { return mEOS; } + + void SetRate(uint32_t aRate) { mHeader.mInfo.mRate = aRate; }; + + void SetBitDepth(uint32_t aBitDepth) { mHeader.mInfo.mBitDepth = aBitDepth; } + + void SetInvalid() { mHeader.mValid = false; } + + // Resets the frame header and data. + void Reset() { *this = Frame(); } + + private: + void SetOffset(MediaResourceIndex& aResource, uint64_t aOffset) { + mOffset = aOffset; + aResource.Seek(SEEK_SET, mOffset); + } + + // The offset to the start of the header. + uint64_t mOffset = 0; + uint32_t mSize = 0; + uint32_t mDuration = 0; + bool mEOS = false; + + // The currently parsed frame header. + FrameHeader mHeader; +}; + +class FrameParser { + public: + // Returns the currently parsed frame. Reset via EndFrameSession. + const Frame& CurrentFrame() const { return mFrame; } + + // Returns the first parsed frame. + const Frame& FirstFrame() const { return mFirstFrame; } + + // Clear the last parsed frame to allow for next frame parsing + void EndFrameSession() { + mNextFrame.Reset(); + mFrame.Reset(); + } + + // Attempt to find the next frame. + bool FindNextFrame(MediaResourceIndex& aResource) { + mFrame = mNextFrame; + if (GetNextFrame(aResource)) { + if (!mFrame.IsValid()) { + mFrame = mNextFrame; + // We need two frames to be able to start playing (or have reached EOS). + GetNextFrame(aResource); + } + } + + if (mFrame.IsValid()) { + if (mNextFrame.EOS()) { + mFrame.SetEndOffset(static_cast<uint64_t>(aResource.Tell())); + // If the blocksize is fixed, the frame's starting sample number will be + // the frame number times the blocksize. However, the last block may + // have been incorrectly set as shorter than the stream blocksize. + // We recalculate the start time of this last sample using the first + // frame blocksize. + // TODO: should we use an overall counter of frames instead? + mFrame.ResetStartTimeIfNeeded(mFirstFrame); + } else if (mNextFrame.IsValid()) { + mFrame.SetEndOffset(mNextFrame.Offset()); + mFrame.SetEndTime(mNextFrame.Header().Index()); + } + } + + if (!mFirstFrame.IsValid()) { + mFirstFrame = mFrame; + } + return mFrame.IsValid(); + } + + // Convenience methods to external FlacFrameParser ones. + bool IsHeaderBlock(const uint8_t* aPacket, size_t aLength) const { + auto res = mParser.IsHeaderBlock(aPacket, aLength); + return res.isOk() ? res.unwrap() : false; + } + + uint32_t HeaderBlockLength(const uint8_t* aPacket) const { + return mParser.HeaderBlockLength(aPacket); + } + + bool DecodeHeaderBlock(const uint8_t* aPacket, size_t aLength) { + return mParser.DecodeHeaderBlock(aPacket, aLength).isOk(); + } + + bool HasFullMetadata() const { return mParser.HasFullMetadata(); } + + AudioInfo Info() const { return mParser.mInfo; } + + // Return a hash table with tag metadata. + UniquePtr<MetadataTags> GetTags() const { return mParser.GetTags(); } + + private: + bool GetNextFrame(MediaResourceIndex& aResource) { + while (mNextFrame.FindNext(aResource)) { + // Move our offset slightly, so that we don't find the same frame at the + // next FindNext call. + aResource.Seek(SEEK_CUR, mNextFrame.Header().Size()); + if (mFrame.IsValid() && + mNextFrame.Offset() - mFrame.Offset() < FLAC_MAX_FRAME_SIZE && + !CheckCRC16AtOffset(mFrame.Offset(), mNextFrame.Offset(), + aResource)) { + // The frame doesn't match its CRC or would be too far, skip it.. + continue; + } + CheckFrameData(); + break; + } + return mNextFrame.IsValid(); + } + + bool CheckFrameData() { + if (mNextFrame.Header().Info().mRate == 0 || + mNextFrame.Header().Info().mBitDepth == 0) { + if (!Info().IsValid()) { + // We can only use the STREAMINFO data if we have one. + mNextFrame.SetInvalid(); + } else { + if (mNextFrame.Header().Info().mRate == 0) { + mNextFrame.SetRate(Info().mRate); + } + if (mNextFrame.Header().Info().mBitDepth == 0) { + mNextFrame.SetBitDepth(Info().mBitDepth); + } + } + } + return mNextFrame.IsValid(); + } + + bool CheckCRC16AtOffset(int64_t aStart, int64_t aEnd, + MediaResourceIndex& aResource) const { + int64_t size = aEnd - aStart; + if (size <= 0) { + return false; + } + UniquePtr<char[]> buffer(new char[static_cast<size_t>(size)]); + uint32_t read = 0; + if (NS_FAILED(aResource.ReadAt(aStart, buffer.get(), size, &read)) || + read != size) { + NS_WARNING("Couldn't read frame content"); + return false; + } + + uint16_t crc = 0; + uint8_t* buf = reinterpret_cast<uint8_t*>(buffer.get()); + const uint8_t* end = buf + size; + while (buf < end) { + crc = CRC16Table[((uint8_t)crc) ^ *buf++] ^ (crc >> 8); + } +#ifdef FUZZING + return true; +#else + return !crc; +#endif + } + + const uint16_t CRC16Table[256] = { + 0x0000, 0x0580, 0x0F80, 0x0A00, 0x1B80, 0x1E00, 0x1400, 0x1180, 0x3380, + 0x3600, 0x3C00, 0x3980, 0x2800, 0x2D80, 0x2780, 0x2200, 0x6380, 0x6600, + 0x6C00, 0x6980, 0x7800, 0x7D80, 0x7780, 0x7200, 0x5000, 0x5580, 0x5F80, + 0x5A00, 0x4B80, 0x4E00, 0x4400, 0x4180, 0xC380, 0xC600, 0xCC00, 0xC980, + 0xD800, 0xDD80, 0xD780, 0xD200, 0xF000, 0xF580, 0xFF80, 0xFA00, 0xEB80, + 0xEE00, 0xE400, 0xE180, 0xA000, 0xA580, 0xAF80, 0xAA00, 0xBB80, 0xBE00, + 0xB400, 0xB180, 0x9380, 0x9600, 0x9C00, 0x9980, 0x8800, 0x8D80, 0x8780, + 0x8200, 0x8381, 0x8601, 0x8C01, 0x8981, 0x9801, 0x9D81, 0x9781, 0x9201, + 0xB001, 0xB581, 0xBF81, 0xBA01, 0xAB81, 0xAE01, 0xA401, 0xA181, 0xE001, + 0xE581, 0xEF81, 0xEA01, 0xFB81, 0xFE01, 0xF401, 0xF181, 0xD381, 0xD601, + 0xDC01, 0xD981, 0xC801, 0xCD81, 0xC781, 0xC201, 0x4001, 0x4581, 0x4F81, + 0x4A01, 0x5B81, 0x5E01, 0x5401, 0x5181, 0x7381, 0x7601, 0x7C01, 0x7981, + 0x6801, 0x6D81, 0x6781, 0x6201, 0x2381, 0x2601, 0x2C01, 0x2981, 0x3801, + 0x3D81, 0x3781, 0x3201, 0x1001, 0x1581, 0x1F81, 0x1A01, 0x0B81, 0x0E01, + 0x0401, 0x0181, 0x0383, 0x0603, 0x0C03, 0x0983, 0x1803, 0x1D83, 0x1783, + 0x1203, 0x3003, 0x3583, 0x3F83, 0x3A03, 0x2B83, 0x2E03, 0x2403, 0x2183, + 0x6003, 0x6583, 0x6F83, 0x6A03, 0x7B83, 0x7E03, 0x7403, 0x7183, 0x5383, + 0x5603, 0x5C03, 0x5983, 0x4803, 0x4D83, 0x4783, 0x4203, 0xC003, 0xC583, + 0xCF83, 0xCA03, 0xDB83, 0xDE03, 0xD403, 0xD183, 0xF383, 0xF603, 0xFC03, + 0xF983, 0xE803, 0xED83, 0xE783, 0xE203, 0xA383, 0xA603, 0xAC03, 0xA983, + 0xB803, 0xBD83, 0xB783, 0xB203, 0x9003, 0x9583, 0x9F83, 0x9A03, 0x8B83, + 0x8E03, 0x8403, 0x8183, 0x8002, 0x8582, 0x8F82, 0x8A02, 0x9B82, 0x9E02, + 0x9402, 0x9182, 0xB382, 0xB602, 0xBC02, 0xB982, 0xA802, 0xAD82, 0xA782, + 0xA202, 0xE382, 0xE602, 0xEC02, 0xE982, 0xF802, 0xFD82, 0xF782, 0xF202, + 0xD002, 0xD582, 0xDF82, 0xDA02, 0xCB82, 0xCE02, 0xC402, 0xC182, 0x4382, + 0x4602, 0x4C02, 0x4982, 0x5802, 0x5D82, 0x5782, 0x5202, 0x7002, 0x7582, + 0x7F82, 0x7A02, 0x6B82, 0x6E02, 0x6402, 0x6182, 0x2002, 0x2582, 0x2F82, + 0x2A02, 0x3B82, 0x3E02, 0x3402, 0x3182, 0x1382, 0x1602, 0x1C02, 0x1982, + 0x0802, 0x0D82, 0x0782, 0x0202, + }; + + FlacFrameParser mParser; + // We keep the first parsed frame around for static info access + // and the currently parsed frame. + Frame mFirstFrame; + Frame mNextFrame; + Frame mFrame; +}; + +} // namespace flac + +// FlacDemuxer + +FlacDemuxer::FlacDemuxer(MediaResource* aSource) : mSource(aSource) { + DDLINKCHILD("source", aSource); +} + +bool FlacDemuxer::InitInternal() { + if (!mTrackDemuxer) { + mTrackDemuxer = new FlacTrackDemuxer(mSource); + DDLINKCHILD("track demuxer", mTrackDemuxer.get()); + } + return mTrackDemuxer->Init(); +} + +RefPtr<FlacDemuxer::InitPromise> FlacDemuxer::Init() { + if (!InitInternal()) { + LOG("Init() failure: waiting for data"); + + return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, + __func__); + } + + LOG("Init() successful"); + return InitPromise::CreateAndResolve(NS_OK, __func__); +} + +uint32_t FlacDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const { + return (aType == TrackInfo::kAudioTrack) ? 1 : 0; +} + +already_AddRefed<MediaTrackDemuxer> FlacDemuxer::GetTrackDemuxer( + TrackInfo::TrackType aType, uint32_t aTrackNumber) { + if (!mTrackDemuxer) { + return nullptr; + } + + return RefPtr<FlacTrackDemuxer>(mTrackDemuxer).forget(); +} + +bool FlacDemuxer::IsSeekable() const { + return mTrackDemuxer && mTrackDemuxer->IsSeekable(); +} + +// FlacTrackDemuxer +FlacTrackDemuxer::FlacTrackDemuxer(MediaResource* aSource) + : mSource(aSource), mParser(new flac::FrameParser()), mTotalFrameLen(0) { + DDLINKCHILD("source", aSource); + Reset(); +} + +FlacTrackDemuxer::~FlacTrackDemuxer() = default; + +bool FlacTrackDemuxer::Init() { + static const int BUFFER_SIZE = 4096; + + // First check if we have a valid Flac start. + char buffer[BUFFER_SIZE]; + const uint8_t* ubuffer = // only needed due to type constraints of ReadAt. + reinterpret_cast<uint8_t*>(buffer); + int64_t offset = 0; + + do { + uint32_t read = 0; + nsresult ret = mSource.ReadAt(offset, buffer, BUFFER_SIZE, &read); + if (NS_FAILED(ret) || read < BUFFER_SIZE) { + // Assume that if we can't read that many bytes while parsing the header, + // that something is wrong. + return false; + } + if (!mParser->IsHeaderBlock(ubuffer, BUFFER_SIZE)) { + // Not a header and we haven't reached the end of the metadata blocks. + // Will fall back to using the frames header instead. + break; + } + uint32_t sizeHeader = mParser->HeaderBlockLength(ubuffer); + RefPtr<MediaByteBuffer> block = mSource.MediaReadAt(offset, sizeHeader); + if (!block || block->Length() != sizeHeader) { + break; + } + if (!mParser->DecodeHeaderBlock(block->Elements(), sizeHeader)) { + break; + } + offset += sizeHeader; + } while (!mParser->HasFullMetadata()); + + // First flac frame is found after the metadata. + // Can seek there immediately to avoid reparsing it all. + mSource.Seek(SEEK_SET, offset); + + // Find the first frame to fully initialise our parser. + if (mParser->FindNextFrame(mSource)) { + // Ensure that the next frame returned will be the first. + mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset()); + mParser->EndFrameSession(); + } else if (!mParser->Info().IsValid() || !mParser->FirstFrame().IsValid()) { + // We must find at least a frame to determine the metadata. + // We can't play this stream. + return false; + } + + if (!mParser->Info().IsValid() || !mParser->Info().mDuration.IsPositive()) { + // Check if we can look at the last frame for the end time to determine the + // duration when we don't have any. + TimeAtEnd(); + } + + return true; +} + +UniquePtr<TrackInfo> FlacTrackDemuxer::GetInfo() const { + if (mParser->Info().IsValid()) { + // We have a proper metadata header. + UniquePtr<TrackInfo> info = mParser->Info().Clone(); + UniquePtr<MetadataTags> tags(mParser->GetTags()); + if (tags) { + for (const auto& entry : *tags) { + info->mTags.AppendElement(MetadataTag(entry.GetKey(), entry.GetData())); + } + } + MOZ_ASSERT(info->IsAudio() && + info->GetAsAudioInfo() + ->mCodecSpecificConfig.is<FlacCodecSpecificData>(), + "Should get flac specific data from parser"); + return info; + } else if (mParser->FirstFrame().Info().IsValid()) { + // Use the first frame header. + UniquePtr<TrackInfo> info = mParser->FirstFrame().Info().Clone(); + info->mDuration = Duration(); + MOZ_ASSERT(info->IsAudio() && + info->GetAsAudioInfo() + ->mCodecSpecificConfig.is<FlacCodecSpecificData>(), + "Should get flac specific data from parser"); + return info; + } + return nullptr; +} + +bool FlacTrackDemuxer::IsSeekable() const { + // For now we only allow seeking if a STREAMINFO block was found and with + // a known number of samples (duration is set). + return mParser->Info().IsValid() && mParser->Info().mDuration.IsPositive(); +} + +RefPtr<FlacTrackDemuxer::SeekPromise> FlacTrackDemuxer::Seek( + const TimeUnit& aTime) { + // 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 FlacTrackDemuxer::FastSeek(const TimeUnit& aTime) { + LOG("FastSeek(%f) avgFrameLen=%f mParsedFramesDuration=%f offset=%" PRId64, + aTime.ToSeconds(), AverageFrameLength(), + mParsedFramesDuration.ToSeconds(), GetResourceOffset()); + + // Invalidate current frames in the parser. + mParser->EndFrameSession(); + + if (!mParser->FirstFrame().IsValid()) { + // Something wrong, and there's nothing to seek to anyway, so we can + // do whatever here. + mSource.Seek(SEEK_SET, 0); + return TimeUnit(); + } + + if (aTime <= mParser->FirstFrame().Time()) { + // We're attempting to seek prior the first frame, return the first frame. + mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset()); + return mParser->FirstFrame().Time(); + } + + // We look for the seek position using a bisection search, starting where the + // estimated position might be using the average frame length. + // Typically, with flac such approximation is typically useless. + + // Estimate where the position might be. + int64_t pivot = + aTime.ToSeconds() * AverageFrameLength() + mParser->FirstFrame().Offset(); + + // Time in seconds where we can stop seeking and will continue using + // ScanUntil. + static const int GAP_THRESHOLD = 5; + int64_t first = mParser->FirstFrame().Offset(); + int64_t last = mSource.GetLength(); + Maybe<uint64_t> lastFoundOffset; + uint32_t iterations = 0; + TimeUnit timeSeekedTo; + + do { + iterations++; + mSource.Seek(SEEK_SET, pivot); + flac::Frame frame; + if (!frame.FindNext(mSource)) { + NS_WARNING("We should have found a point"); + break; + } + timeSeekedTo = frame.Time(); + + LOGV("FastSeek: interation:%u found:%f @ %" PRIu64, iterations, + timeSeekedTo.ToSeconds(), frame.Offset()); + + if (lastFoundOffset && lastFoundOffset.ref() == frame.Offset()) { + // Same frame found twice. We're done. + break; + } + lastFoundOffset = Some(frame.Offset()); + + if (frame.Time() == aTime) { + break; + } + if (aTime > frame.Time() && + aTime - frame.Time() <= TimeUnit::FromSeconds(GAP_THRESHOLD)) { + // We're close enough to the target, experimentation shows that bisection + // search doesn't help much after that. + break; + } + if (frame.Time() > aTime) { + last = pivot; + pivot -= (pivot - first) / 2; + } else { + first = pivot; + pivot += (last - pivot) / 2; + } + } while (true); + + if (lastFoundOffset) { + mSource.Seek(SEEK_SET, lastFoundOffset.ref()); + } + + return timeSeekedTo; +} + +TimeUnit FlacTrackDemuxer::ScanUntil(const TimeUnit& aTime) { + LOG("ScanUntil(%f avgFrameLen=%f mParsedFramesDuration=%f offset=%" PRId64, + aTime.ToSeconds(), AverageFrameLength(), + mParsedFramesDuration.ToSeconds(), mParser->CurrentFrame().Offset()); + + if (!mParser->FirstFrame().IsValid() || + aTime <= mParser->FirstFrame().Time()) { + return FastSeek(aTime); + } + + int64_t previousOffset = 0; + TimeUnit previousTime; + while (FindNextFrame().IsValid() && mParser->CurrentFrame().Time() < aTime) { + previousOffset = mParser->CurrentFrame().Offset(); + previousTime = mParser->CurrentFrame().Time(); + } + + if (!mParser->CurrentFrame().IsValid()) { + // We reached EOS. + return Duration(); + } + + // Seek back to the last frame found prior the target. + mParser->EndFrameSession(); + mSource.Seek(SEEK_SET, previousOffset); + return previousTime; +} + +RefPtr<FlacTrackDemuxer::SamplesPromise> FlacTrackDemuxer::GetSamples( + int32_t aNumSamples) { + LOGV("GetSamples(%d) Begin offset=%" PRId64 + " mParsedFramesDuration=%f" + " mTotalFrameLen=%" PRIu64, + aNumSamples, GetResourceOffset(), mParsedFramesDuration.ToSeconds(), + mTotalFrameLen); + + 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); + } + + LOGV("GetSamples() End mSamples.Length=%zu aNumSamples=%d offset=%" PRId64 + " mParsedFramesDuration=%f mTotalFrameLen=%" PRIu64, + frames->GetSamples().Length(), aNumSamples, GetResourceOffset(), + mParsedFramesDuration.ToSeconds(), mTotalFrameLen); + + if (frames->GetSamples().IsEmpty()) { + return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM, + __func__); + } + + return SamplesPromise::CreateAndResolve(frames, __func__); +} + +void FlacTrackDemuxer::Reset() { + LOG("Reset()"); + MOZ_ASSERT(mParser); + if (mParser->FirstFrame().IsValid()) { + mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset()); + } else { + mSource.Seek(SEEK_SET, 0); + } + mParser->EndFrameSession(); +} + +RefPtr<FlacTrackDemuxer::SkipAccessPointPromise> +FlacTrackDemuxer::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 FlacTrackDemuxer::GetResourceOffset() const { return mSource.Tell(); } + +TimeIntervals FlacTrackDemuxer::GetBuffered() { + TimeUnit duration = Duration(); + + if (duration <= TimeUnit()) { + return TimeIntervals(); + } + + // We could simply parse the cached data instead and read the timestamps. + // However, for now this will do. + AutoPinned<MediaResource> stream(mSource.GetResource()); + return GetEstimatedBufferedTimeRanges(stream, duration.ToMicroseconds()); +} + +const flac::Frame& FlacTrackDemuxer::FindNextFrame() { + LOGV("FindNext() Begin offset=%" PRId64 + " mParsedFramesDuration=%f" + " mTotalFrameLen=%" PRIu64, + GetResourceOffset(), mParsedFramesDuration.ToSeconds(), mTotalFrameLen); + + if (mParser->FindNextFrame(mSource)) { + // Update our current progress stats. + mParsedFramesDuration = + std::max(mParsedFramesDuration, mParser->CurrentFrame().Time() - + mParser->FirstFrame().Time() + + mParser->CurrentFrame().Duration()); + mTotalFrameLen = + std::max<uint64_t>(mTotalFrameLen, mParser->CurrentFrame().Offset() - + mParser->FirstFrame().Offset() + + mParser->CurrentFrame().Size()); + + LOGV("FindNext() End time=%f offset=%" PRId64 + " mParsedFramesDuration=%f" + " mTotalFrameLen=%" PRIu64, + mParser->CurrentFrame().Time().ToSeconds(), GetResourceOffset(), + mParsedFramesDuration.ToSeconds(), mTotalFrameLen); + } + + return mParser->CurrentFrame(); +} + +already_AddRefed<MediaRawData> FlacTrackDemuxer::GetNextFrame( + const flac::Frame& aFrame) { + if (!aFrame.IsValid()) { + LOG("GetNextFrame() EOS"); + return nullptr; + } + + LOG("GetNextFrame() Begin(time=%f offset=%" PRId64 " size=%u)", + aFrame.Time().ToSeconds(), aFrame.Offset(), aFrame.Size()); + + const uint64_t offset = aFrame.Offset(); + const uint32_t size = aFrame.Size(); + + RefPtr<MediaRawData> frame = new MediaRawData(); + frame->mOffset = offset; + + UniquePtr<MediaRawDataWriter> frameWriter(frame->CreateWriter()); + if (!frameWriter->SetSize(size)) { + LOG("GetNext() Exit failed to allocated media buffer"); + return nullptr; + } + + const uint32_t read = Read(frameWriter->Data(), offset, size); + if (read != size) { + LOG("GetNextFrame() Exit read=%u frame->Size=%zu", read, frame->Size()); + return nullptr; + } + + frame->mTime = aFrame.Time(); + frame->mDuration = aFrame.Duration(); + frame->mTimecode = frame->mTime; + frame->mOffset = aFrame.Offset(); + frame->mKeyframe = true; + + MOZ_ASSERT(!frame->mTime.IsNegative()); + MOZ_ASSERT(!frame->mDuration.IsNegative()); + + return frame.forget(); +} + +uint32_t FlacTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset, + int32_t aSize) { + uint32_t read = 0; + const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer), + static_cast<uint32_t>(aSize), &read); + NS_ENSURE_SUCCESS(rv, 0); + return read; +} + +double FlacTrackDemuxer::AverageFrameLength() const { + if (mParsedFramesDuration.ToMicroseconds()) { + return mTotalFrameLen / mParsedFramesDuration.ToSeconds(); + } + + return 0.0; +} + +TimeUnit FlacTrackDemuxer::Duration() const { + return std::max(mParsedFramesDuration, mParser->Info().mDuration); +} + +TimeUnit FlacTrackDemuxer::TimeAtEnd() { + // Scan the last 128kB if available to determine the last frame. + static const int OFFSET_FROM_END = 128 * 1024; + + // Seek to the end of the file and attempt to find the last frame. + MediaResourceIndex source(mSource.GetResource()); + TimeUnit previousDuration; + TimeUnit previousTime; + + const int64_t streamLen = mSource.GetLength(); + if (streamLen < 0) { + return TimeUnit::FromInfinity(); + } + + flac::FrameParser parser; + + source.Seek(SEEK_SET, std::max<int64_t>(0LL, streamLen - OFFSET_FROM_END)); + while (parser.FindNextFrame(source)) { + // FFmpeg flac muxer can generate a last frame with earlier than the others. + previousTime = std::max(previousTime, parser.CurrentFrame().Time()); + if (parser.CurrentFrame().Duration() > TimeUnit()) { + // The last frame doesn't have a duration, so only update our duration + // if we do have one. + previousDuration = parser.CurrentFrame().Duration(); + } + if (source.Tell() >= streamLen) { + // Limit the read, in case the length change half-way. + break; + } + } + + // Update our current progress stats. + mParsedFramesDuration = + previousTime + previousDuration - mParser->FirstFrame().Time(); + + mTotalFrameLen = + static_cast<uint64_t>(streamLen) - mParser->FirstFrame().Offset(); + + return mParsedFramesDuration; +} + +/* static */ +bool FlacDemuxer::FlacSniffer(const uint8_t* aData, const uint32_t aLength) { + if (aLength < FLAC_MIN_FRAME_SIZE) { + return false; + } + + flac::Frame frame; + return frame.FindNext(aData, aLength) >= 0; +} + +} // namespace mozilla diff --git a/dom/media/flac/FlacDemuxer.h b/dom/media/flac/FlacDemuxer.h new file mode 100644 index 0000000000..cfbbfb7c57 --- /dev/null +++ b/dom/media/flac/FlacDemuxer.h @@ -0,0 +1,112 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 FLAC_DEMUXER_H_ +#define FLAC_DEMUXER_H_ + +#include "mozilla/Attributes.h" +#include "MediaDataDemuxer.h" +#include "MediaResource.h" +namespace mozilla { + +namespace flac { +class Frame; +class FrameParser; +} // namespace flac +class FlacTrackDemuxer; + +DDLoggedTypeDeclNameAndBase(FlacDemuxer, MediaDataDemuxer); +DDLoggedTypeNameAndBase(FlacTrackDemuxer, MediaTrackDemuxer); + +class FlacDemuxer : public MediaDataDemuxer, + public DecoderDoctorLifeLogger<FlacDemuxer> { + public: + // MediaDataDemuxer interface. + explicit FlacDemuxer(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; + + // Return true if a valid flac frame header could be found. + static bool FlacSniffer(const uint8_t* aData, const uint32_t aLength); + + private: + bool InitInternal(); + + RefPtr<MediaResource> mSource; + RefPtr<FlacTrackDemuxer> mTrackDemuxer; +}; + +class FlacTrackDemuxer : public MediaTrackDemuxer, + public DecoderDoctorLifeLogger<FlacTrackDemuxer> { + public: + explicit FlacTrackDemuxer(MediaResource* aSource); + + // Initializes the track demuxer by reading the first frame for meta data. + // Returns initialization success state. + bool Init(); + + // 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; + int64_t GetResourceOffset() const override; + media::TimeIntervals GetBuffered() override; + RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint( + const media::TimeUnit& aTimeThreshold) override; + + bool IsSeekable() const; + + private: + // Destructor. + ~FlacTrackDemuxer(); + + // Returns the estimated stream duration, or a 0-duration if unknown. + media::TimeUnit Duration() const; + media::TimeUnit TimeAtEnd(); + + // 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 next valid frame and return it. + const flac::Frame& FindNextFrame(); + + // Returns the next ADTS frame, if available. + already_AddRefed<MediaRawData> GetNextFrame(const flac::Frame& aFrame); + + // 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, int32_t aSize); + + // Returns the average frame length derived from the previously parsed frames. + double AverageFrameLength() const; + + // The (hopefully) Flac resource. + MediaResourceIndex mSource; + + // Flac frame parser used to detect frames and extract side info. + UniquePtr<flac::FrameParser> mParser; + + // Total duration of parsed frames. + media::TimeUnit mParsedFramesDuration; + + // Sum of parsed frames' lengths in bytes. + uint64_t mTotalFrameLen; + + // Audio track config info. + UniquePtr<AudioInfo> mInfo; +}; + +} // namespace mozilla + +#endif // !FLAC_DEMUXER_H_ diff --git a/dom/media/flac/FlacFrameParser.cpp b/dom/media/flac/FlacFrameParser.cpp new file mode 100644 index 0000000000..c042a7d334 --- /dev/null +++ b/dom/media/flac/FlacFrameParser.cpp @@ -0,0 +1,244 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "FlacFrameParser.h" +#include "nsTArray.h" +#include "OggCodecState.h" +#include "OpusParser.h" +#include "VideoUtils.h" +#include "BufferReader.h" +#include "mozilla/ResultExtensions.h" + +namespace mozilla { + +#define OGG_FLAC_METADATA_TYPE_STREAMINFO 0x7F +#define FLAC_STREAMINFO_SIZE 34 + +#define BITMASK(x) ((1ULL << x) - 1) + +enum { + FLAC_METADATA_TYPE_STREAMINFO = 0, + FLAC_METADATA_TYPE_PADDING, + FLAC_METADATA_TYPE_APPLICATION, + FLAC_METADATA_TYPE_SEEKTABLE, + FLAC_METADATA_TYPE_VORBIS_COMMENT, + FLAC_METADATA_TYPE_CUESHEET, + FLAC_METADATA_TYPE_PICTURE, + FLAC_METADATA_TYPE_INVALID = 127 +}; + +FlacFrameParser::FlacFrameParser() + : mMinBlockSize(0), + mMaxBlockSize(0), + mMinFrameSize(0), + mMaxFrameSize(0), + mNumFrames(0), + mFullMetadata(false), + mPacketCount(0){}; + +FlacFrameParser::~FlacFrameParser() = default; + +uint32_t FlacFrameParser::HeaderBlockLength(const uint8_t* aPacket) const { + uint32_t extra = 4; + if (aPacket[0] == 'f') { + // This must be the first block read, which contains the fLaC signature. + aPacket += 4; + extra += 4; + } + return (BigEndian::readUint32(aPacket) & BITMASK(24)) + extra; +} + +Result<Ok, nsresult> FlacFrameParser::DecodeHeaderBlock(const uint8_t* aPacket, + size_t aLength) { + if (aLength < 4 || aPacket[0] == 0xff) { + // Not a header block. + return Err(NS_ERROR_FAILURE); + } + BufferReader br(aPacket, aLength); + + mPacketCount++; + + if (aPacket[0] == 'f') { + if (mPacketCount != 1 || memcmp(br.Read(4), "fLaC", 4) || + br.Remaining() != FLAC_STREAMINFO_SIZE + 4) { + return Err(NS_ERROR_FAILURE); + } + } + uint8_t blockHeader; + MOZ_TRY_VAR(blockHeader, br.ReadU8()); + // blockType is a misnomer as it could indicate here either a packet type + // should it points to the start of a Flac in Ogg metadata, or an actual + // block type as per the flac specification. + uint32_t blockType = blockHeader & 0x7f; + bool lastBlock = blockHeader & 0x80; + + if (blockType == OGG_FLAC_METADATA_TYPE_STREAMINFO) { + if (mPacketCount != 1 || memcmp(br.Read(4), "FLAC", 4) || + br.Remaining() != FLAC_STREAMINFO_SIZE + 12) { + return Err(NS_ERROR_FAILURE); + } + uint32_t major; + MOZ_TRY_VAR(major, br.ReadU8()); + if (major != 1) { + // unsupported version; + return Err(NS_ERROR_FAILURE); + } + MOZ_TRY(br.ReadU8()); // minor version + uint32_t header; + MOZ_TRY_VAR(header, br.ReadU16()); + mNumHeaders = Some(header); + br.Read(4); // fLaC + MOZ_TRY_VAR(blockType, br.ReadU8()); + blockType &= BITMASK(7); + // First METADATA_BLOCK_STREAMINFO + if (blockType != FLAC_METADATA_TYPE_STREAMINFO) { + // First block must be a stream info. + return Err(NS_ERROR_FAILURE); + } + } + + uint32_t blockDataSize; + MOZ_TRY_VAR(blockDataSize, br.ReadU24()); + const uint8_t* blockDataStart = br.Peek(blockDataSize); + if (!blockDataStart) { + // Incomplete block. + return Err(NS_ERROR_FAILURE); + } + + switch (blockType) { + case FLAC_METADATA_TYPE_STREAMINFO: { + if (mPacketCount != 1 || blockDataSize != FLAC_STREAMINFO_SIZE) { + // STREAMINFO must be the first metadata block found, and its size + // is constant. + return Err(NS_ERROR_FAILURE); + } + + MOZ_TRY_VAR(mMinBlockSize, br.ReadU16()); + MOZ_TRY_VAR(mMaxBlockSize, br.ReadU16()); + MOZ_TRY_VAR(mMinFrameSize, br.ReadU24()); + MOZ_TRY_VAR(mMaxFrameSize, br.ReadU24()); + + uint64_t blob; + MOZ_TRY_VAR(blob, br.ReadU64()); + uint32_t sampleRate = (blob >> 44) & BITMASK(20); + if (!sampleRate) { + return Err(NS_ERROR_FAILURE); + } + uint32_t numChannels = ((blob >> 41) & BITMASK(3)) + 1; + if (numChannels > FLAC_MAX_CHANNELS) { + return Err(NS_ERROR_FAILURE); + } + uint32_t bps = ((blob >> 36) & BITMASK(5)) + 1; + if (bps > 24) { + return Err(NS_ERROR_FAILURE); + } + mNumFrames = blob & BITMASK(36); + + mInfo.mMimeType = "audio/flac"; + mInfo.mRate = sampleRate; + mInfo.mChannels = numChannels; + mInfo.mBitDepth = bps; + FlacCodecSpecificData flacCodecSpecificData; + flacCodecSpecificData.mStreamInfoBinaryBlob->AppendElements( + blockDataStart, blockDataSize); + mInfo.mCodecSpecificConfig = + AudioCodecSpecificVariant{std::move(flacCodecSpecificData)}; + auto duration = media::TimeUnit(mNumFrames, sampleRate); + mInfo.mDuration = duration.IsValid() ? duration : media::TimeUnit::Zero(); + mParser = MakeUnique<OpusParser>(); + break; + } + case FLAC_METADATA_TYPE_VORBIS_COMMENT: { + if (!mParser) { + // We must have seen a valid streaminfo first. + return Err(NS_ERROR_FAILURE); + } + nsTArray<uint8_t> comments(blockDataSize + 8); + comments.AppendElements("OpusTags", 8); + comments.AppendElements(blockDataStart, blockDataSize); + if (!mParser->DecodeTags(comments.Elements(), comments.Length())) { + return Err(NS_ERROR_FAILURE); + } + break; + } + default: + break; + } + + if (mNumHeaders && mPacketCount > mNumHeaders.ref() + 1) { + // Received too many header block. assuming invalid. + return Err(NS_ERROR_FAILURE); + } + + if (lastBlock || (mNumHeaders && mNumHeaders.ref() + 1 == mPacketCount)) { + mFullMetadata = true; + } + + return Ok(); +} + +int64_t FlacFrameParser::BlockDuration(const uint8_t* aPacket, + size_t aLength) const { + if (!mInfo.IsValid()) { + return -1; + } + if (mMinBlockSize == mMaxBlockSize) { + // block size is fixed, use this instead of looking at the frame header. + return mMinBlockSize; + } + // TODO + return 0; +} + +Result<bool, nsresult> FlacFrameParser::IsHeaderBlock(const uint8_t* aPacket, + size_t aLength) const { + // Ogg Flac header + // The one-byte packet type 0x7F + // The four-byte ASCII signature "FLAC", i.e. 0x46, 0x4C, 0x41, 0x43 + + // Flac header: + // "fLaC", the FLAC stream marker in ASCII, meaning byte 0 of the stream is + // 0x66, followed by 0x4C 0x61 0x43 + + // If we detect either a ogg or plain flac header, then it must be valid. + if (aLength < 4 || aPacket[0] == 0xff) { + // A header is at least 4 bytes. + return false; + } + if (aPacket[0] == 0x7f) { + // Ogg packet + BufferReader br(aPacket + 1, aLength - 1); + const uint8_t* signature = br.Read(4); + return signature && !memcmp(signature, "FLAC", 4); + } + BufferReader br(aPacket, aLength - 1); + const uint8_t* signature = br.Read(4); + if (signature && !memcmp(signature, "fLaC", 4)) { + // Flac start header, must have STREAMINFO as first metadata block; + uint32_t blockType; + MOZ_TRY_VAR(blockType, br.ReadU8()); + blockType &= 0x7f; + return blockType == FLAC_METADATA_TYPE_STREAMINFO; + } + char type = aPacket[0] & 0x7f; + return type >= 1 && type <= 6; +} + +UniquePtr<MetadataTags> FlacFrameParser::GetTags() const { + if (!mParser) { + return nullptr; + } + + auto tags = MakeUnique<MetadataTags>(); + for (uint32_t i = 0; i < mParser->mTags.Length(); i++) { + OggCodecState::AddVorbisComment(tags, mParser->mTags[i].Data(), + mParser->mTags[i].Length()); + } + + return tags; +} + +} // namespace mozilla diff --git a/dom/media/flac/FlacFrameParser.h b/dom/media/flac/FlacFrameParser.h new file mode 100644 index 0000000000..2bd457ea03 --- /dev/null +++ b/dom/media/flac/FlacFrameParser.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 FLAC_FRAME_PARSER_H_ +#define FLAC_FRAME_PARSER_H_ + +#include "mozilla/Maybe.h" +#include "mozilla/Result.h" +#include "MediaDecoder.h" // For MetadataTags +#include "MediaInfo.h" +#include "MediaResource.h" + +namespace mozilla { + +#define FLAC_MAX_CHANNELS 8 +#define FLAC_MIN_BLOCKSIZE 16 +#define FLAC_MAX_BLOCKSIZE 65535 +#define FLAC_MIN_FRAME_SIZE 11 +#define FLAC_MAX_FRAME_HEADER_SIZE 16 +#define FLAC_MAX_FRAME_SIZE \ + (FLAC_MAX_FRAME_HEADER_SIZE + FLAC_MAX_BLOCKSIZE * FLAC_MAX_CHANNELS * 3) + +class OpusParser; + +// Decode a Flac Metadata block contained in either a ogg packet +// (https://xiph.org/flac/ogg_mapping.html) or in flac container +// (https://xiph.org/flac/format.html#frame_header) + +class FlacFrameParser { + public: + FlacFrameParser(); + ~FlacFrameParser(); + + Result<bool, nsresult> IsHeaderBlock(const uint8_t* aPacket, + size_t aLength) const; + // Return the length of the block header (METADATA_BLOCK_HEADER+ + // METADATA_BLOCK_DATA), aPacket must point to at least 4 + // bytes and to a valid block header start (as determined by IsHeaderBlock). + uint32_t HeaderBlockLength(const uint8_t* aPacket) const; + Result<Ok, nsresult> DecodeHeaderBlock(const uint8_t* aPacket, + size_t aLength); + bool HasFullMetadata() const { return mFullMetadata; } + // Return the duration in frames found in the block. -1 if error + // such as invalid packet. + int64_t BlockDuration(const uint8_t* aPacket, size_t aLength) const; + + // Return a hash table with tag metadata. + UniquePtr<MetadataTags> GetTags() const; + + AudioInfo mInfo; + + private: + bool ReconstructFlacGranulepos(void); + Maybe<uint32_t> mNumHeaders; + uint32_t mMinBlockSize; + uint32_t mMaxBlockSize; + uint32_t mMinFrameSize; + uint32_t mMaxFrameSize; + uint64_t mNumFrames; + bool mFullMetadata; + uint32_t mPacketCount; + + // Used to decode the vorbis comment metadata. + UniquePtr<OpusParser> mParser; +}; + +} // namespace mozilla + +#endif // FLAC_FRAME_PARSER_H_ diff --git a/dom/media/flac/moz.build b/dom/media/flac/moz.build new file mode 100644 index 0000000000..0e3c8eae61 --- /dev/null +++ b/dom/media/flac/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; 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 += [ + "FlacDecoder.h", + "FlacDemuxer.h", + "FlacFrameParser.h", +] + +UNIFIED_SOURCES += [ + "FlacDecoder.cpp", + "FlacDemuxer.cpp", + "FlacFrameParser.cpp", +] + +CXXFLAGS += CONFIG["MOZ_LIBVPX_CFLAGS"] + +FINAL_LIBRARY = "xul" + +# Add libFuzzer configuration directives +include("/tools/fuzzing/libfuzzer-config.mozbuild") |