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   uint32_t mMPEGLayer;
65   uint32_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   uint32_t mSampleRate;
74   uint32_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   uint32_t mBitrate;
80   uint32_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     uint64_t bitrateSum = 0;
422     uint32_t numFrames = 0;
423     uint32_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     EXPECT_GE(numSamples, 0u);
460 
461     // There may be trailing headers which we don't parse, so the stream length
462     // is the upper bound.
463     if (target.mFileSize > 0) {
464       EXPECT_GE(target.mFileSize, parsedLength);
465     }
466 
467     if (target.mIsVBR) {
468       ASSERT_TRUE(numFrames);
469       EXPECT_EQ(target.mBitrate, bitrateSum / numFrames);
470     }
471   }
472 }
473 
TEST_F(MP3DemuxerTest,Duration)474 TEST_F(MP3DemuxerTest, Duration) {
475   for (const auto& target : mTargets) {
476     RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
477     ASSERT_TRUE(frameData);
478     EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength());
479 
480     while (frameData) {
481       if (target.mDuration) {
482         ASSERT_TRUE(target.mDemuxer->Duration());
483         EXPECT_NEAR(target.mDuration->mMicroseconds,
484                     target.mDemuxer->Duration()->ToMicroseconds(),
485                     target.mDuration->Tolerance());
486       } else {
487         EXPECT_FALSE(target.mDemuxer->Duration());
488       }
489       frameData = target.mDemuxer->DemuxSample();
490     }
491   }
492 
493   // Seek out of range tests.
494   for (const auto& target : mTargets) {
495     // Skip tests for stream media resources because of lacking duration.
496     if (target.mFileSize <= 0) {
497       continue;
498     }
499 
500     target.mDemuxer->Reset();
501     RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
502     ASSERT_TRUE(frameData);
503 
504     ASSERT_TRUE(target.mDemuxer->Duration());
505     const auto duration = target.mDemuxer->Duration().value();
506     const auto pos = duration + TimeUnit::FromMicroseconds(1e6);
507 
508     // Attempt to seek 1 second past the end of stream.
509     target.mDemuxer->Seek(pos);
510     // The seek should bring us to the end of the stream.
511     EXPECT_NEAR(duration.ToMicroseconds(),
512                 target.mDemuxer->SeekPosition().ToMicroseconds(),
513                 target.mSeekError * duration.ToMicroseconds());
514 
515     // Since we're at the end of the stream, there should be no frames left.
516     frameData = target.mDemuxer->DemuxSample();
517     ASSERT_FALSE(frameData);
518   }
519 }
520 
TEST_F(MP3DemuxerTest,Seek)521 TEST_F(MP3DemuxerTest, Seek) {
522   for (const auto& target : mTargets) {
523     RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
524     ASSERT_TRUE(frameData);
525 
526     const auto seekTime = TimeUnit::FromSeconds(1);
527     auto pos = target.mDemuxer->SeekPosition();
528 
529     while (frameData) {
530       EXPECT_NEAR(pos.ToMicroseconds(),
531                   target.mDemuxer->SeekPosition().ToMicroseconds(),
532                   target.mSeekError * pos.ToMicroseconds());
533 
534       pos += seekTime;
535       target.mDemuxer->Seek(pos);
536       frameData = target.mDemuxer->DemuxSample();
537     }
538   }
539 
540   // Seeking should work with in-between resets, too.
541   for (const auto& target : mTargets) {
542     target.mDemuxer->Reset();
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->Reset();
556       target.mDemuxer->Seek(pos);
557       frameData = target.mDemuxer->DemuxSample();
558     }
559   }
560 }
561