TestMP3Demuxer.cpp (19635B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include <gtest/gtest.h> 8 9 #include <vector> 10 11 #include "MP3Demuxer.h" 12 #include "MockMediaResource.h" 13 14 class MockMP3MediaResource; 15 class MockMP3StreamMediaResource; 16 namespace mozilla { 17 DDLoggedTypeNameAndBase(::MockMP3MediaResource, MockMediaResource); 18 DDLoggedTypeNameAndBase(::MockMP3StreamMediaResource, MockMP3MediaResource); 19 } // namespace mozilla 20 21 using namespace mozilla; 22 using media::TimeUnit; 23 24 // Regular MP3 file mock resource. 25 class MockMP3MediaResource 26 : public MockMediaResource, 27 public DecoderDoctorLifeLogger<MockMP3MediaResource> { 28 public: 29 explicit MockMP3MediaResource(const char* aFileName) 30 : MockMediaResource(aFileName) {} 31 32 protected: 33 virtual ~MockMP3MediaResource() = default; 34 }; 35 36 // MP3 stream mock resource. 37 class MockMP3StreamMediaResource 38 : public MockMP3MediaResource, 39 public DecoderDoctorLifeLogger<MockMP3StreamMediaResource> { 40 public: 41 explicit MockMP3StreamMediaResource(const char* aFileName) 42 : MockMP3MediaResource(aFileName) {} 43 44 int64_t GetLength() override { return -1; } 45 46 protected: 47 virtual ~MockMP3StreamMediaResource() = default; 48 }; 49 50 struct MP3Resource { 51 enum class HeaderType { NONE, XING, VBRI }; 52 struct Duration { 53 int64_t mMicroseconds; 54 float mTolerableRate; 55 56 Duration(int64_t aMicroseconds, float aTolerableRate) 57 : mMicroseconds(aMicroseconds), mTolerableRate(aTolerableRate) {} 58 int64_t Tolerance() const { 59 return AssertedCast<int64_t>(mTolerableRate * 60 static_cast<float>(mMicroseconds)); 61 } 62 }; 63 64 const char* mFilePath{}; 65 bool mIsVBR{}; 66 HeaderType mHeaderType{HeaderType::NONE}; 67 int64_t mFileSize{}; 68 uint32_t mMPEGLayer{}; 69 uint32_t mMPEGVersion{}; 70 uint8_t mID3MajorVersion{}; 71 uint8_t mID3MinorVersion{}; 72 uint8_t mID3Flags{}; 73 uint32_t mID3Size{}; 74 75 Maybe<Duration> mDuration; 76 float mSeekError{}; 77 uint32_t mSampleRate{}; 78 uint32_t mSamplesPerFrame{}; 79 uint32_t mNumSamples{}; 80 uint32_t mPadding{}; 81 uint32_t mEncoderDelay{}; 82 uint32_t mBitrate{}; 83 uint32_t mSlotSize{}; 84 int32_t mPrivate{}; 85 86 // The first n frame offsets. 87 std::vector<int32_t> mSyncOffsets; 88 RefPtr<MockMP3MediaResource> mResource; 89 RefPtr<MP3TrackDemuxer> mDemuxer; 90 }; 91 92 class MP3DemuxerTest : public ::testing::Test { 93 protected: 94 void SetUp() override { 95 { 96 MP3Resource res; 97 res.mFilePath = "noise.mp3"; 98 res.mIsVBR = false; 99 res.mHeaderType = MP3Resource::HeaderType::NONE; 100 res.mFileSize = 965257; 101 res.mMPEGLayer = 3; 102 res.mMPEGVersion = 1; 103 res.mID3MajorVersion = 3; 104 res.mID3MinorVersion = 0; 105 res.mID3Flags = 0; 106 res.mID3Size = 2141; 107 // The tolerance comes from the fact that this file has ID3v1 information 108 // at the end, this trips our CBR duration calculation. The file has 109 // however the correct duration when decoded / demuxed completely. 110 res.mDuration = Some(MP3Resource::Duration{30093063, 0.00015f}); 111 res.mSeekError = 0.02f; 112 res.mSampleRate = 44100; 113 res.mSamplesPerFrame = 1152; 114 res.mNumSamples = 1327104; 115 res.mPadding = 0; 116 res.mEncoderDelay = 0; 117 res.mBitrate = 256000; 118 res.mSlotSize = 1; 119 res.mPrivate = 0; 120 const int syncs[] = {2151, 2987, 3823, 4659, 5495, 6331}; 121 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); 122 123 // No content length can be estimated for CBR stream resources. 124 MP3Resource streamRes = res; 125 streamRes.mFileSize = -1; 126 streamRes.mDuration = Nothing(); 127 128 res.mResource = new MockMP3MediaResource(res.mFilePath); 129 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 130 mTargets.push_back(res); 131 132 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 133 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 134 mTargets.push_back(streamRes); 135 } 136 137 { 138 MP3Resource res; 139 // This file trips up the MP3 demuxer if ID3v2 tags aren't properly 140 // skipped. If skipping is not properly implemented, depending on the 141 // strictness of the MPEG frame parser a false sync will be detected 142 // somewhere within the metadata at or after 112087, or failing that, at 143 // the artificially added extraneous header at 114532. 144 res.mFilePath = "id3v2header.mp3"; 145 res.mIsVBR = false; 146 res.mHeaderType = MP3Resource::HeaderType::NONE; 147 res.mFileSize = 191302; 148 res.mMPEGLayer = 3; 149 res.mMPEGVersion = 1; 150 res.mID3MajorVersion = 3; 151 res.mID3MinorVersion = 0; 152 res.mID3Flags = 0; 153 res.mID3Size = 115304; 154 // The tolerance comes from the fact that this file has ID3v1 information 155 // at the end, this trips our CBR duration calculation. The file has 156 // however the correct duration when decoded / demuxed completely. 157 res.mDuration = Some(MP3Resource::Duration{3160833, 0.0017f}); 158 res.mSeekError = 0.02f; 159 res.mSampleRate = 44100; 160 res.mSamplesPerFrame = 1152; 161 res.mNumSamples = 139392; 162 res.mPadding = 0; 163 res.mEncoderDelay = 0; 164 res.mBitrate = 192000; 165 res.mSlotSize = 1; 166 res.mPrivate = 1; 167 const int syncs[] = {115314, 115941, 116568, 117195, 117822, 118449}; 168 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); 169 170 // No content length can be estimated for CBR stream resources. 171 MP3Resource streamRes = res; 172 streamRes.mFileSize = -1; 173 streamRes.mDuration = Nothing(); 174 175 res.mResource = new MockMP3MediaResource(res.mFilePath); 176 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 177 mTargets.push_back(res); 178 179 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 180 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 181 mTargets.push_back(streamRes); 182 } 183 184 { 185 MP3Resource res; 186 res.mFilePath = "noise_vbr.mp3"; 187 res.mIsVBR = true; 188 res.mHeaderType = MP3Resource::HeaderType::XING; 189 res.mFileSize = 583679; 190 res.mMPEGLayer = 3; 191 res.mMPEGVersion = 1; 192 res.mID3MajorVersion = 3; 193 res.mID3MinorVersion = 0; 194 res.mID3Flags = 0; 195 res.mID3Size = 2221; 196 res.mDuration = Some(MP3Resource::Duration{30081065, 0.f}); 197 res.mSeekError = 0.02f; 198 res.mSampleRate = 44100; 199 res.mSamplesPerFrame = 1152; 200 res.mNumSamples = 1326575; 201 res.mPadding = 576; 202 res.mEncoderDelay = 2257; 203 res.mBitrate = 154000; 204 res.mSlotSize = 1; 205 res.mPrivate = 0; 206 const int syncs[] = {2231, 2648, 2752, 3796, 4318, 4735}; 207 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); 208 209 // VBR stream resources contain header info on total frames numbers, which 210 // is used to estimate the total duration. 211 MP3Resource streamRes = res; 212 streamRes.mFileSize = -1; 213 214 res.mResource = new MockMP3MediaResource(res.mFilePath); 215 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 216 mTargets.push_back(res); 217 218 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 219 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 220 mTargets.push_back(streamRes); 221 } 222 223 { 224 MP3Resource res; 225 res.mFilePath = "small-shot.mp3"; 226 res.mIsVBR = true; 227 res.mHeaderType = MP3Resource::HeaderType::XING; 228 res.mFileSize = 6825; 229 res.mMPEGLayer = 3; 230 res.mMPEGVersion = 1; 231 res.mID3MajorVersion = 4; 232 res.mID3MinorVersion = 0; 233 res.mID3Flags = 0; 234 res.mID3Size = 24; 235 res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); 236 res.mSeekError = 0.2f; 237 res.mSampleRate = 44100; 238 res.mSamplesPerFrame = 1152; 239 res.mNumSamples = 12; 240 res.mPadding = 0; 241 res.mEncoderDelay = 1152 + 529; 242 res.mBitrate = 256000; 243 res.mSlotSize = 1; 244 res.mPrivate = 0; 245 const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, 246 3691, 4213, 4736, 5258, 5781, 6303}; 247 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); 248 249 // No content length can be estimated for CBR stream resources. 250 MP3Resource streamRes = res; 251 streamRes.mFileSize = -1; 252 253 res.mResource = new MockMP3MediaResource(res.mFilePath); 254 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 255 mTargets.push_back(res); 256 257 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 258 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 259 mTargets.push_back(streamRes); 260 } 261 262 { 263 MP3Resource res; 264 // This file contains a false frame sync at 34, just after the ID3 tag, 265 // which should be identified as a false positive and skipped. 266 res.mFilePath = "small-shot-false-positive.mp3"; 267 res.mIsVBR = true; 268 res.mHeaderType = MP3Resource::HeaderType::XING; 269 res.mFileSize = 6845; 270 res.mMPEGLayer = 3; 271 res.mMPEGVersion = 1; 272 res.mID3MajorVersion = 4; 273 res.mID3MinorVersion = 0; 274 res.mID3Flags = 0; 275 res.mID3Size = 24; 276 res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); 277 res.mSeekError = 0.2f; 278 res.mSampleRate = 44100; 279 res.mSamplesPerFrame = 1152; 280 res.mNumSamples = 12; 281 res.mPadding = 0; 282 res.mEncoderDelay = 1681; 283 res.mBitrate = 256000; 284 res.mSlotSize = 1; 285 res.mPrivate = 0; 286 const int syncs[] = {54, 576, 1098, 1621, 2143, 2666, 3188, 287 3711, 4233, 4756, 5278, 5801, 6323}; 288 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); 289 290 // No content length can be estimated for CBR stream resources. 291 MP3Resource streamRes = res; 292 streamRes.mFileSize = -1; 293 294 res.mResource = new MockMP3MediaResource(res.mFilePath); 295 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 296 mTargets.push_back(res); 297 298 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 299 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 300 mTargets.push_back(streamRes); 301 } 302 303 { 304 MP3Resource res; 305 res.mFilePath = "small-shot-partial-xing.mp3"; 306 res.mIsVBR = true; 307 res.mHeaderType = MP3Resource::HeaderType::XING; 308 res.mFileSize = 6825; 309 res.mMPEGLayer = 3; 310 res.mMPEGVersion = 1; 311 res.mID3MajorVersion = 4; 312 res.mID3MinorVersion = 0; 313 res.mID3Flags = 0; 314 res.mID3Size = 24; 315 res.mDuration = Some(MP3Resource::Duration{301473, 0.f}); 316 res.mSeekError = 0.2f; 317 res.mSampleRate = 44100; 318 res.mSamplesPerFrame = 1152; 319 res.mNumSamples = 12; 320 res.mPadding = 0; 321 res.mEncoderDelay = 1681; 322 res.mBitrate = 256000; 323 res.mSlotSize = 1; 324 res.mPrivate = 0; 325 const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168, 326 3691, 4213, 4736, 5258, 5781, 6303}; 327 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13); 328 329 // No content length can be estimated for CBR stream resources. 330 MP3Resource streamRes = res; 331 streamRes.mFileSize = -1; 332 333 res.mResource = new MockMP3MediaResource(res.mFilePath); 334 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 335 mTargets.push_back(res); 336 337 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 338 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 339 mTargets.push_back(streamRes); 340 } 341 342 { 343 MP3Resource res; 344 res.mFilePath = "test_vbri.mp3"; 345 res.mIsVBR = true; 346 res.mHeaderType = MP3Resource::HeaderType::VBRI; 347 res.mFileSize = 16519; 348 res.mMPEGLayer = 3; 349 res.mMPEGVersion = 1; 350 res.mID3MajorVersion = 3; 351 res.mID3MinorVersion = 0; 352 res.mID3Flags = 0; 353 res.mID3Size = 4202; 354 res.mDuration = Some(MP3Resource::Duration{731428, 0.f}); 355 res.mSeekError = 0.02f; 356 res.mSampleRate = 44100; 357 res.mSamplesPerFrame = 1152; 358 res.mNumSamples = 29; 359 res.mPadding = 0; 360 res.mEncoderDelay = 1152; 361 res.mBitrate = 0; 362 res.mSlotSize = 1; 363 res.mPrivate = 0; 364 const int syncs[] = {4212, 4734, 5047, 5464, 5986, 6403}; 365 res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6); 366 367 // VBR stream resources contain header info on total frames numbers, which 368 // is used to estimate the total duration. 369 MP3Resource streamRes = res; 370 streamRes.mFileSize = -1; 371 372 res.mResource = new MockMP3MediaResource(res.mFilePath); 373 res.mDemuxer = new MP3TrackDemuxer(res.mResource); 374 mTargets.push_back(res); 375 376 streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath); 377 streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource); 378 mTargets.push_back(streamRes); 379 } 380 381 for (auto& target : mTargets) { 382 ASSERT_EQ(NS_OK, target.mResource->Open()); 383 ASSERT_TRUE(target.mDemuxer->Init()); 384 } 385 } 386 387 std::vector<MP3Resource> mTargets; 388 }; 389 390 TEST_F(MP3DemuxerTest, ID3Tags) { 391 for (const auto& target : mTargets) { 392 RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); 393 ASSERT_TRUE(frame); 394 395 const auto& id3 = target.mDemuxer->ID3Header(); 396 ASSERT_TRUE(id3.IsValid()); 397 398 EXPECT_EQ(target.mID3MajorVersion, id3.MajorVersion()); 399 EXPECT_EQ(target.mID3MinorVersion, id3.MinorVersion()); 400 EXPECT_EQ(target.mID3Flags, id3.Flags()); 401 EXPECT_EQ(target.mID3Size, id3.Size()); 402 } 403 } 404 405 TEST_F(MP3DemuxerTest, VBRHeader) { 406 for (const auto& target : mTargets) { 407 RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample()); 408 ASSERT_TRUE(frame); 409 410 const auto& vbr = target.mDemuxer->VBRInfo(); 411 412 if (target.mHeaderType == MP3Resource::HeaderType::XING) { 413 EXPECT_EQ(FrameParser::VBRHeader::XING, vbr.Type()); 414 } else if (target.mHeaderType == MP3Resource::HeaderType::VBRI) { 415 EXPECT_TRUE(target.mIsVBR); 416 EXPECT_EQ(FrameParser::VBRHeader::VBRI, vbr.Type()); 417 } else { // MP3Resource::HeaderType::NONE 418 EXPECT_EQ(FrameParser::VBRHeader::NONE, vbr.Type()); 419 EXPECT_FALSE(vbr.NumAudioFrames()); 420 } 421 } 422 } 423 424 TEST_F(MP3DemuxerTest, FrameParsing) { 425 for (const auto& target : mTargets) { 426 printf("Testing: %s\n", target.mFilePath); 427 RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); 428 ASSERT_TRUE(frameData); 429 EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); 430 431 const auto& id3 = target.mDemuxer->ID3Header(); 432 ASSERT_TRUE(id3.IsValid()); 433 434 int64_t parsedLength = id3.Size(); 435 uint64_t bitrateSum = 0; 436 uint32_t numFrames = 0; 437 uint32_t numSamples = 0; 438 439 while (frameData) { 440 if (static_cast<int64_t>(target.mSyncOffsets.size()) > numFrames) { 441 // Test sync offsets. 442 EXPECT_EQ(target.mSyncOffsets[numFrames], frameData->mOffset); 443 } 444 445 ++numFrames; 446 parsedLength += AssertedCast<int64_t>(frameData->Size()); 447 448 const auto& frame = target.mDemuxer->LastFrame(); 449 const auto& header = frame.Header(); 450 ASSERT_TRUE(header.IsValid()); 451 452 numSamples += header.SamplesPerFrame(); 453 454 EXPECT_EQ(target.mMPEGLayer, header.Layer()); 455 EXPECT_EQ(target.mSampleRate, header.SampleRate()); 456 EXPECT_EQ(target.mSamplesPerFrame, header.SamplesPerFrame()); 457 EXPECT_EQ(target.mSlotSize, header.SlotSize()); 458 EXPECT_EQ(target.mPrivate, header.Private()); 459 460 if (target.mIsVBR) { 461 // Used to compute the average bitrate for VBR streams. 462 bitrateSum += target.mBitrate; 463 } else { 464 EXPECT_EQ(target.mBitrate, header.Bitrate()); 465 } 466 467 frameData = target.mDemuxer->DemuxSample(); 468 } 469 470 EXPECT_EQ(target.mPadding, target.mDemuxer->PaddingFrames()); 471 EXPECT_EQ(target.mEncoderDelay, target.mDemuxer->EncoderDelayFrames()); 472 EXPECT_GE(numSamples, 0u); 473 474 // There may be trailing headers which we don't parse, so the stream length 475 // is the upper bound. 476 if (target.mFileSize > 0) { 477 EXPECT_GE(target.mFileSize, parsedLength); 478 } 479 480 if (target.mIsVBR) { 481 ASSERT_TRUE(numFrames); 482 EXPECT_EQ(target.mBitrate, bitrateSum / numFrames); 483 } 484 } 485 } 486 487 TEST_F(MP3DemuxerTest, Duration) { 488 for (const auto& target : mTargets) { 489 printf("Testing: %s\n", target.mFilePath); 490 RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); 491 ASSERT_TRUE(frameData); 492 EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength()); 493 494 while (frameData) { 495 if (target.mDuration) { 496 ASSERT_TRUE(target.mDemuxer->Duration()); 497 EXPECT_NEAR(target.mDuration->mMicroseconds, 498 target.mDemuxer->Duration()->ToMicroseconds(), 499 target.mDuration->Tolerance()); 500 } else { 501 EXPECT_FALSE(target.mDemuxer->Duration()); 502 } 503 frameData = target.mDemuxer->DemuxSample(); 504 } 505 if (target.mDuration) { 506 // At the end, the durations should always be exact. 507 EXPECT_EQ(target.mDuration->mMicroseconds, 508 target.mDemuxer->Duration()->ToMicroseconds()); 509 } 510 } 511 512 // Seek out of range tests. 513 for (const auto& target : mTargets) { 514 printf("Testing %s\n", target.mFilePath); 515 // Skip tests for stream media resources because of lacking duration. 516 if (target.mFileSize <= 0) { 517 continue; 518 } 519 520 target.mDemuxer->Reset(); 521 RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); 522 ASSERT_TRUE(frameData); 523 524 ASSERT_TRUE(target.mDemuxer->Duration()); 525 const auto duration = target.mDemuxer->Duration().value(); 526 const auto pos = duration + TimeUnit::FromMicroseconds(1e6); 527 528 // Attempt to seek 1 second past the end of stream. 529 target.mDemuxer->Seek(pos); 530 // The seek should bring us to the end of the stream. 531 EXPECT_NEAR(duration.ToMicroseconds(), 532 target.mDemuxer->SeekPosition().ToMicroseconds(), 533 target.mSeekError * duration.ToMicroseconds()); 534 535 // Since we're at the end of the stream, there should be no frames left. 536 frameData = target.mDemuxer->DemuxSample(); 537 ASSERT_FALSE(frameData); 538 } 539 } 540 541 TEST_F(MP3DemuxerTest, Seek) { 542 for (const auto& target : mTargets) { 543 RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); 544 ASSERT_TRUE(frameData); 545 546 const auto seekTime = TimeUnit::FromSeconds(1); 547 auto pos = target.mDemuxer->SeekPosition(); 548 549 while (frameData) { 550 EXPECT_NEAR(pos.ToMicroseconds(), 551 target.mDemuxer->SeekPosition().ToMicroseconds(), 552 target.mSeekError * pos.ToMicroseconds()); 553 554 pos += seekTime; 555 target.mDemuxer->Seek(pos); 556 frameData = target.mDemuxer->DemuxSample(); 557 } 558 } 559 560 // Seeking should work with in-between resets, too. 561 for (const auto& target : mTargets) { 562 target.mDemuxer->Reset(); 563 RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample()); 564 ASSERT_TRUE(frameData); 565 566 const auto seekTime = TimeUnit::FromSeconds(1); 567 auto pos = target.mDemuxer->SeekPosition(); 568 569 while (frameData) { 570 EXPECT_NEAR(pos.ToMicroseconds(), 571 target.mDemuxer->SeekPosition().ToMicroseconds(), 572 target.mSeekError * pos.ToMicroseconds()); 573 574 pos += seekTime; 575 target.mDemuxer->Reset(); 576 target.mDemuxer->Seek(pos); 577 frameData = target.mDemuxer->DemuxSample(); 578 } 579 } 580 }