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