xref: /aosp_15_r20/cts/tests/tests/media/extractor/src/android/media/extractor/cts/MediaExtractorTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media.extractor.cts;
18 
19 import static android.media.MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION;
20 
21 import static com.android.media.extractor.flags.Flags.FLAG_EXTRACTOR_MP4_ENABLE_APV;
22 
23 import static org.junit.Assert.assertEquals;
24 import static org.junit.Assert.assertNotNull;
25 import static org.junit.Assert.assertTrue;
26 import static org.junit.Assert.fail;
27 import static org.junit.Assume.assumeTrue;
28 
29 import android.content.Context;
30 import android.content.res.AssetFileDescriptor;
31 import android.hardware.display.DisplayManager;
32 import android.icu.util.ULocale;
33 import android.media.AudioFormat;
34 import android.media.AudioPresentation;
35 import android.media.MediaCodec;
36 import android.media.MediaCodecInfo;
37 import android.media.MediaDataSource;
38 import android.media.MediaExtractor;
39 import android.media.MediaFormat;
40 import android.media.cts.StreamUtils;
41 import android.media.cts.TestMediaDataSource;
42 import android.os.Build;
43 import android.os.ParcelFileDescriptor;
44 import android.os.PersistableBundle;
45 import android.platform.test.annotations.AppModeFull;
46 import android.platform.test.annotations.RequiresFlagsEnabled;
47 import android.platform.test.flag.junit.CheckFlagsRule;
48 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
49 import android.util.Log;
50 import android.view.Display;
51 import android.view.Display.HdrCapabilities;
52 import android.webkit.cts.CtsTestServer;
53 
54 import androidx.test.ext.junit.runners.AndroidJUnit4;
55 import androidx.test.filters.SmallTest;
56 import androidx.test.platform.app.InstrumentationRegistry;
57 
58 import com.android.compatibility.common.util.ApiLevelUtil;
59 import com.android.compatibility.common.util.ApiTest;
60 import com.android.compatibility.common.util.CddTest;
61 import com.android.compatibility.common.util.FrameworkSpecificTest;
62 import com.android.compatibility.common.util.MediaUtils;
63 import com.android.compatibility.common.util.Preconditions;
64 
65 import org.junit.After;
66 import org.junit.Before;
67 import org.junit.Rule;
68 import org.junit.Test;
69 import org.junit.runner.RunWith;
70 
71 import java.io.Closeable;
72 import java.io.File;
73 import java.io.FileNotFoundException;
74 import java.io.IOException;
75 import java.nio.ByteBuffer;
76 import java.util.Arrays;
77 import java.util.List;
78 import java.util.Map;
79 import java.util.SortedMap;
80 import java.util.TreeMap;
81 
82 @AppModeFull(reason = "Instant apps cannot access the SD card")
83 @RunWith(AndroidJUnit4.class)
84 public class MediaExtractorTest {
85     private static final String TAG = "MediaExtractorTest";
86     private static final boolean IS_AT_LEAST_S = ApiLevelUtil.isAtLeast(Build.VERSION_CODES.S);
87     private static final boolean IS_AT_LEAST_T =
88             ApiLevelUtil.isAtLeast(Build.VERSION_CODES.TIRAMISU);
89     public static final boolean IS_AT_LEAST_U =
90             ApiLevelUtil.isAtLeast(Build.VERSION_CODES.UPSIDE_DOWN_CAKE);
91     static final String mInpPrefix = WorkDir.getMediaDirString();
92     protected MediaExtractor mExtractor;
93 
94     @Rule
95     public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
96     @Before
setUp()97     public void setUp() throws Exception {
98         mExtractor = new MediaExtractor();
99     }
100 
101     @After
tearDown()102     public void tearDown() throws Exception {
103         mExtractor.release();
104     }
105 
getContext()106     private Context getContext() {
107         return InstrumentationRegistry.getInstrumentation().getContext();
108     }
109 
getAssetFileDescriptorFor(final String res)110     private AssetFileDescriptor getAssetFileDescriptorFor(final String res)
111             throws FileNotFoundException {
112         File inpFile = new File(mInpPrefix + res);
113         Preconditions.assertTestFileExists(mInpPrefix + res);
114         ParcelFileDescriptor parcelFD =
115                 ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY);
116         return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize());
117     }
118 
getDataSourceFor(final String res)119     private TestMediaDataSource getDataSourceFor(final String res) throws Exception {
120         AssetFileDescriptor afd = getAssetFileDescriptorFor(res);
121         return TestMediaDataSource.fromAssetFd(afd);
122     }
123 
setDataSource(final String res)124     private TestMediaDataSource setDataSource(final String res) throws Exception {
125         TestMediaDataSource ds = getDataSourceFor(res);
126         mExtractor.setDataSource(ds);
127         return ds;
128     }
129 
130     @Test
131     @FrameworkSpecificTest
testExtractorFrameworkStub()132     public void testExtractorFrameworkStub() throws Exception {
133         Log.d(TAG, "Stub FrameworkSpecific tests are non-empty");
134     }
135 
136     @Test
testExtractorFailsIfMediaDataSourceReturnsAnError()137     public void testExtractorFailsIfMediaDataSourceReturnsAnError() throws Exception {
138         TestMediaDataSource dataSource = getDataSourceFor("testvideo.3gp");
139         dataSource.returnFromReadAt(-2);
140         try {
141             mExtractor.setDataSource(dataSource);
142             fail("Expected IOException.");
143         } catch (IOException e) {
144             // Expected.
145         }
146     }
147 
148     @CddTest(requirement = "5.3.8")
149     @RequiresFlagsEnabled(FLAG_EXTRACTOR_MP4_ENABLE_APV)
150     @Test
testApvMediaExtractor()151     public void testApvMediaExtractor() throws Exception {
152         TestMediaDataSource dataSource =
153                 setDataSource("pattern_640x480_30fps_8213kbps_apv_10bit.mp4");
154 
155         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
156 
157         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
158         assertEquals("video/apv", mimeType);
159     }
160 
advertisesDolbyVision()161     private boolean advertisesDolbyVision() {
162         // Device advertises support for DV if 1) it has a DV decoder, OR
163         // 2) it lists DV on the Display HDR capabilities.
164         if (MediaUtils.hasDecoder(MIMETYPE_VIDEO_DOLBY_VISION)) {
165             return true;
166         }
167 
168         DisplayManager displayManager = getContext().getSystemService(DisplayManager.class);
169         Display defaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
170         HdrCapabilities cap = defaultDisplay.getHdrCapabilities();
171         for (int type : cap.getSupportedHdrTypes()) {
172             if (type == HdrCapabilities.HDR_TYPE_DOLBY_VISION) {
173                 return true;
174             }
175         }
176         return false;
177     }
178 
179     // DolbyVisionMediaExtractor for profile-level (DvheDtr/Fhd30).
180     @CddTest(requirement="5.3.8")
181     @Test
testDolbyVisionMediaExtractorProfileDvheDtr()182     public void testDolbyVisionMediaExtractorProfileDvheDtr() throws Exception {
183         TestMediaDataSource dataSource = setDataSource("video_dovi_1920x1080_30fps_dvhe_04.mp4");
184 
185         assertTrue("There should be either 1 or 2 tracks",
186             0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount());
187 
188         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
189         int trackCountForDolbyVision = 1;
190 
191         // Handle the case where there is a Dolby Vision extractor
192         // Note that it may or may not have a Dolby Vision Decoder
193         if (mExtractor.getTrackCount() == 2) {
194             if (trackFormat.getString(MediaFormat.KEY_MIME)
195                     .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) {
196                 trackFormat = mExtractor.getTrackFormat(1);
197                 trackCountForDolbyVision = 0;
198             }
199         }
200 
201         if (advertisesDolbyVision()) {
202             assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount());
203 
204             MediaFormat trackFormatForDolbyVision =
205                 mExtractor.getTrackFormat(trackCountForDolbyVision);
206 
207             final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME);
208             assertEquals("video/dolby-vision", mimeType);
209 
210             int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE);
211             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr, profile);
212 
213             int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL);
214             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd30, level);
215 
216             final int trackIdForDolbyVision =
217                 trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID);
218 
219             final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID);
220             assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat);
221         }
222 
223         // The backward-compatible track should have mime video/hevc
224         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
225         assertEquals("video/hevc", mimeType);
226     }
227 
228     // DolbyVisionMediaExtractor for profile-level (DvheSt/Fhd60).
229     @CddTest(requirement="5.3.8")
230     @Test
testDolbyVisionMediaExtractorProfileDvheSt()231     public void testDolbyVisionMediaExtractorProfileDvheSt() throws Exception {
232         TestMediaDataSource dataSource = setDataSource("video_dovi_1920x1080_60fps_dvhe_08.mp4");
233 
234         assertTrue("There should be either 1 or 2 tracks",
235             0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount());
236 
237         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
238         int trackCountForDolbyVision = 1;
239 
240         // Handle the case where there is a Dolby Vision extractor
241         // Note that it may or may not have a Dolby Vision Decoder
242         if (mExtractor.getTrackCount() == 2) {
243             if (trackFormat.getString(MediaFormat.KEY_MIME)
244                     .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) {
245                 trackFormat = mExtractor.getTrackFormat(1);
246                 trackCountForDolbyVision = 0;
247             }
248         }
249 
250         if (advertisesDolbyVision()) {
251             assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount());
252 
253             MediaFormat trackFormatForDolbyVision =
254                 mExtractor.getTrackFormat(trackCountForDolbyVision);
255 
256             final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME);
257             assertEquals("video/dolby-vision", mimeType);
258 
259             int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE);
260             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, profile);
261 
262             int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL);
263             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60, level);
264 
265             final int trackIdForDolbyVision =
266                 trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID);
267 
268             final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID);
269             assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat);
270         }
271 
272         // The backward-compatible track should have mime video/hevc
273         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
274         assertEquals("video/hevc", mimeType);
275     }
276 
277     // DolbyVisionMediaExtractor for profile-level (DvavSe/Fhd60).
278     @CddTest(requirement="5.3.8")
279     @Test
testDolbyVisionMediaExtractorProfileDvavSe()280     public void testDolbyVisionMediaExtractorProfileDvavSe() throws Exception {
281         TestMediaDataSource dataSource = setDataSource("video_dovi_1920x1080_60fps_dvav_09.mp4");
282 
283         assertTrue("There should be either 1 or 2 tracks",
284             0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount());
285 
286         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
287         int trackCountForDolbyVision = 1;
288 
289         // Handle the case where there is a Dolby Vision extractor
290         // Note that it may or may not have a Dolby Vision Decoder
291         if (mExtractor.getTrackCount() == 2) {
292             if (trackFormat.getString(MediaFormat.KEY_MIME)
293                     .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) {
294                 trackFormat = mExtractor.getTrackFormat(1);
295                 trackCountForDolbyVision = 0;
296             }
297         }
298 
299         if (advertisesDolbyVision()) {
300             assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount());
301 
302             MediaFormat trackFormatForDolbyVision =
303                 mExtractor.getTrackFormat(trackCountForDolbyVision);
304 
305             final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME);
306             assertEquals("video/dolby-vision", mimeType);
307 
308             int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE);
309             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavSe, profile);
310 
311             int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL);
312             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60, level);
313 
314             final int trackIdForDolbyVision =
315                 trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID);
316 
317             final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID);
318             assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat);
319         }
320 
321         // The backward-compatible track should have mime video/avc
322         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
323         assertEquals("video/avc", mimeType);
324     }
325 
326     // DolbyVisionMediaExtractor for profile-level (Dvav1 10.0/Uhd30)
327     @SmallTest
328     @CddTest(requirement="5.3.8")
329     @Test
testDolbyVisionMediaExtractorProfileDvav1()330     public void testDolbyVisionMediaExtractorProfileDvav1() throws Exception {
331         TestMediaDataSource dataSource = setDataSource("video_dovi_3840x2160_30fps_dav1_10.mp4");
332 
333         if (advertisesDolbyVision()) {
334             assertEquals(1, mExtractor.getTrackCount());
335 
336             // Dvav1 10 exposes a single backward compatible track.
337             final MediaFormat trackFormat = mExtractor.getTrackFormat(0);
338             final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
339 
340             assertEquals("video/dolby-vision", mimeType);
341 
342             final int profile = trackFormat.getInteger(MediaFormat.KEY_PROFILE);
343             final int level = trackFormat.getInteger(MediaFormat.KEY_LEVEL);
344 
345             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvav110, profile);
346             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd30, level);
347         } else {
348             MediaUtils.skipTest("Device does not provide a Dolby Vision decoder");
349         }
350     }
351 
352     // DolbyVisionMediaExtractor for profile-level (Dvav1 10.1/Uhd30)
353     @SmallTest
354     @CddTest(requirement="5.3.8")
355     @Test
testDolbyVisionMediaExtractorProfileDvav1_2()356     public void testDolbyVisionMediaExtractorProfileDvav1_2() throws Exception {
357         TestMediaDataSource dataSource = setDataSource("video_dovi_3840x2160_30fps_dav1_10_2.mp4");
358 
359         assertTrue("There should be either 1 or 2 tracks",
360             0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount());
361 
362         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
363         int trackCountForDolbyVision = 1;
364 
365         // Handle the case where there is a Dolby Vision extractor
366         // Note that it may or may not have a Dolby Vision Decoder
367         if (mExtractor.getTrackCount() == 2) {
368             if (trackFormat.getString(MediaFormat.KEY_MIME)
369                     .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) {
370                 trackFormat = mExtractor.getTrackFormat(1);
371                 trackCountForDolbyVision = 0;
372             }
373         }
374 
375         if (advertisesDolbyVision()) {
376             assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount());
377 
378             MediaFormat trackFormatForDolbyVision =
379                 mExtractor.getTrackFormat(trackCountForDolbyVision);
380 
381             final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME);
382             assertEquals("video/dolby-vision", mimeType);
383 
384             int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE);
385             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvav110, profile);
386 
387             int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL);
388             assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelUhd30, level);
389 
390             final int trackIdForDolbyVision =
391                 trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID);
392 
393             final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID);
394             assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat);
395         }
396 
397         // The backward-compatible track should have mime video/av01
398         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
399         assertEquals("video/av01", mimeType);
400     }
401 
402     //MPEG-H 3D Audio single stream (mha1)
403     @Test
testMpegh3dAudioMediaExtractorMha1()404     public void testMpegh3dAudioMediaExtractorMha1() throws Exception {
405         TestMediaDataSource dataSource = setDataSource("sample_mpegh_mha1.mp4");
406         assertEquals(1, mExtractor.getTrackCount());
407 
408         // The following values below require API Build.VERSION_CODES.S
409         if (!MediaUtils.check(IS_AT_LEAST_S, "test needs Android 12")) return;
410 
411         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
412         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
413         assertEquals(MediaFormat.MIMETYPE_AUDIO_MPEGH_MHA1, mimeType);
414 
415         final int hpli = trackFormat.getInteger(MediaFormat.KEY_MPEGH_PROFILE_LEVEL_INDICATION);
416         assertEquals(0x0D, hpli);
417 
418         final int hrcl = trackFormat.getInteger(MediaFormat.KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT);
419         assertEquals(0x13, hrcl);
420     }
421 
422     //MPEG-H 3D Audio single stream encapsulated in MHAS (mhm1)
423     @Test
testMpegh3dAudioMediaExtractorMhm1()424     public void testMpegh3dAudioMediaExtractorMhm1() throws Exception {
425         TestMediaDataSource dataSource = setDataSource("sample_mpegh_mhm1.mp4");
426         assertEquals(1, mExtractor.getTrackCount());
427 
428         // The following values below require API Build.VERSION_CODES.S
429         if (!MediaUtils.check(IS_AT_LEAST_S, "test needs Android 12")) return;
430 
431         MediaFormat trackFormat = mExtractor.getTrackFormat(0);
432         final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME);
433         assertEquals(MediaFormat.MIMETYPE_AUDIO_MPEGH_MHM1, mimeType);
434 
435         final int hpli = trackFormat.getInteger(MediaFormat.KEY_MPEGH_PROFILE_LEVEL_INDICATION);
436         assertEquals(0x0D, hpli);
437 
438         final int hrcl = trackFormat.getInteger(MediaFormat.KEY_MPEGH_REFERENCE_CHANNEL_LAYOUT);
439         assertEquals(0x13, hrcl);
440 
441         final ByteBuffer hcos = trackFormat.getByteBuffer(MediaFormat.KEY_MPEGH_COMPATIBLE_SETS);
442         assertEquals(0x12, hcos.get());
443     }
444 
checkExtractorSamplesAndMetrics()445     private void checkExtractorSamplesAndMetrics() {
446         // 1MB is enough for any sample.
447         final ByteBuffer buf = ByteBuffer.allocate(1024*1024);
448         final int trackCount = mExtractor.getTrackCount();
449 
450         for (int i = 0; i < trackCount; i++) {
451             mExtractor.selectTrack(i);
452         }
453 
454         for (int i = 0; i < trackCount; i++) {
455             assertTrue(mExtractor.readSampleData(buf, 0) > 0);
456             assertTrue(mExtractor.advance());
457         }
458 
459         // verify some getMetrics() behaviors while we're here.
460         PersistableBundle metrics = mExtractor.getMetrics();
461         if (metrics == null) {
462             fail("getMetrics() returns no data");
463         } else {
464             // ensure existence of some known fields
465             int tracks = metrics.getInt(MediaExtractor.MetricsConstants.TRACKS, -1);
466             if (tracks != trackCount) {
467                 fail("getMetrics() trackCount expect " + trackCount + " got " + tracks);
468             }
469         }
470     }
471 
audioPresentationSetMatchesReference( Map<Integer, AudioPresentation> reference, List<AudioPresentation> actual)472     static boolean audioPresentationSetMatchesReference(
473             Map<Integer, AudioPresentation> reference,
474             List<AudioPresentation> actual) {
475         if (reference.size() != actual.size()) {
476             Log.w(TAG, "AudioPresentations set size is invalid, expected: " +
477                     reference.size() + ", actual: " + actual.size());
478             return false;
479         }
480         for (AudioPresentation ap : actual) {
481             AudioPresentation refAp = reference.get(ap.getPresentationId());
482             if (refAp == null) {
483                 Log.w(TAG, "AudioPresentation not found in the reference set, presentation id=" +
484                         ap.getPresentationId());
485                 return false;
486             }
487             if (!refAp.equals(ap)) {
488                 Log.w(TAG, "AudioPresentations are different, reference: " +
489                         refAp + ", actual: " + ap);
490                 return false;
491             }
492         }
493         return true;
494     }
495 
496     @Test
testGetAudioPresentations()497     public void testGetAudioPresentations() throws Exception {
498         Preconditions.assertTestFileExists(mInpPrefix +
499                         "MultiLangPerso_1PID_PC0_Select_AC4_H265_DVB_50fps_Audio_Only.ts");
500         setDataSource("MultiLangPerso_1PID_PC0_Select_AC4_H265_DVB_50fps_Audio_Only.ts");
501         int ac4TrackIndex = -1;
502         for (int i = 0; i < mExtractor.getTrackCount(); i++) {
503             MediaFormat format = mExtractor.getTrackFormat(i);
504             String mime = format.getString(MediaFormat.KEY_MIME);
505             if (MediaFormat.MIMETYPE_AUDIO_AC4.equals(mime)) {
506                 ac4TrackIndex = i;
507                 break;
508             }
509         }
510 
511         // Not all devices support AC4.
512         if (ac4TrackIndex == -1) {
513             List<AudioPresentation> presentations =
514                     mExtractor.getAudioPresentations(0 /*trackIndex*/);
515             assertNotNull(presentations);
516             assertTrue(presentations.isEmpty());
517             return;
518         }
519 
520         // The test file has two sets of audio presentations. The presentation set
521         // changes for every 100 audio presentation descriptors between two presentations.
522         // Instead of attempting to count the presentation descriptors, the test assumes
523         // a particular order of the presentations and advances to the next reference set
524         // once getAudioPresentations returns a set that doesn't match the current reference set.
525         // Thus the test can match the set 0 several times, then it encounters set 1,
526         // advances the reference set index, matches set 1 until it encounters set 2 etc.
527         // At the end it verifies that all the reference sets were met.
528         List<Map<Integer, AudioPresentation>> refPresentations = Arrays.asList(
529                 // First set.
530                 Map.of(
531                     10, new AudioPresentation.Builder(10)
532                             .setLocale(ULocale.ENGLISH)
533                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
534                             .setHasDialogueEnhancement(true)
535                             .build(),
536                     11, new AudioPresentation.Builder(11)
537                             .setLocale(ULocale.ENGLISH)
538                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
539                             .setHasAudioDescription(true)
540                             .setHasDialogueEnhancement(true)
541                             .build(),
542                     12, new AudioPresentation.Builder(12)
543                             .setLocale(ULocale.FRENCH)
544                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
545                             .setHasDialogueEnhancement(true)
546                             .build()
547                 ),
548                 // Second set.
549                 Map.of(
550                     10, new AudioPresentation.Builder(10)
551                             .setLocale(ULocale.GERMAN)
552                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
553                             .setHasAudioDescription(true)
554                             .setHasDialogueEnhancement(true)
555                             .build(),
556                     11, new AudioPresentation.Builder(11)
557                             .setLocale(new ULocale("es"))
558                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
559                             .setHasSpokenSubtitles(true)
560                             .setHasDialogueEnhancement(true)
561                             .build(),
562                     12, new AudioPresentation.Builder(12)
563                             .setLocale(ULocale.ENGLISH)
564                             .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND)
565                             .setHasDialogueEnhancement(true)
566                             .build()
567                 ),
568                 null,
569                 null
570         );
571         refPresentations.set(2, refPresentations.get(0));
572         refPresentations.set(3, refPresentations.get(1));
573         boolean[] presentationsMatched = new boolean[refPresentations.size()];
574         mExtractor.selectTrack(ac4TrackIndex);
575         for (int i = 0; i < refPresentations.size(); ) {
576             List<AudioPresentation> presentations = mExtractor.getAudioPresentations(ac4TrackIndex);
577             assertNotNull(presentations);
578             // Assumes all presentation sets have the same number of presentations.
579             assertEquals(refPresentations.get(i).size(), presentations.size());
580             if (!audioPresentationSetMatchesReference(refPresentations.get(i), presentations)) {
581                     // Time to advance to the next presentation set.
582                     i++;
583                     continue;
584             }
585             Log.d(TAG, "Matched presentation " + i);
586             presentationsMatched[i] = true;
587             // No need to wait for another switch after the last presentation has been matched.
588             if (i == presentationsMatched.length - 1 || !mExtractor.advance()) {
589                 break;
590             }
591         }
592         for (int i = 0; i < presentationsMatched.length; i++) {
593             assertTrue("Presentation set " + i + " was not found in the stream",
594                     presentationsMatched[i]);
595         }
596     }
597 
598     /* package */ static class ByteBufferDataSource extends MediaDataSource {
599         private final long mSize;
600         private TreeMap<Long, ByteBuffer> mMap = new TreeMap<Long, ByteBuffer>();
601 
ByteBufferDataSource(StreamUtils.ByteBufferStream bufferStream)602         public ByteBufferDataSource(StreamUtils.ByteBufferStream bufferStream)
603                 throws IOException {
604             long size = 0;
605             while (true) {
606                 final ByteBuffer buffer = bufferStream.read();
607                 if (buffer == null) break;
608                 final int limit = buffer.limit();
609                 if (limit == 0) continue;
610                 size += limit;
611                 mMap.put(size - 1, buffer); // key: last byte of validity for the buffer.
612             }
613             mSize = size;
614         }
615 
616         @Override
getSize()617         public long getSize() {
618             return mSize;
619         }
620 
621         @Override
readAt(long position, byte[] buffer, int offset, int size)622         public int readAt(long position, byte[] buffer, int offset, int size) {
623             Log.v(TAG, "reading at " + position + " offset " + offset + " size " + size);
624 
625             // This chooses all buffers with key >= position (e.g. valid buffers)
626             final SortedMap<Long, ByteBuffer> map = mMap.tailMap(position);
627             int copied = 0;
628             for (Map.Entry<Long, ByteBuffer> e : map.entrySet()) {
629                 // Get a read-only version of the byte buffer.
630                 final ByteBuffer bb = e.getValue().asReadOnlyBuffer();
631                 // Convert read position to an offset within that byte buffer, bboffs.
632                 final long bboffs = position - e.getKey() + bb.limit() - 1;
633                 if (bboffs >= bb.limit() || bboffs < 0) {
634                     break; // (negative position)?
635                 }
636                 bb.position((int)bboffs); // cast is safe as bb.limit is int.
637                 final int tocopy = Math.min(size, bb.remaining());
638                 if (tocopy == 0) {
639                     break; // (size == 0)?
640                 }
641                 bb.get(buffer, offset, tocopy);
642                 copied += tocopy;
643                 size -= tocopy;
644                 offset += tocopy;
645                 position += tocopy;
646                 if (size == 0) {
647                     break; // finished copying.
648                 }
649             }
650             if (copied == 0) {
651                 copied = -1;  // signal end of file
652             }
653             return copied;
654         }
655 
656         @Override
close()657         public void close() {
658             mMap = null;
659         }
660     }
661 
662     /* package */ static class MediaExtractorStream
663                 extends StreamUtils.ByteBufferStream implements Closeable {
664         public boolean mIsFloat;
665         public boolean mSawOutputEOS;
666         public MediaFormat mFormat;
667 
668         private MediaExtractor mExtractor;
669         private StreamUtils.MediaCodecStream mDecoderStream;
670 
MediaExtractorStream( String inMime, String outMime, MediaDataSource dataSource)671         public MediaExtractorStream(
672                 String inMime, String outMime,
673                 MediaDataSource dataSource) throws Exception {
674             mExtractor = new MediaExtractor();
675             mExtractor.setDataSource(dataSource);
676             final int numTracks = mExtractor.getTrackCount();
677             // Single track?
678             // assertEquals("Number of tracks should be 1", 1, numTracks);
679             for (int i = 0; i < numTracks; ++i) {
680                 final MediaFormat format = mExtractor.getTrackFormat(i);
681                 final String actualMime = format.getString(MediaFormat.KEY_MIME);
682                 mExtractor.selectTrack(i);
683                 mFormat = format;
684                 if (outMime.equals(actualMime)) {
685                     break;
686                 } else { // no matching mime, try to use decoder
687                     mDecoderStream = new StreamUtils.MediaCodecStream(
688                             mExtractor, mFormat);
689                     Log.w(TAG, "fallback to input mime type with decoder");
690                 }
691             }
692             assertNotNull("MediaExtractor cannot find mime type " + inMime, mFormat);
693             mIsFloat = mFormat.getInteger(
694                     MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT)
695                             == AudioFormat.ENCODING_PCM_FLOAT;
696         }
697 
MediaExtractorStream( String inMime, String outMime, StreamUtils.ByteBufferStream inputStream)698         public MediaExtractorStream(
699                 String inMime, String outMime,
700                 StreamUtils.ByteBufferStream inputStream) throws Exception {
701             this(inMime, outMime, new ByteBufferDataSource(inputStream));
702         }
703 
704         @Override
read()705         public ByteBuffer read() throws IOException {
706             if (mSawOutputEOS) {
707                 return null;
708             }
709             if (mDecoderStream != null) {
710                 return mDecoderStream.read();
711             }
712             // To preserve codec-like behavior, we create ByteBuffers
713             // equal to the media sample size.
714             final long size = mExtractor.getSampleSize();
715             if (size >= 0) {
716                 final ByteBuffer inputBuffer = ByteBuffer.allocate((int)size);
717                 final int red = mExtractor.readSampleData(inputBuffer, 0 /* offset */); // sic
718                 if (red >= 0) {
719                     assertEquals("position must be zero", 0, inputBuffer.position());
720                     assertEquals("limit must be read bytes", red, inputBuffer.limit());
721                     mExtractor.advance();
722                     return inputBuffer;
723                 }
724             }
725             mSawOutputEOS = true;
726             return null;
727         }
728 
729         @Override
close()730         public void close() throws IOException {
731             if (mExtractor != null) {
732                 mExtractor.release();
733                 mExtractor = null;
734             }
735             mFormat = null;
736         }
737 
738         @Override
finalize()739         protected void finalize() throws Throwable {
740             if (mExtractor != null) {
741                 Log.w(TAG, "MediaExtractorStream wasn't closed");
742                 mExtractor.release();
743             }
744             mFormat = null;
745         }
746     }
747 
748     @Test
testProgramStreamExtraction()749     public void testProgramStreamExtraction() throws Exception {
750         AssetFileDescriptor testFd = getAssetFileDescriptorFor("programstream.mpeg");
751 
752         MediaExtractor extractor = new MediaExtractor();
753         extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
754                 testFd.getLength());
755         testFd.close();
756         assertEquals("There must be 2 tracks", 2, extractor.getTrackCount());
757         extractor.selectTrack(0);
758         extractor.selectTrack(1);
759         boolean lastAdvanceResult = true;
760         boolean lastReadResult = true;
761         int [] bytesRead = new int[2];
762         MediaCodec [] codecs = { null, null };
763 
764         try {
765             MediaFormat f = extractor.getTrackFormat(0);
766             codecs[0] = MediaCodec.createDecoderByType(f.getString(MediaFormat.KEY_MIME));
767             codecs[0].configure(f, null /* surface */, null /* crypto */, 0 /* flags */);
768             codecs[0].start();
769         } catch (IOException | IllegalArgumentException e) {
770             // ignore
771         }
772         try {
773             MediaFormat f = extractor.getTrackFormat(1);
774             codecs[1] = MediaCodec.createDecoderByType(f.getString(MediaFormat.KEY_MIME));
775             codecs[1].configure(f, null /* surface */, null /* crypto */, 0 /* flags */);
776             codecs[1].start();
777         } catch (IOException | IllegalArgumentException e) {
778             // ignore
779         }
780 
781         final int RETRY_LIMIT = 100;
782         final long INPUTBUFFER_TIMEOUT_US = 10000;
783         int num_retry = 0;
784         ByteBuffer buf = ByteBuffer.allocate(2*1024*1024);
785         MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
786         while(num_retry < RETRY_LIMIT) {
787             for (MediaCodec codec : codecs) {
788                 if (codec == null) {
789                     continue;
790                 }
791                 while (true) {
792                     int idx = codec.dequeueOutputBuffer(info, 0);
793                     if (idx < 0) {
794                         break;
795                     }
796                     codec.releaseOutputBuffer(idx, false);
797                 }
798             }
799 
800             int trackIdx = extractor.getSampleTrackIndex();
801             MediaCodec codec = codecs[trackIdx];
802             ByteBuffer b = buf;
803             int bufIdx = -1;
804             if (codec != null) {
805                 bufIdx = codec.dequeueInputBuffer(INPUTBUFFER_TIMEOUT_US);
806                 // No available input buffer now, retry again.
807                 if (bufIdx < 0) {
808                     num_retry += 1;
809                     continue;
810                 }
811 
812                 num_retry = 0;
813                 b = codec.getInputBuffer(bufIdx);
814             }
815             int n = extractor.readSampleData(b, 0);
816             if (n > 0) {
817                 bytesRead[trackIdx] += n;
818             }
819             if (codec != null) {
820                 int sampleFlags = extractor.getSampleFlags();
821                 long sampleTime = extractor.getSampleTime();
822                 codec.queueInputBuffer(bufIdx, 0, n, sampleTime, sampleFlags);
823             }
824             if (!extractor.advance()) {
825                 break;
826             }
827         }
828         extractor.release();
829 
830         assertTrue("dequeueing input buffer exceeded timeout", num_retry < RETRY_LIMIT);
831         assertTrue("did not read from track 0", bytesRead[0] > 0);
832         assertTrue("did not read from track 1", bytesRead[1] > 0);
833     }
834 
doTestAdvance(final String res)835     private void doTestAdvance(final String res) throws Exception {
836         AssetFileDescriptor testFd = getAssetFileDescriptorFor(res);
837 
838         MediaExtractor extractor = new MediaExtractor();
839         extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(),
840                 testFd.getLength());
841         testFd.close();
842         extractor.selectTrack(0);
843         boolean lastAdvanceResult = true;
844         boolean lastReadResult = true;
845         ByteBuffer buf = ByteBuffer.allocate(2*1024*1024);
846         while(lastAdvanceResult || lastReadResult) {
847             int n = extractor.readSampleData(buf, 0);
848             if (lastAdvanceResult) {
849                 // previous advance() was successful, so readSampleData() should succeed
850                 assertTrue("readSampleData() failed after successful advance()", n >= 0);
851                 assertTrue("getSampleTime() failed after successful advance()",
852                         extractor.getSampleTime() >= 0);
853                 assertTrue("getSampleSize() failed after successful advance()",
854                         extractor.getSampleSize() >= 0);
855                 assertTrue("getSampleTrackIndex() failed after successful advance()",
856                         extractor.getSampleTrackIndex() >= 0);
857             } else {
858                 // previous advance() failed, so readSampleData() should fail too
859                 assertTrue("readSampleData() succeeded after failed advance()", n < 0);
860                 assertTrue("getSampleTime() succeeded after failed advance()",
861                         extractor.getSampleTime() < 0);
862                 assertTrue("getSampleSize() succeeded after failed advance()",
863                         extractor.getSampleSize() < 0);
864                 assertTrue("getSampleTrackIndex() succeeded after failed advance()",
865                         extractor.getSampleTrackIndex() < 0);
866             }
867             lastReadResult = (n >= 0);
868             lastAdvanceResult = extractor.advance();
869         }
870         extractor.release();
871     }
872 
readAllData()873     private void readAllData() {
874         // 1MB is enough for any sample.
875         final ByteBuffer buf = ByteBuffer.allocate(1024*1024);
876         final int trackCount = mExtractor.getTrackCount();
877 
878         for (int i = 0; i < trackCount; i++) {
879             mExtractor.selectTrack(i);
880         }
881         do {
882             mExtractor.readSampleData(buf, 0);
883         } while (mExtractor.advance());
884         mExtractor.seekTo(0, MediaExtractor.SEEK_TO_NEXT_SYNC);
885         do {
886             mExtractor.readSampleData(buf, 0);
887         } while (mExtractor.advance());
888     }
889 
890     @Test
891     @ApiTest(apis = {"android.media.MediaFormat#MIMETYPE_AUDIO_DTS"})
testDtsInMpeg2ts()892     public void testDtsInMpeg2ts() throws Exception {
893         setDataSource("sample_dts.ts");
894         assumeTrue("extractor did not find the DTS track", 1 == mExtractor.getTrackCount());
895 
896         // The following values below require API Build.VERSION_CODES.TIRAMISU
897         if (IS_AT_LEAST_T) {
898             MediaFormat trackFormat = mExtractor.getTrackFormat(0);
899             final String mediaType = trackFormat.getString(MediaFormat.KEY_MIME);
900             assertEquals(MediaFormat.MIMETYPE_AUDIO_DTS, mediaType);
901         }
902         readAllData();
903     }
904 
905     @Test
906     @ApiTest(apis = {"android.media.MediaFormat#MIMETYPE_AUDIO_DTS_HD"})
testDtsHdInMpeg2ts()907     public void testDtsHdInMpeg2ts() throws Exception {
908         setDataSource("sample_dts_hd.ts");
909         assumeTrue("extractor did not find the DTS track", 1 == mExtractor.getTrackCount());
910 
911         // The following values below require API Build.VERSION_CODES.TIRAMISU
912         if (IS_AT_LEAST_T) {
913             MediaFormat trackFormat = mExtractor.getTrackFormat(0);
914             final String mediaType = trackFormat.getString(MediaFormat.KEY_MIME);
915             assertEquals(MediaFormat.MIMETYPE_AUDIO_DTS_HD, mediaType);
916             // The following values below require API Build.VERSION_CODES.UPSIDE_DOWN_CAKE
917             if (IS_AT_LEAST_U) {
918                 int mediaProfile = trackFormat.getInteger(MediaFormat.KEY_PROFILE);
919                 assertEquals(MediaCodecInfo.CodecProfileLevel.DTS_HDProfileLBR, mediaProfile);
920             }
921         }
922         readAllData();
923     }
924 
925     @Test
926     @ApiTest(apis = {"android.media.MediaFormat#MIMETYPE_AUDIO_DTS_UHD"})
testDtsUhdInMpeg2ts()927     public void testDtsUhdInMpeg2ts() throws Exception {
928         setDataSource("sample_dts_uhd.ts");
929         assumeTrue("extractor did not find the DTS track", 1 == mExtractor.getTrackCount());
930 
931         // The following values below require API Build.VERSION_CODES.TIRAMISU
932         if (IS_AT_LEAST_T) {
933             MediaFormat trackFormat = mExtractor.getTrackFormat(0);
934             final String mediaType = trackFormat.getString(MediaFormat.KEY_MIME);
935             assertEquals(MediaFormat.MIMETYPE_AUDIO_DTS_UHD, mediaType);
936             // The following values below require API Build.VERSION_CODES.UPSIDE_DOWN_CAKE
937             if (IS_AT_LEAST_U) {
938                 int mediaProfile = trackFormat.getInteger(MediaFormat.KEY_PROFILE);
939                 assertEquals(MediaCodecInfo.CodecProfileLevel.DTS_UHDProfileP2, mediaProfile);
940             }
941         }
942         readAllData();
943     }
944 
945     @Test
testAV1InMP4()946     public void testAV1InMP4() throws Exception {
947         setDataSource("video_dovi_3840x2160_30fps_dav1_10_2.mp4");
948         readAllData();
949     }
950 
951     @Test
testDolbyVisionInMP4()952     public void testDolbyVisionInMP4() throws Exception {
953         setDataSource("video_dovi_3840x2160_30fps_dav1_10.mp4");
954         readAllData();
955     }
956 
957     @Test
testPcmLeInMov()958     public void testPcmLeInMov() throws Exception {
959         setDataSource("sinesweeppcmlemov.mov");
960         readAllData();
961     }
962 
963     @Test
testPcmBeInMov()964     public void testPcmBeInMov() throws Exception {
965         setDataSource("sinesweeppcmbemov.mov");
966         readAllData();
967     }
968 
969     @Test
testFragmentedRead()970     public void testFragmentedRead() throws Exception {
971         Preconditions.assertTestFileExists(mInpPrefix + "psshtest.mp4");
972         setDataSource("psshtest.mp4");
973         readAllData();
974     }
975 
976     @AppModeFull(reason = "Instant apps cannot bind sockets.")
977     @Test
testFragmentedHttpRead()978     public void testFragmentedHttpRead() throws Exception {
979         CtsTestServer server = new CtsTestServer(getContext());
980         Preconditions.assertTestFileExists(mInpPrefix + "psshtest.mp4");
981         String url = server.getAssetUrl(mInpPrefix + "psshtest.mp4");
982         mExtractor.setDataSource(url);
983         readAllData();
984         server.shutdown();
985     }
986 }
987