• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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