1 /* 2 * Copyright (C) 2022 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.decoder.cts; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertTrue; 22 23 import android.app.ActivityManager; 24 import android.media.AudioAttributes; 25 import android.media.AudioFormat; 26 import android.media.AudioManager; 27 import android.media.AudioTrack; 28 import android.media.MediaCodec; 29 import android.media.MediaCodecInfo; 30 import android.media.MediaCodecList; 31 import android.media.MediaExtractor; 32 import android.media.MediaFormat; 33 import android.media.cts.MediaHeavyPresubmitTest; 34 import android.media.cts.MediaTestBase; 35 import android.os.Build; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.HandlerThread; 39 import android.os.Looper; 40 import android.platform.test.annotations.AppModeFull; 41 42 import androidx.test.ext.junit.runners.AndroidJUnit4; 43 import androidx.test.filters.SdkSuppress; 44 45 import com.android.compatibility.common.util.ApiTest; 46 import com.android.compatibility.common.util.Preconditions; 47 48 import org.junit.After; 49 import org.junit.Assert; 50 import org.junit.Assume; 51 import org.junit.Before; 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 55 import java.io.IOException; 56 import java.nio.ByteBuffer; 57 import java.time.Duration; 58 import java.util.ArrayDeque; 59 import java.util.ArrayList; 60 import java.util.Collections; 61 import java.util.List; 62 import java.util.Queue; 63 import java.util.concurrent.atomic.AtomicBoolean; 64 import java.util.concurrent.atomic.AtomicLong; 65 import java.util.concurrent.locks.Lock; 66 import java.util.concurrent.locks.ReentrantLock; 67 import java.util.function.Supplier; 68 69 @MediaHeavyPresubmitTest 70 @AppModeFull(reason = "There should be no instant apps specific behavior related to decoders") 71 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE, codeName = "UpsideDownCake") 72 @RunWith(AndroidJUnit4.class) 73 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) 74 public class DecodeOnlyTest extends MediaTestBase { 75 private static final String MEDIA_DIR_STRING = WorkDir.getMediaDirString(); 76 private static final String HEVC_VIDEO = 77 "video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz.mkv"; 78 private static final String AVC_VIDEO = 79 "video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz.mp4"; 80 private static final String VP9_VIDEO = 81 "bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz.webm"; 82 private static final String MIME_VIDEO_PREFIX = "video/"; 83 private static final String MIME_AUDIO_PREFIX = "audio/"; 84 private static final long EOS_TIMESTAMP_TUNNEL_MODE = Long.MAX_VALUE; 85 86 @Before 87 @Override setUp()88 public void setUp() throws Throwable { 89 super.setUp(); 90 } 91 92 @After 93 @Override tearDown()94 public void tearDown() { 95 super.tearDown(); 96 } 97 98 /** 99 * When testing perfect seek, assert that the first frame rendered after seeking is the exact 100 * frame we seeked to 101 */ 102 @Test 103 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOnAvc()104 public void testTunneledPerfectSeekInitialPeekOnAvc() throws Exception { 105 testTunneledPerfectSeek(AVC_VIDEO, true); 106 } 107 108 @Test 109 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOnVp9()110 public void testTunneledPerfectSeekInitialPeekOnVp9() throws Exception { 111 testTunneledPerfectSeek(VP9_VIDEO, true); 112 } 113 114 @Test 115 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOnHevc()116 public void testTunneledPerfectSeekInitialPeekOnHevc() throws Exception { 117 testTunneledPerfectSeek(HEVC_VIDEO, true); 118 } 119 120 @Test 121 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOffAvc()122 public void testTunneledPerfectSeekInitialPeekOffAvc() throws Exception { 123 testTunneledPerfectSeek(AVC_VIDEO, false); 124 } 125 126 @Test 127 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOffVp9()128 public void testTunneledPerfectSeekInitialPeekOffVp9() throws Exception { 129 testTunneledPerfectSeek(VP9_VIDEO, false); 130 } 131 132 @Test 133 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledPerfectSeekInitialPeekOffHevc()134 public void testTunneledPerfectSeekInitialPeekOffHevc() throws Exception { 135 testTunneledPerfectSeek(HEVC_VIDEO, false); 136 } 137 138 /** 139 * In trick play, we expect to receive/render the non DECODE_ONLY frames only 140 */ 141 @Test 142 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledTrickPlayHevc()143 public void testTunneledTrickPlayHevc() throws Exception { 144 testTunneledTrickPlay(HEVC_VIDEO); 145 } 146 147 @Test 148 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledTrickPlayAvc()149 public void testTunneledTrickPlayAvc() throws Exception { 150 testTunneledTrickPlay(AVC_VIDEO); 151 } 152 153 @Test 154 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testTunneledTrickPlayVp9()155 public void testTunneledTrickPlayVp9() throws Exception { 156 testTunneledTrickPlay(VP9_VIDEO); 157 } 158 159 @Test 160 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testNonTunneledTrickPlayHevc()161 public void testNonTunneledTrickPlayHevc() throws Exception { 162 testNonTunneledTrickPlay(HEVC_VIDEO); 163 } 164 165 @Test 166 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testNonTunneledTrickPlayAvc()167 public void testNonTunneledTrickPlayAvc() throws Exception { 168 testNonTunneledTrickPlay(AVC_VIDEO); 169 } 170 171 @Test 172 @ApiTest(apis = {"android.media.MediaCodec#BUFFER_FLAG_DECODE_ONLY"}) testNonTunneledTrickPlayVp9()173 public void testNonTunneledTrickPlayVp9() throws Exception { 174 testNonTunneledTrickPlay(VP9_VIDEO); 175 } 176 testNonTunneledTrickPlay(String fileName)177 private void testNonTunneledTrickPlay(String fileName) throws Exception { 178 Preconditions.assertTestFileExists(MEDIA_DIR_STRING + fileName); 179 // create the video extractor 180 MediaExtractor videoExtractor = createMediaExtractor(fileName); 181 182 // choose the first track that has the prefix "video/" and select it 183 int videoTrackIndex = getFirstTrackWithMimePrefix(MIME_VIDEO_PREFIX, videoExtractor); 184 videoExtractor.selectTrack(videoTrackIndex); 185 186 // create the video codec 187 MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex); 188 String mime = videoFormat.getString(MediaFormat.KEY_MIME); 189 MediaCodec videoCodec = MediaCodec.createDecoderByType(mime); 190 191 AtomicBoolean done = new AtomicBoolean(false); 192 List<Long> expectedPresentationTimes = new ArrayList<>(); 193 List<Long> receivedPresentationTimes = new ArrayList<>(); 194 195 // set a callback on the video codec to process the frames 196 videoCodec.setCallback(new MediaCodec.Callback() { 197 private boolean mEosQueued; 198 int mDecodeOnlyCounter = 0; 199 200 // before queueing a frame, check if it is the last frame and set the EOS flag 201 // to the frame if that's the case. If the frame is to be only decoded 202 // (every other frame), then set the DECODE_ONLY flag to the frame. Only frames not 203 // tagged with EOS or DECODE_ONLY are expected to be rendered and added 204 // to expectedPresentationTimes 205 @Override 206 public void onInputBufferAvailable(MediaCodec codec, int index) { 207 if (!mEosQueued) { 208 ByteBuffer inputBuffer = videoCodec.getInputBuffer(index); 209 int sampleSize = videoExtractor.readSampleData(inputBuffer, 0); 210 long presentationTime = videoExtractor.getSampleTime(); 211 int flags = videoExtractor.getSampleFlags(); 212 if (sampleSize < 0) { 213 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 214 sampleSize = 0; 215 mEosQueued = true; 216 } else if (mDecodeOnlyCounter % 2 == 0) { 217 flags = MediaCodec.BUFFER_FLAG_DECODE_ONLY; 218 } else { 219 expectedPresentationTimes.add(presentationTime); 220 } 221 mDecodeOnlyCounter++; 222 videoCodec.queueInputBuffer(index, 0, sampleSize, presentationTime, flags); 223 videoExtractor.advance(); 224 } 225 } 226 227 // the frames received here are the frames that are rendered, not the DECODE_ONLY ones, 228 // if the DECODE_ONLY flag behaves correctly, receivedPresentationTimes should exactly 229 // match expectedPresentationTimes 230 // When the codec receives the EOS frame, set the done variable true, which will exit 231 // the loop below, signaling that the codec has finished processing the video and 232 // should now assert on the contents of the lists 233 @Override 234 public void onOutputBufferAvailable(MediaCodec codec, int index, 235 MediaCodec.BufferInfo info) { 236 videoCodec.releaseOutputBuffer(index, false); 237 if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { 238 done.set(true); 239 } else { 240 receivedPresentationTimes.add(info.presentationTimeUs); 241 } 242 } 243 244 @Override 245 public void onError(MediaCodec codec, MediaCodec.CodecException e) { 246 247 } 248 249 @Override 250 public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { 251 252 } 253 }); 254 255 videoCodec.configure(videoFormat, getActivity().getSurfaceHolder().getSurface(), null, 0); 256 // start the video the codec with track selected 257 videoCodec.start(); 258 259 // keep looping until the codec receives the EOS frame 260 while (!done.get()) { 261 Thread.sleep(100); 262 } 263 264 videoCodec.stop(); 265 videoCodec.release(); 266 267 Collections.sort(expectedPresentationTimes); 268 assertEquals(expectedPresentationTimes, receivedPresentationTimes); 269 } 270 testTunneledTrickPlay(String fileName)271 private void testTunneledTrickPlay(String fileName) throws Exception { 272 Preconditions.assertTestFileExists(MEDIA_DIR_STRING + fileName); 273 274 // generate the audio session id needed for tunnel mode playback 275 AudioManager audioManager = mContext.getSystemService(AudioManager.class); 276 int audioSessionId = audioManager.generateAudioSessionId(); 277 278 // create the video extractor 279 MediaExtractor videoExtractor = createMediaExtractor(fileName); 280 281 // choose the first track that has the prefix "video/" and select it 282 int videoTrackIndex = getFirstTrackWithMimePrefix(MIME_VIDEO_PREFIX, videoExtractor); 283 videoExtractor.selectTrack(videoTrackIndex); 284 285 // create the video codec for tunneled play 286 MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex); 287 videoFormat.setFeatureEnabled(MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback, 288 true); 289 MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS); 290 String codecName = mcl.findDecoderForFormat(videoFormat); 291 Assume.assumeTrue("Codec is not supported on this device", 292 codecName != null); 293 videoFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, audioSessionId); 294 MediaCodec videoCodec = MediaCodec.createByCodecName(codecName); 295 296 // create the audio extractor 297 MediaExtractor audioExtractor = createMediaExtractor(fileName); 298 299 // choose the first track that has the prefix "audio/" and select it 300 int audioTrackIndex = getFirstTrackWithMimePrefix(MIME_AUDIO_PREFIX, audioExtractor); 301 audioExtractor.selectTrack(audioTrackIndex); 302 303 // create the audio codec 304 MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrackIndex); 305 String mime = audioFormat.getString(MediaFormat.KEY_MIME); 306 MediaCodec audioCodec = MediaCodec.createDecoderByType(mime); 307 308 // audio track used by audio codec 309 AudioTrack audioTrack = createAudioTrack(audioFormat, audioSessionId); 310 311 List<Long> expectedPresentationTimes = new ArrayList<>(); 312 313 videoCodec.setCallback(new MediaCodec.Callback() { 314 int mDecodeOnlyCounter = 0; 315 316 // before queueing a frame, check if it is the last frame and set the EOS flag 317 // to the frame if that's the case. If the frame is to be only decoded 318 // (every other frame), then set the DECODE_ONLY flag to the frame. Only frames not 319 // tagged with EOS or DECODE_ONLY are expected to be rendered and added 320 // to expectedPresentationTimes 321 @Override 322 public void onInputBufferAvailable(MediaCodec codec, int index) { 323 ByteBuffer inputBuffer = videoCodec.getInputBuffer(index); 324 int sampleSize = videoExtractor.readSampleData(inputBuffer, 0); 325 long presentationTime = videoExtractor.getSampleTime(); 326 int flags = videoExtractor.getSampleFlags(); 327 if (sampleSize < 0) { 328 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 329 sampleSize = 0; 330 } else if (mDecodeOnlyCounter % 2 == 0) { 331 flags = MediaCodec.BUFFER_FLAG_DECODE_ONLY; 332 } else { 333 expectedPresentationTimes.add(presentationTime); 334 } 335 mDecodeOnlyCounter++; 336 videoCodec.queueInputBuffer(index, 0, sampleSize, presentationTime, flags); 337 videoExtractor.advance(); 338 } 339 340 // nothing to do here - in tunneled mode, the frames are rendered directly by the 341 // hardware, they are not sent back to the codec for extra processing 342 @Override 343 public void onOutputBufferAvailable(MediaCodec codec, int index, 344 MediaCodec.BufferInfo info) { 345 Assert.fail("onOutputBufferAvailable should not be called in tunnel mode."); 346 } 347 348 @Override 349 public void onError(MediaCodec codec, MediaCodec.CodecException e) { 350 Assert.fail("Encountered unexpected error while decoding video: " + e.getMessage()); 351 } 352 353 @Override 354 public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { 355 356 } 357 }); 358 videoCodec.configure(videoFormat, getActivity().getSurfaceHolder().getSurface(), null, 0); 359 AtomicBoolean done = new AtomicBoolean(false); 360 // Since data is written to AudioTrack in a blocking manner, run it in a separate thread to 361 // not block other operations. 362 HandlerThread audioThread = new HandlerThread("audioThread"); 363 audioThread.start(); 364 audioCodec.setCallback(new AudioCallback(audioCodec, audioExtractor, audioTrack, done), 365 new Handler(audioThread.getLooper())); 366 audioCodec.configure(audioFormat, null, null, 0); 367 368 List<Long> renderedPresentationTimes = new ArrayList<>(); 369 370 // a listener on rendered frames, if it is the last frame (EOS), then set the boolean to 371 // true and exit the loop below, else add the frame to renderedPresentationTimes 372 // to expectedPresentationTimes should be equal at the end of the test 373 videoCodec.setOnFrameRenderedListener((codec, presentationTimeUs, nanoTime) -> { 374 if (presentationTimeUs == EOS_TIMESTAMP_TUNNEL_MODE) { 375 done.set(true); 376 } else { 377 renderedPresentationTimes.add(presentationTimeUs); 378 } 379 }, new Handler(Looper.getMainLooper())); 380 381 // start media playback 382 videoCodec.start(); 383 audioCodec.start(); 384 audioTrack.play(); 385 386 // keep looping until the codec receives the EOS frame 387 while (!done.get()) { 388 Thread.sleep(100); 389 } 390 audioTrack.stop(); 391 audioTrack.release(); 392 videoCodec.stop(); 393 videoCodec.release(); 394 audioCodec.stop(); 395 audioCodec.release(); 396 397 Collections.sort(expectedPresentationTimes); 398 Collections.sort(renderedPresentationTimes); 399 assertEquals(expectedPresentationTimes, renderedPresentationTimes); 400 } 401 sleepUntil(Supplier<Boolean> supplier, Duration maxWait)402 private void sleepUntil(Supplier<Boolean> supplier, Duration maxWait) throws Exception { 403 final long deadLineMs = System.currentTimeMillis() + maxWait.toMillis(); 404 do { 405 Thread.sleep(50); 406 } while (!supplier.get() && System.currentTimeMillis() < deadLineMs); 407 } 408 testTunneledPerfectSeek(String fileName, final boolean initialPeek)409 private void testTunneledPerfectSeek(String fileName, 410 final boolean initialPeek) throws Exception { 411 Preconditions.assertTestFileExists(MEDIA_DIR_STRING + fileName); 412 413 // generate the audio session id needed for tunnel mode playback 414 AudioManager audioManager = mContext.getSystemService(AudioManager.class); 415 int audioSessionId = audioManager.generateAudioSessionId(); 416 417 // create the video extractor 418 MediaExtractor videoExtractor = createMediaExtractor(fileName); 419 420 // choose the first track that has the prefix "video/" and select it 421 int videoTrackIndex = getFirstTrackWithMimePrefix(MIME_VIDEO_PREFIX, videoExtractor); 422 videoExtractor.selectTrack(videoTrackIndex); 423 424 // create the video codec for tunneled play 425 MediaFormat videoFormat = videoExtractor.getTrackFormat(videoTrackIndex); 426 videoFormat.setFeatureEnabled(MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback, 427 true); 428 MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS); 429 String codecName = mcl.findDecoderForFormat(videoFormat); 430 Assume.assumeTrue("Codec is not supported on this device", 431 codecName != null); 432 videoFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, audioSessionId); 433 MediaCodec videoCodec = MediaCodec.createByCodecName(codecName); 434 435 // create the audio extractor 436 MediaExtractor audioExtractor = createMediaExtractor(fileName); 437 438 // choose the first track that has the prefix "audio/" and select it 439 int audioTrackIndex = getFirstTrackWithMimePrefix(MIME_AUDIO_PREFIX, audioExtractor); 440 audioExtractor.selectTrack(audioTrackIndex); 441 442 // create the audio codec 443 MediaFormat audioFormat = audioExtractor.getTrackFormat(audioTrackIndex); 444 String mime = audioFormat.getString(MediaFormat.KEY_MIME); 445 MediaCodec audioCodec = MediaCodec.createDecoderByType(mime); 446 447 // audio track used by audio codec 448 AudioTrack audioTrack = createAudioTrack(audioFormat, audioSessionId); 449 450 // Frames at 2s of each file are not key frame 451 AtomicLong seekTime = new AtomicLong(2000 * 1000); 452 videoExtractor.seekTo(seekTime.get(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC); 453 audioExtractor.seekTo(seekTime.get(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC); 454 455 List<Long> expectedPresentationTimes = new ArrayList<>(); 456 // Setting it to true so that buffers are not queued in the beginning. 457 AtomicBoolean done = new AtomicBoolean(true); 458 AtomicBoolean hasDecodeOnlyFrames = new AtomicBoolean(false); 459 460 class VideoAsyncHandler extends MediaCodec.Callback { 461 private final Queue<Integer> mAvailableInputIndices = new ArrayDeque<>(); 462 private final Lock mLock = new ReentrantLock(); 463 464 private void queueInput(MediaCodec codec, int index) { 465 ByteBuffer inputBuffer = codec.getInputBuffer(index); 466 int sampleSize = videoExtractor.readSampleData(inputBuffer, 0); 467 long presentationTime = videoExtractor.getSampleTime(); 468 int flags = videoExtractor.getSampleFlags(); 469 if (sampleSize < 0) { 470 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 471 sampleSize = 0; 472 } else if (presentationTime < seekTime.get()) { 473 flags = MediaCodec.BUFFER_FLAG_DECODE_ONLY; 474 hasDecodeOnlyFrames.set(true); 475 } else { 476 expectedPresentationTimes.add(presentationTime); 477 } 478 codec.queueInputBuffer(index, 0, sampleSize, presentationTime, flags); 479 videoExtractor.advance(); 480 } 481 482 @Override 483 public void onInputBufferAvailable(MediaCodec codec, int index) { 484 mLock.lock(); 485 try { 486 if (!done.get()) { 487 queueInput(codec, index); 488 } else { 489 mAvailableInputIndices.offer(index); 490 } 491 } finally { 492 mLock.unlock(); 493 } 494 } 495 496 public void startProcess() { 497 mLock.lock(); 498 try { 499 done.set(false); 500 while (!mAvailableInputIndices.isEmpty()) { 501 queueInput(videoCodec, mAvailableInputIndices.poll()); 502 } 503 } finally { 504 mLock.unlock(); 505 } 506 } 507 508 public void clearBufferQueue() { 509 mLock.lock(); 510 try { 511 mAvailableInputIndices.clear(); 512 } finally { 513 mLock.unlock(); 514 } 515 } 516 517 @Override 518 public void onOutputBufferAvailable(MediaCodec codec, int index, 519 MediaCodec.BufferInfo info) { 520 Assert.fail("onOutputBufferAvailable should not be called in tunnel mode."); 521 } 522 523 @Override 524 public void onError(MediaCodec codec, MediaCodec.CodecException e) { 525 Assert.fail("Encountered unexpected error while decoding video: " + e.getMessage()); 526 } 527 528 @Override 529 public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { 530 531 } 532 } 533 534 VideoAsyncHandler videoAsyncHandler = new VideoAsyncHandler(); 535 videoCodec.setCallback(videoAsyncHandler); 536 videoCodec.configure(videoFormat, getActivity().getSurfaceHolder().getSurface(), null, 0); 537 // Since data is written to AudioTrack in a blocking manner, run it in a separate thread to 538 // not block other operations. 539 HandlerThread audioThread = new HandlerThread("audioThread"); 540 audioThread.start(); 541 audioCodec.setCallback(new AudioCallback(audioCodec, audioExtractor, audioTrack, done), 542 new Handler(audioThread.getLooper())); 543 audioCodec.configure(audioFormat, null, null, 0); 544 545 List<Long> renderedPresentationTimes = new ArrayList<>(); 546 // play for 500ms 547 videoCodec.setOnFrameRenderedListener((codec, presentationTimeUs, nanoTime) -> { 548 renderedPresentationTimes.add(presentationTimeUs); 549 if (presentationTimeUs >= seekTime.get() + 500 * 1000) { 550 done.set(true); 551 } 552 }, new Handler(Looper.getMainLooper())); 553 554 AtomicBoolean firstTunnelFrameReady = new AtomicBoolean(false); 555 videoCodec.setOnFirstTunnelFrameReadyListener(new Handler(Looper.getMainLooper()), 556 (codec) -> { 557 firstTunnelFrameReady.set(true); 558 }); 559 560 // start media playback 561 videoCodec.start(); 562 audioCodec.start(); 563 // Set peeking value after MediaCodec.start() is called. 564 boolean isPeeking = setKeyTunnelPeek(videoCodec, initialPeek ? 1 : 0); 565 // start to queue video frames 566 videoAsyncHandler.startProcess(); 567 568 // When video codecs are started, large chunks of contiguous physical memory need to be 569 // allocated, which, on low-RAM devices, can trigger high CPU usage for moving memory 570 // around to create contiguous space for the video decoder. This can cause an increase in 571 // startup time for playback. 572 ActivityManager activityManager = mContext.getSystemService(ActivityManager.class); 573 final int firstFrameReadyTimeoutSeconds = activityManager.isLowRamDevice() ? 3 : 1; 574 sleepUntil(firstTunnelFrameReady::get, Duration.ofSeconds(firstFrameReadyTimeoutSeconds)); 575 assertTrue(String.format("onFirstTunnelFrameReady not called within %d seconds", 576 firstFrameReadyTimeoutSeconds), firstTunnelFrameReady.get()); 577 578 final int waitForRenderingMs = 1000; 579 // Sleep for 1s here to ensure that either (1) when peek is on, high-latency display 580 // pipelines have enough time to render the first frame, or (2) when peek is off, the 581 // frame isn't rendered after long time. 582 Thread.sleep(waitForRenderingMs); 583 if (isPeeking) { 584 assertEquals(1, renderedPresentationTimes.size()); 585 assertEquals(seekTime.get(), (long) renderedPresentationTimes.get(0)); 586 } else { 587 assertTrue(renderedPresentationTimes.isEmpty()); 588 } 589 590 assertTrue("No DECODE_ONLY frames have been produced, " 591 + "try changing the offset for the seek. To do this, find a timestamp " 592 + "that falls between two sync frames to ensure that there will " 593 + "be a few DECODE_ONLY frames. For example \"ffprobe -show_frames $video\"" 594 + " can be used to list all the frames of a certain video and will show" 595 + " info about key frames and their timestamps.", 596 hasDecodeOnlyFrames.get()); 597 598 // Run the playback to verify that frame at seek time is also rendered when peek is off. 599 audioTrack.play(); 600 while (!done.get()) { 601 Thread.sleep(100); 602 } 603 if (!isPeeking) { 604 assertFalse(renderedPresentationTimes.isEmpty()); 605 assertEquals(seekTime.get(), (long) renderedPresentationTimes.get(0)); 606 } 607 608 audioTrack.pause(); 609 // Just be safe that pause may take some time. 610 Thread.sleep(500); 611 videoCodec.flush(); 612 audioCodec.flush(); 613 videoAsyncHandler.clearBufferQueue(); 614 615 // Frames at 7s of each file are not key frame, and there is non-zero key frame before it 616 seekTime.set(7000 * 1000); 617 videoExtractor.seekTo(seekTime.get(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC); 618 audioExtractor.seekTo(seekTime.get(), MediaExtractor.SEEK_TO_PREVIOUS_SYNC); 619 expectedPresentationTimes.clear(); 620 renderedPresentationTimes.clear(); 621 622 // a listener on rendered frames, if it is the last frame (EOS), then set the boolean to 623 // true and exit the loop below, else add the frame to renderedPresentationTimes 624 // to expectedPresentationTimes should be equal at the end of the test 625 // renderedPresentationTimes should only contain frames starting with desired seek timestamp 626 videoCodec.setOnFrameRenderedListener((codec, presentationTimeUs, nanoTime) -> { 627 if (presentationTimeUs == EOS_TIMESTAMP_TUNNEL_MODE) { 628 done.set(true); 629 } else { 630 renderedPresentationTimes.add(presentationTimeUs); 631 } 632 }, new Handler(Looper.getMainLooper())); 633 634 // Restart media playback 635 firstTunnelFrameReady.set(false); 636 videoCodec.start(); 637 audioCodec.start(); 638 // Set peek on when it was off, and set it off when it was on. 639 isPeeking = setKeyTunnelPeek(videoCodec, isPeeking ? 0 : 1); 640 videoAsyncHandler.startProcess(); 641 sleepUntil(firstTunnelFrameReady::get, Duration.ofSeconds(firstFrameReadyTimeoutSeconds)); 642 assertTrue(String.format("onFirstTunnelFrameReady not called within %d seconds", 643 firstFrameReadyTimeoutSeconds), firstTunnelFrameReady.get()); 644 645 // Sleep for 1s here to ensure that either (1) when peek is on, high-latency display 646 // pipelines have enough time to render the first frame, or (2) when peek is off, the 647 // frame isn't rendered after long time. 648 Thread.sleep(waitForRenderingMs); 649 if (isPeeking) { 650 assertEquals(1, renderedPresentationTimes.size()); 651 assertEquals(seekTime.get(), (long) renderedPresentationTimes.get(0)); 652 } else { 653 assertTrue(renderedPresentationTimes.isEmpty()); 654 } 655 656 if (!isPeeking) { 657 // First frame should be rendered immediately after setting peek on. 658 isPeeking = setKeyTunnelPeek(videoCodec, 1); 659 // This is long due to high-latency display pipelines on TV devices. 660 Thread.sleep(waitForRenderingMs); 661 assertEquals(1, renderedPresentationTimes.size()); 662 assertEquals(seekTime.get(), (long) renderedPresentationTimes.get(0)); 663 } 664 665 audioTrack.play(); 666 // keep looping until the codec receives the EOS frame 667 while (!done.get()) { 668 Thread.sleep(100); 669 } 670 671 audioTrack.stop(); 672 audioTrack.release(); 673 videoCodec.stop(); 674 videoCodec.release(); 675 audioCodec.stop(); 676 audioCodec.release(); 677 Collections.sort(expectedPresentationTimes); 678 Collections.sort(renderedPresentationTimes); 679 assertEquals(expectedPresentationTimes, renderedPresentationTimes); 680 assertEquals(seekTime.get(), (long) renderedPresentationTimes.get(0)); 681 } 682 683 // 1 is on, 0 is off. setKeyTunnelPeek(MediaCodec videoCodec, int value)684 private boolean setKeyTunnelPeek(MediaCodec videoCodec, int value) { 685 Bundle parameters = new Bundle(); 686 parameters.putInt(MediaCodec.PARAMETER_KEY_TUNNEL_PEEK, value); 687 videoCodec.setParameters(parameters); 688 return value != 0; 689 } 690 691 createAudioTrack(MediaFormat audioFormat, int audioSessionId)692 private AudioTrack createAudioTrack(MediaFormat audioFormat, int audioSessionId) { 693 int sampleRate = audioFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE); 694 int channelCount = audioFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT); 695 int channelConfig; 696 697 switch (channelCount) { 698 case 1: 699 channelConfig = AudioFormat.CHANNEL_OUT_MONO; 700 break; 701 case 2: 702 channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 703 break; 704 case 6: 705 channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; 706 break; 707 default: 708 throw new IllegalArgumentException(); 709 } 710 711 int minBufferSize = 712 AudioTrack.getMinBufferSize( 713 sampleRate, 714 channelConfig, 715 AudioFormat.ENCODING_PCM_16BIT); 716 AudioAttributes audioAttributes = (new AudioAttributes.Builder()) 717 .setLegacyStreamType(AudioManager.STREAM_MUSIC) 718 .setFlags(AudioAttributes.FLAG_HW_AV_SYNC) 719 .build(); 720 AudioFormat af = (new AudioFormat.Builder()) 721 .setChannelMask(channelConfig) 722 .setEncoding(AudioFormat.ENCODING_PCM_16BIT) 723 .setSampleRate(sampleRate) 724 .build(); 725 return new AudioTrack(audioAttributes, af, 2 * minBufferSize, 726 AudioTrack.MODE_STREAM, audioSessionId); 727 } 728 getFirstTrackWithMimePrefix(String prefix, MediaExtractor videoExtractor)729 private int getFirstTrackWithMimePrefix(String prefix, MediaExtractor videoExtractor) { 730 int trackIndex = -1; 731 for (int i = 0; i < videoExtractor.getTrackCount(); ++i) { 732 MediaFormat format = videoExtractor.getTrackFormat(i); 733 if (format.getString(MediaFormat.KEY_MIME).startsWith(prefix)) { 734 trackIndex = i; 735 break; 736 } 737 } 738 assertTrue("Video track was not found.", trackIndex >= 0); 739 return trackIndex; 740 } 741 createMediaExtractor(String fileName)742 private MediaExtractor createMediaExtractor(String fileName) throws IOException { 743 MediaExtractor mediaExtractor = new MediaExtractor(); 744 mediaExtractor.setDataSource(MEDIA_DIR_STRING + fileName); 745 return mediaExtractor; 746 } 747 748 private static class AudioCallback extends MediaCodec.Callback { 749 private final MediaCodec mAudioCodec; 750 private final MediaExtractor mAudioExtractor; 751 private final AudioTrack mAudioTrack; 752 private final AtomicBoolean mDone; 753 AudioCallback(MediaCodec audioCodec, MediaExtractor audioExtractor, AudioTrack audioTrack, AtomicBoolean done)754 AudioCallback(MediaCodec audioCodec, MediaExtractor audioExtractor, 755 AudioTrack audioTrack, AtomicBoolean done) { 756 this.mAudioCodec = audioCodec; 757 this.mAudioExtractor = audioExtractor; 758 this.mAudioTrack = audioTrack; 759 this.mDone = done; 760 } 761 762 @Override onInputBufferAvailable(MediaCodec codec, int index)763 public void onInputBufferAvailable(MediaCodec codec, int index) { 764 ByteBuffer audioInputBuffer = mAudioCodec.getInputBuffer(index); 765 int audioSampleSize = mAudioExtractor.readSampleData(audioInputBuffer, 0); 766 long presentationTime = mAudioExtractor.getSampleTime(); 767 int flags = mAudioExtractor.getSampleFlags(); 768 if (audioSampleSize < 0) { 769 flags = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 770 audioSampleSize = 0; 771 presentationTime = 0; 772 } 773 mAudioCodec.queueInputBuffer(index, 0, audioSampleSize, presentationTime, flags); 774 mAudioExtractor.advance(); 775 } 776 777 @Override onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)778 public void onOutputBufferAvailable(MediaCodec codec, int index, 779 MediaCodec.BufferInfo info) { 780 if (!mDone.get()) { 781 ByteBuffer outputBuffer = mAudioCodec.getOutputBuffer(index); 782 byte[] audioArray = new byte[info.size]; 783 outputBuffer.get(audioArray); 784 outputBuffer.clear(); 785 ByteBuffer audioData = ByteBuffer.wrap(audioArray); 786 int writtenSize = 0; 787 // This is a workaround, just to avoid blocking audio track write and codec 788 // crashes caused by invalid index after audio track pause and codec flush. 789 // b/291959069 to fix the outstanding callback issue. 790 while (!mDone.get()) { 791 int written = mAudioTrack.write(audioData, info.size - writtenSize, 792 AudioTrack.WRITE_BLOCKING, info.presentationTimeUs * 1000); 793 if (written >= 0) { 794 writtenSize += written; 795 if (writtenSize >= info.size) { 796 break; 797 } 798 // When audio track is not in playing state, the write operation does not 799 // block in WRITE_BLOCKING mode. And when audio track is full, the audio 800 // data can not be fully written. Must sleep here to wait for free spaces. 801 try { 802 Thread.sleep(50); 803 } catch (InterruptedException ignored) { 804 } 805 } else { 806 Assert.fail("AudioTrack write failure."); 807 } 808 } 809 mAudioCodec.releaseOutputBuffer(index, false); 810 } 811 } 812 813 @Override onError(MediaCodec codec, MediaCodec.CodecException e)814 public void onError(MediaCodec codec, MediaCodec.CodecException e) { 815 816 } 817 818 @Override onOutputFormatChanged(MediaCodec codec, MediaFormat format)819 public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) { 820 } 821 } 822 } 823