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