diff options
Diffstat (limited to 'dom/media/gtest/TestMP3Demuxer.cpp')
-rw-r--r-- | dom/media/gtest/TestMP3Demuxer.cpp | 579 |
1 files changed, 579 insertions, 0 deletions
diff --git a/dom/media/gtest/TestMP3Demuxer.cpp b/dom/media/gtest/TestMP3Demuxer.cpp new file mode 100644 index 0000000000..f6c589b835 --- /dev/null +++ b/dom/media/gtest/TestMP3Demuxer.cpp @@ -0,0 +1,579 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <gtest/gtest.h> +#include <vector> + +#include "MP3Demuxer.h" +#include "mozilla/ArrayUtils.h" +#include "MockMediaResource.h" + +class MockMP3MediaResource; +class MockMP3StreamMediaResource; +namespace mozilla { +DDLoggedTypeNameAndBase(::MockMP3MediaResource, MockMediaResource); +DDLoggedTypeNameAndBase(::MockMP3StreamMediaResource, MockMP3MediaResource); +} // namespace mozilla + +using namespace mozilla; +using media::TimeUnit; + +// Regular MP3 file mock resource. +class MockMP3MediaResource + : public MockMediaResource, + public DecoderDoctorLifeLogger<MockMP3MediaResource> { + public: + explicit MockMP3MediaResource(const char* aFileName) + : MockMediaResource(aFileName) {} + + protected: + virtual ~MockMP3MediaResource() = default; +}; + +// MP3 stream mock resource. +class MockMP3StreamMediaResource + : public MockMP3MediaResource, + public DecoderDoctorLifeLogger<MockMP3StreamMediaResource> { + public: + explicit MockMP3StreamMediaResource(const char* aFileName) + : MockMP3MediaResource(aFileName) {} + + int64_t GetLength() override { return -1; } + + protected: + virtual ~MockMP3StreamMediaResource() = default; +}; + +struct MP3Resource { + enum class HeaderType { NONE, XING, VBRI }; + struct Duration { + int64_t mMicroseconds; + float mTolerableRate; + + Duration(int64_t aMicroseconds, float aTolerableRate) + : mMicroseconds(aMicroseconds), mTolerableRate(aTolerableRate) {} + int64_t Tolerance() const { + return AssertedCast<int64_t>(mTolerableRate * + static_cast<float>(mMicroseconds)); + } + }; + + const char* mFilePath{}; + bool mIsVBR{}; + HeaderType mHeaderType{HeaderType::NONE}; + int64_t mFileSize{}; + uint32_t mMPEGLayer{}; + uint32_t mMPEGVersion{}; + uint8_t mID3MajorVersion{}; + uint8_t mID3MinorVersion{}; + uint8_t mID3Flags{}; + uint32_t mID3Size{}; + + Maybe<Duration> mDuration; + float mSeekError{}; + uint32_t mSampleRate{}; + uint32_t mSamplesPerFrame{}; + uint32_t mNumSamples{}; + uint32_t mPadding{}; + uint32_t mEncoderDelay{}; + uint32_t mBitrate{}; + uint32_t mSlotSize{}; + int32_t mPrivate{}; + + // The first n frame offsets. + std::vector<int32_t> mSyncOffsets; + RefPtr<MockMP3MediaResource> mResource; + RefPtr<MP3TrackDemuxer> mDemuxer; +}; + +class MP3DemuxerTest : public ::testing::Test { + protected: + void SetUp() override { + { + MP3Resource res; + res.mFilePath = "noise.mp3"; + res.mIsVBR = false; + res.mHeaderType = MP3Resource::HeaderType::NONE; + res.mFileSize = 965257; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 2141; + // The tolerance comes from the fact that this file has ID3v1 information + // at the end, this trips our CBR duration calculation. The file has + // however the correct duration when decoded / demuxed completely. + res.mDuration = Some(MP3Resource::Duration{30093063, 0.00015f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 1327104; + res.mPadding = 0; + res.mEncoderDelay = 0; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {2151, 2987, 3823, 4659, 5495, 6331}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + streamRes.mDuration = Nothing(); + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + // This file trips up the MP3 demuxer if ID3v2 tags aren't properly + // skipped. If skipping is not properly implemented, depending on the + // strictness of the MPEG frame parser a false sync will be detected + // somewhere within the metadata at or after 112087, or failing that, at + // the artificially added extraneous header at 114532. + res.mFilePath = "id3v2header.mp3"; + res.mIsVBR = false; + res.mHeaderType = MP3Resource::HeaderType::NONE; + res.mFileSize = 191302; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 115304; + // The tolerance comes from the fact that this file has ID3v1 information + // at the end, this trips our CBR duration calculation. The file has + // however the correct duration when decoded / demuxed completely. + res.mDuration = Some(MP3Resource::Duration{3160833, 0.0017f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 139392; + res.mPadding = 0; + res.mEncoderDelay = 0; + res.mBitrate = 192000; + res.mSlotSize = 1; + res.mPrivate = 1; + const int syncs[] = {115314, 115941, 116568, 117195, 117822, 118449}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + streamRes.mDuration = Nothing(); + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "noise_vbr.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 583679; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 2221; + res.mDuration = Some(MP3Resource::Duration{30081065, 0.f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 1326575; + res.mPadding = 576; + res.mEncoderDelay = 2257; + res.mBitrate = 154000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {2231, 2648, 2752, 3796, 4318, 4735}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // VBR stream resources contain header info on total frames numbers, which + // is used to estimate the total duration. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "small-shot.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6825; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mPadding = 0; + res.mEncoderDelay = 1152 + 529; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, + 3691, 4213, 4736, 5258, 5781, 6303}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + // This file contains a false frame sync at 34, just after the ID3 tag, + // which should be identified as a false positive and skipped. + res.mFilePath = "small-shot-false-positive.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6845; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mPadding = 0; + res.mEncoderDelay = 1681; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {54, 576, 1098, 1621, 2143, 2666, 3188, + 3711, 4233, 4756, 5278, 5801, 6323}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "small-shot-partial-xing.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::XING; + res.mFileSize = 6825; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 4; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 24; + res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); + res.mSeekError = 0.2f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 12; + res.mPadding = 0; + res.mEncoderDelay = 1681; + res.mBitrate = 256000; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, + 3691, 4213, 4736, 5258, 5781, 6303}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); + + // No content length can be estimated for CBR stream resources. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + { + MP3Resource res; + res.mFilePath = "test_vbri.mp3"; + res.mIsVBR = true; + res.mHeaderType = MP3Resource::HeaderType::VBRI; + res.mFileSize = 16519; + res.mMPEGLayer = 3; + res.mMPEGVersion = 1; + res.mID3MajorVersion = 3; + res.mID3MinorVersion = 0; + res.mID3Flags = 0; + res.mID3Size = 4202; + res.mDuration = Some(MP3Resource::Duration{731428, 0.f}); + res.mSeekError = 0.02f; + res.mSampleRate = 44100; + res.mSamplesPerFrame = 1152; + res.mNumSamples = 29; + res.mPadding = 0; + res.mEncoderDelay = 1152; + res.mBitrate = 0; + res.mSlotSize = 1; + res.mPrivate = 0; + const int syncs[] = {4212, 4734, 5047, 5464, 5986, 6403}; + res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); + + // VBR stream resources contain header info on total frames numbers, which + // is used to estimate the total duration. + MP3Resource streamRes = res; + streamRes.mFileSize = -1; + + res.mResource = new MockMP3MediaResource(res.mFilePath); + res.mDemuxer = new MP3TrackDemuxer(res.mResource); + mTargets.push_back(res); + + streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); + streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); + mTargets.push_back(streamRes); + } + + for (auto& target : mTargets) { + ASSERT_EQ(NS_OK, target.mResource->Open()); + ASSERT_TRUE(target.mDemuxer->Init()); + } + } + + std::vector<MP3Resource> mTargets; +}; + +TEST_F(MP3DemuxerTest, ID3Tags) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frame); + + const auto& id3 = target.mDemuxer->ID3Header(); + ASSERT_TRUE(id3.IsValid()); + + EXPECT_EQ(target.mID3MajorVersion, id3.MajorVersion()); + EXPECT_EQ(target.mID3MinorVersion, id3.MinorVersion()); + EXPECT_EQ(target.mID3Flags, id3.Flags()); + EXPECT_EQ(target.mID3Size, id3.Size()); + } +} + +TEST_F(MP3DemuxerTest, VBRHeader) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frame); + + const auto& vbr = target.mDemuxer->VBRInfo(); + + if (target.mHeaderType == MP3Resource::HeaderType::XING) { + EXPECT_EQ(FrameParser::VBRHeader::XING, vbr.Type()); + } else if (target.mHeaderType == MP3Resource::HeaderType::VBRI) { + EXPECT_TRUE(target.mIsVBR); + EXPECT_EQ(FrameParser::VBRHeader::VBRI, vbr.Type()); + } else { // MP3Resource::HeaderType::NONE + EXPECT_EQ(FrameParser::VBRHeader::NONE, vbr.Type()); + EXPECT_FALSE(vbr.NumAudioFrames()); + } + } +} + +TEST_F(MP3DemuxerTest, FrameParsing) { + for (const auto& target : mTargets) { + printf("Testing: %s\n", target.mFilePath); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); + + const auto& id3 = target.mDemuxer->ID3Header(); + ASSERT_TRUE(id3.IsValid()); + + int64_t parsedLength = id3.Size(); + uint64_t bitrateSum = 0; + uint32_t numFrames = 0; + uint32_t numSamples = 0; + + while (frameData) { + if (static_cast<int64_t>(target.mSyncOffsets.size()) > numFrames) { + // Test sync offsets. + EXPECT_EQ(target.mSyncOffsets[numFrames], frameData->mOffset); + } + + ++numFrames; + parsedLength += AssertedCast<int64_t>(frameData->Size()); + + const auto& frame = target.mDemuxer->LastFrame(); + const auto& header = frame.Header(); + ASSERT_TRUE(header.IsValid()); + + numSamples += header.SamplesPerFrame(); + + EXPECT_EQ(target.mMPEGLayer, header.Layer()); + EXPECT_EQ(target.mSampleRate, header.SampleRate()); + EXPECT_EQ(target.mSamplesPerFrame, header.SamplesPerFrame()); + EXPECT_EQ(target.mSlotSize, header.SlotSize()); + EXPECT_EQ(target.mPrivate, header.Private()); + + if (target.mIsVBR) { + // Used to compute the average bitrate for VBR streams. + bitrateSum += target.mBitrate; + } else { + EXPECT_EQ(target.mBitrate, header.Bitrate()); + } + + frameData = target.mDemuxer->DemuxSample(); + } + + EXPECT_EQ(target.mPadding, target.mDemuxer->PaddingFrames()); + EXPECT_EQ(target.mEncoderDelay, target.mDemuxer->EncoderDelayFrames()); + EXPECT_GE(numSamples, 0u); + + // There may be trailing headers which we don't parse, so the stream length + // is the upper bound. + if (target.mFileSize > 0) { + EXPECT_GE(target.mFileSize, parsedLength); + } + + if (target.mIsVBR) { + ASSERT_TRUE(numFrames); + EXPECT_EQ(target.mBitrate, bitrateSum / numFrames); + } + } +} + +TEST_F(MP3DemuxerTest, Duration) { + for (const auto& target : mTargets) { + printf("Testing: %s\n", target.mFilePath); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); + + while (frameData) { + if (target.mDuration) { + ASSERT_TRUE(target.mDemuxer->Duration()); + EXPECT_NEAR(target.mDuration->mMicroseconds, + target.mDemuxer->Duration()->ToMicroseconds(), + target.mDuration->Tolerance()); + } else { + EXPECT_FALSE(target.mDemuxer->Duration()); + } + frameData = target.mDemuxer->DemuxSample(); + } + if (target.mDuration) { + // At the end, the durations should always be exact. + EXPECT_EQ(target.mDuration->mMicroseconds, + target.mDemuxer->Duration()->ToMicroseconds()); + } + } + + // Seek out of range tests. + for (const auto& target : mTargets) { + printf("Testing %s\n", target.mFilePath); + // Skip tests for stream media resources because of lacking duration. + if (target.mFileSize <= 0) { + continue; + } + + target.mDemuxer->Reset(); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + ASSERT_TRUE(target.mDemuxer->Duration()); + const auto duration = target.mDemuxer->Duration().value(); + const auto pos = duration + TimeUnit::FromMicroseconds(1e6); + + // Attempt to seek 1 second past the end of stream. + target.mDemuxer->Seek(pos); + // The seek should bring us to the end of the stream. + EXPECT_NEAR(duration.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * duration.ToMicroseconds()); + + // Since we're at the end of the stream, there should be no frames left. + frameData = target.mDemuxer->DemuxSample(); + ASSERT_FALSE(frameData); + } +} + +TEST_F(MP3DemuxerTest, Seek) { + for (const auto& target : mTargets) { + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + const auto seekTime = TimeUnit::FromSeconds(1); + auto pos = target.mDemuxer->SeekPosition(); + + while (frameData) { + EXPECT_NEAR(pos.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * pos.ToMicroseconds()); + + pos += seekTime; + target.mDemuxer->Seek(pos); + frameData = target.mDemuxer->DemuxSample(); + } + } + + // Seeking should work with in-between resets, too. + for (const auto& target : mTargets) { + target.mDemuxer->Reset(); + RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); + ASSERT_TRUE(frameData); + + const auto seekTime = TimeUnit::FromSeconds(1); + auto pos = target.mDemuxer->SeekPosition(); + + while (frameData) { + EXPECT_NEAR(pos.ToMicroseconds(), + target.mDemuxer->SeekPosition().ToMicroseconds(), + target.mSeekError * pos.ToMicroseconds()); + + pos += seekTime; + target.mDemuxer->Reset(); + target.mDemuxer->Seek(pos); + frameData = target.mDemuxer->DemuxSample(); + } + } +} |