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