| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.media.cts; |
| |
| import com.android.cts.media.R; |
| |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.res.AssetFileDescriptor; |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecInfo.CodecCapabilities; |
| import android.media.MediaCodecInfo.CodecProfileLevel; |
| import android.media.MediaCodecList; |
| import android.media.MediaExtractor; |
| import android.media.MediaFormat; |
| import android.util.Log; |
| import android.view.Surface; |
| |
| import android.opengl.GLES20; |
| import javax.microedition.khronos.opengles.GL10; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Locale; |
| import java.util.zip.CRC32; |
| |
| public class AdaptivePlaybackTest extends MediaPlayerTestBase { |
| private static final String TAG = "AdaptivePlaybackTest"; |
| private boolean sanity = false; |
| private static final int MIN_FRAMES_BEFORE_DRC = 2; |
| |
| public Iterable<Codec> H264(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_AVC, |
| "OMX.google.h264.decoder", |
| R.raw.video_480x360_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz, |
| R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz); |
| } |
| |
| public Iterable<Codec> HEVC(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_HEVC, |
| "OMX.google.hevc.decoder", |
| R.raw.video_640x360_mp4_hevc_450kbps_30fps_aac_stereo_128kbps_48000hz, |
| R.raw.video_1280x720_mp4_hevc_1150kbps_30fps_aac_stereo_128kbps_48000hz); |
| } |
| |
| public Iterable<Codec> H263(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_H263, |
| "OMX.google.h263.decoder", |
| R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_22050hz, |
| R.raw.video_352x288_3gp_h263_300kbps_12fps_aac_stereo_128kbps_22050hz); |
| } |
| |
| public Iterable<Codec> Mpeg4(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_MPEG4, |
| "OMX.google.mpeg4.decoder", |
| |
| R.raw.video_1280x720_mp4_mpeg4_1000kbps_25fps_aac_stereo_128kbps_44100hz, |
| R.raw.video_480x360_mp4_mpeg4_860kbps_25fps_aac_stereo_128kbps_44100hz); |
| } |
| |
| public Iterable<Codec> VP8(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_VP8, |
| "OMX.google.vp8.decoder", |
| R.raw.video_480x360_webm_vp8_333kbps_25fps_vorbis_stereo_128kbps_48000hz, |
| R.raw.video_1280x720_webm_vp8_333kbps_25fps_vorbis_stereo_128kbps_44100hz); |
| } |
| |
| public Iterable<Codec> VP9(CodecFactory factory) { |
| return factory.createCodecList( |
| mContext, |
| MediaFormat.MIMETYPE_VIDEO_VP9, |
| "OMX.google.vp9.decoder", |
| R.raw.video_480x360_webm_vp9_333kbps_25fps_vorbis_stereo_128kbps_48000hz, |
| R.raw.video_1280x720_webm_vp9_309kbps_25fps_vorbis_stereo_128kbps_48000hz); |
| } |
| |
| CodecFactory ALL = new CodecFactory(); |
| CodecFactory SW = new SWCodecFactory(); |
| CodecFactory HW = new HWCodecFactory(); |
| |
| public Iterable<Codec> H264() { return H264(ALL); } |
| public Iterable<Codec> HEVC() { return HEVC(ALL); } |
| public Iterable<Codec> VP8() { return VP8(ALL); } |
| public Iterable<Codec> VP9() { return VP9(ALL); } |
| public Iterable<Codec> Mpeg4() { return Mpeg4(ALL); } |
| public Iterable<Codec> H263() { return H263(ALL); } |
| |
| public Iterable<Codec> AllCodecs() { |
| return chain(H264(ALL), HEVC(ALL), VP8(ALL), VP9(ALL), Mpeg4(ALL), H263(ALL)); |
| } |
| |
| public Iterable<Codec> SWCodecs() { |
| return chain(H264(SW), HEVC(SW), VP8(SW), VP9(SW), Mpeg4(SW), H263(SW)); |
| } |
| |
| public Iterable<Codec> HWCodecs() { |
| return chain(H264(HW), HEVC(HW), VP8(HW), VP9(HW), Mpeg4(HW), H263(HW)); |
| } |
| |
| /* tests for adaptive codecs */ |
| Test adaptiveEarlyEos = new EarlyEosTest().adaptive(); |
| Test adaptiveEosFlushSeek = new EosFlushSeekTest().adaptive(); |
| Test adaptiveSkipAhead = new AdaptiveSkipTest(true /* forward */); |
| Test adaptiveSkipBack = new AdaptiveSkipTest(false /* forward */); |
| |
| /* DRC tests for adaptive codecs */ |
| Test adaptiveReconfigDrc = new ReconfigDrcTest().adaptive(); |
| Test adaptiveSmallReconfigDrc = new ReconfigDrcTest().adaptiveSmall(); |
| Test adaptiveDrc = new AdaptiveDrcTest(); /* adaptive */ |
| Test adaptiveSmallDrc = new AdaptiveDrcTest().adaptiveSmall(); |
| |
| /* tests for regular codecs */ |
| Test earlyEos = new EarlyEosTest(); |
| Test eosFlushSeek = new EosFlushSeekTest(); |
| Test flushConfigureDrc = new ReconfigDrcTest(); |
| |
| Test[] allTests = { |
| adaptiveEarlyEos, |
| adaptiveEosFlushSeek, |
| adaptiveSkipAhead, |
| adaptiveSkipBack, |
| adaptiveReconfigDrc, |
| adaptiveSmallReconfigDrc, |
| adaptiveDrc, |
| adaptiveSmallDrc, |
| earlyEos, |
| eosFlushSeek, |
| flushConfigureDrc, |
| }; |
| |
| /* helpers to run sets of tests */ |
| public void runEOS() { ex(AllCodecs(), new Test[] { |
| adaptiveEarlyEos, |
| adaptiveEosFlushSeek, |
| adaptiveReconfigDrc, |
| adaptiveSmallReconfigDrc, |
| earlyEos, |
| eosFlushSeek, |
| flushConfigureDrc, |
| }); } |
| |
| public void runAll() { ex(AllCodecs(), allTests); } |
| public void runSW() { ex(SWCodecs(), allTests); } |
| public void runHW() { ex(HWCodecs(), allTests); } |
| |
| public void sanityAll() { sanity = true; try { runAll(); } finally { sanity = false; } } |
| public void sanitySW() { sanity = true; try { runSW(); } finally { sanity = false; } } |
| public void sanityHW() { sanity = true; try { runHW(); } finally { sanity = false; } } |
| |
| public void runH264() { ex(H264(), allTests); } |
| public void runHEVC() { ex(HEVC(), allTests); } |
| public void runVP8() { ex(VP8(), allTests); } |
| public void runVP9() { ex(VP9(), allTests); } |
| public void runMpeg4() { ex(Mpeg4(), allTests); } |
| public void runH263() { ex(H263(), allTests); } |
| |
| public void onlyH264HW() { ex(H264(HW), allTests); } |
| public void onlyHEVCHW() { ex(HEVC(HW), allTests); } |
| public void onlyVP8HW() { ex(VP8(HW), allTests); } |
| public void onlyVP9HW() { ex(VP9(HW), allTests); } |
| public void onlyMpeg4HW() { ex(Mpeg4(HW), allTests); } |
| public void onlyH263HW() { ex(H263(HW), allTests); } |
| |
| public void onlyH264SW() { ex(H264(SW), allTests); } |
| public void onlyHEVCSW() { ex(HEVC(SW), allTests); } |
| public void onlyVP8SW() { ex(VP8(SW), allTests); } |
| public void onlyVP9SW() { ex(VP9(SW), allTests); } |
| public void onlyMpeg4SW() { ex(Mpeg4(SW), allTests); } |
| public void onlyH263SW() { ex(H263(SW), allTests); } |
| |
| public void bytebuffer() { ex(H264(SW), new EarlyEosTest().byteBuffer()); } |
| public void texture() { ex(H264(HW), new EarlyEosTest().texture()); } |
| |
| /* inidividual tests */ |
| public void testH264_adaptiveEarlyEos() { ex(H264(), adaptiveEarlyEos); } |
| public void testHEVC_adaptiveEarlyEos() { ex(HEVC(), adaptiveEarlyEos); } |
| public void testVP8_adaptiveEarlyEos() { ex(VP8(), adaptiveEarlyEos); } |
| public void testVP9_adaptiveEarlyEos() { ex(VP9(), adaptiveEarlyEos); } |
| public void testMpeg4_adaptiveEarlyEos() { ex(Mpeg4(), adaptiveEarlyEos); } |
| public void testH263_adaptiveEarlyEos() { ex(H263(), adaptiveEarlyEos); } |
| |
| public void testH264_adaptiveEosFlushSeek() { ex(H264(), adaptiveEosFlushSeek); } |
| public void testHEVC_adaptiveEosFlushSeek() { ex(HEVC(), adaptiveEosFlushSeek); } |
| public void testVP8_adaptiveEosFlushSeek() { ex(VP8(), adaptiveEosFlushSeek); } |
| public void testVP9_adaptiveEosFlushSeek() { ex(VP9(), adaptiveEosFlushSeek); } |
| public void testMpeg4_adaptiveEosFlushSeek() { ex(Mpeg4(), adaptiveEosFlushSeek); } |
| public void testH263_adaptiveEosFlushSeek() { ex(H263(), adaptiveEosFlushSeek); } |
| |
| public void testH264_adaptiveSkipAhead() { ex(H264(), adaptiveSkipAhead); } |
| public void testHEVC_adaptiveSkipAhead() { ex(HEVC(), adaptiveSkipAhead); } |
| public void testVP8_adaptiveSkipAhead() { ex(VP8(), adaptiveSkipAhead); } |
| public void testVP9_adaptiveSkipAhead() { ex(VP9(), adaptiveSkipAhead); } |
| public void testMpeg4_adaptiveSkipAhead() { ex(Mpeg4(), adaptiveSkipAhead); } |
| public void testH263_adaptiveSkipAhead() { ex(H263(), adaptiveSkipAhead); } |
| |
| public void testH264_adaptiveSkipBack() { ex(H264(), adaptiveSkipBack); } |
| public void testHEVC_adaptiveSkipBack() { ex(HEVC(), adaptiveSkipBack); } |
| public void testVP8_adaptiveSkipBack() { ex(VP8(), adaptiveSkipBack); } |
| public void testVP9_adaptiveSkipBack() { ex(VP9(), adaptiveSkipBack); } |
| public void testMpeg4_adaptiveSkipBack() { ex(Mpeg4(), adaptiveSkipBack); } |
| public void testH263_adaptiveSkipBack() { ex(H263(), adaptiveSkipBack); } |
| |
| public void testH264_adaptiveReconfigDrc() { ex(H264(), adaptiveReconfigDrc); } |
| public void testHEVC_adaptiveReconfigDrc() { ex(HEVC(), adaptiveReconfigDrc); } |
| public void testVP8_adaptiveReconfigDrc() { ex(VP8(), adaptiveReconfigDrc); } |
| public void testVP9_adaptiveReconfigDrc() { ex(VP9(), adaptiveReconfigDrc); } |
| public void testMpeg4_adaptiveReconfigDrc() { ex(Mpeg4(), adaptiveReconfigDrc); } |
| public void testH263_adaptiveReconfigDrc() { ex(H263(), adaptiveReconfigDrc); } |
| |
| public void testH264_adaptiveSmallReconfigDrc() { ex(H264(), adaptiveSmallReconfigDrc); } |
| public void testHEVC_adaptiveSmallReconfigDrc() { ex(HEVC(), adaptiveSmallReconfigDrc); } |
| public void testVP8_adaptiveSmallReconfigDrc() { ex(VP8(), adaptiveSmallReconfigDrc); } |
| public void testVP9_adaptiveSmallReconfigDrc() { ex(VP9(), adaptiveSmallReconfigDrc); } |
| public void testMpeg4_adaptiveSmallReconfigDrc() { ex(Mpeg4(), adaptiveSmallReconfigDrc); } |
| public void testH263_adaptiveSmallReconfigDrc() { ex(H263(), adaptiveSmallReconfigDrc); } |
| |
| public void testH264_adaptiveDrc() { ex(H264(), adaptiveDrc); } |
| public void testHEVC_adaptiveDrc() { ex(HEVC(), adaptiveDrc); } |
| public void testVP8_adaptiveDrc() { ex(VP8(), adaptiveDrc); } |
| public void testVP9_adaptiveDrc() { ex(VP9(), adaptiveDrc); } |
| public void testMpeg4_adaptiveDrc() { ex(Mpeg4(), adaptiveDrc); } |
| public void testH263_adaptiveDrc() { ex(H263(), adaptiveDrc); } |
| |
| public void testH264_adaptiveDrcEarlyEos() { ex(H264(), new AdaptiveDrcEarlyEosTest()); } |
| public void testHEVC_adaptiveDrcEarlyEos() { ex(HEVC(), new AdaptiveDrcEarlyEosTest()); } |
| public void testVP8_adaptiveDrcEarlyEos() { ex(VP8(), new AdaptiveDrcEarlyEosTest()); } |
| public void testVP9_adaptiveDrcEarlyEos() { ex(VP9(), new AdaptiveDrcEarlyEosTest()); } |
| |
| public void testH264_adaptiveSmallDrc() { ex(H264(), adaptiveSmallDrc); } |
| public void testHEVC_adaptiveSmallDrc() { ex(HEVC(), adaptiveSmallDrc); } |
| public void testVP8_adaptiveSmallDrc() { ex(VP8(), adaptiveSmallDrc); } |
| public void testVP9_adaptiveSmallDrc() { ex(VP9(), adaptiveSmallDrc); } |
| |
| public void testH264_earlyEos() { ex(H264(), earlyEos); } |
| public void testHEVC_earlyEos() { ex(HEVC(), earlyEos); } |
| public void testVP8_earlyEos() { ex(VP8(), earlyEos); } |
| public void testVP9_earlyEos() { ex(VP9(), earlyEos); } |
| public void testMpeg4_earlyEos() { ex(Mpeg4(), earlyEos); } |
| public void testH263_earlyEos() { ex(H263(), earlyEos); } |
| |
| public void testH264_eosFlushSeek() { ex(H264(), eosFlushSeek); } |
| public void testHEVC_eosFlushSeek() { ex(HEVC(), eosFlushSeek); } |
| public void testVP8_eosFlushSeek() { ex(VP8(), eosFlushSeek); } |
| public void testVP9_eosFlushSeek() { ex(VP9(), eosFlushSeek); } |
| public void testMpeg4_eosFlushSeek() { ex(Mpeg4(), eosFlushSeek); } |
| public void testH263_eosFlushSeek() { ex(H263(), eosFlushSeek); } |
| |
| public void testH264_flushConfigureDrc() { ex(H264(), flushConfigureDrc); } |
| public void testHEVC_flushConfigureDrc() { ex(HEVC(), flushConfigureDrc); } |
| public void testVP8_flushConfigureDrc() { ex(VP8(), flushConfigureDrc); } |
| public void testVP9_flushConfigureDrc() { ex(VP9(), flushConfigureDrc); } |
| public void testMpeg4_flushConfigureDrc() { ex(Mpeg4(), flushConfigureDrc); } |
| public void testH263_flushConfigureDrc() { ex(H263(), flushConfigureDrc); } |
| |
| /* only use unchecked exceptions to allow brief test methods */ |
| private void ex(Iterable<Codec> codecList, Test test) { |
| ex(codecList, new Test[] { test } ); |
| } |
| |
| private void ex(Iterable<Codec> codecList, Test[] testList) { |
| if (codecList == null) { |
| Log.i(TAG, "CodecList was empty. Skipping test."); |
| return; |
| } |
| |
| TestList tests = new TestList(); |
| for (Codec c : codecList) { |
| for (Test test : testList) { |
| if (test.isValid(c)) { |
| test.addTests(tests, c); |
| } |
| } |
| } |
| try { |
| tests.run(); |
| } catch (Throwable t) { |
| throw new RuntimeException(t); |
| } |
| } |
| |
| /* need an inner class to have access to the activity */ |
| abstract class ActivityTest extends Test { |
| TestSurface mNullSurface = new ActivitySurface(null); |
| protected TestSurface getSurface() { |
| if (mUseSurface) { |
| return new ActivitySurface(getActivity().getSurfaceHolder().getSurface()); |
| } else if (mUseSurfaceTexture) { |
| return new DecoderSurface(1280, 720, mCRC); |
| } |
| return mNullSurface; |
| } |
| } |
| |
| static final int NUM_FRAMES = 50; |
| |
| /** |
| * Queue some frames with an EOS on the last one. Test that we have decoded as many |
| * frames as we queued. This tests the EOS handling of the codec to see if all queued |
| * (and out-of-order) frames are actually decoded and returned. |
| */ |
| class EarlyEosTest extends ActivityTest { |
| public boolean isValid(Codec c) { |
| return getFormat(c) != null; |
| } |
| public void addTests(TestList tests, final Codec c) { |
| for (int i = NUM_FRAMES / 2; i > 0; i--) { |
| final int queuedFrames = i; |
| tests.add( |
| new Step("testing early EOS at " + queuedFrames, this, c) { |
| public void run() { |
| Decoder decoder = new Decoder(c.name); |
| try { |
| decoder.configureAndStart(stepFormat(), stepSurface()); |
| int decodedFrames = -decoder.queueInputBufferRange( |
| stepMedia(), |
| 0 /* startFrame */, |
| queuedFrames, |
| true /* sendEos */, |
| true /* waitForEos */); |
| if (decodedFrames <= 0) { |
| Log.w(TAG, "Did not receive EOS -- negating frame count"); |
| } |
| decoder.stop(); |
| if (decodedFrames != queuedFrames) { |
| warn("decoded " + decodedFrames + " frames out of " + |
| queuedFrames + " queued"); |
| } |
| } finally { |
| warn(decoder.getWarnings()); |
| decoder.releaseQuietly(); |
| } |
| } |
| }); |
| if (sanity) { |
| i >>= 1; |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Similar to EarlyEosTest, but we keep the component alive and running in between the steps. |
| * This is how seeking should be done if all frames must be outputted. This also tests that |
| * PTS can be repeated after flush. |
| */ |
| class EosFlushSeekTest extends ActivityTest { |
| Decoder mDecoder; // test state |
| public boolean isValid(Codec c) { |
| return getFormat(c) != null; |
| } |
| public void addTests(TestList tests, final Codec c) { |
| tests.add( |
| new Step("testing EOS & flush before seek - init", this, c) { |
| public void run() { |
| mDecoder = new Decoder(c.name); |
| mDecoder.configureAndStart(stepFormat(), stepSurface()); |
| }}); |
| |
| for (int i = NUM_FRAMES; i > 0; i--) { |
| final int queuedFrames = i; |
| tests.add( |
| new Step("testing EOS & flush before seeking after " + queuedFrames + |
| " frames", this, c) { |
| public void run() { |
| int decodedFrames = -mDecoder.queueInputBufferRange( |
| stepMedia(), |
| 0 /* startFrame */, |
| queuedFrames, |
| true /* sendEos */, |
| true /* waitForEos */); |
| if (decodedFrames != queuedFrames) { |
| warn("decoded " + decodedFrames + " frames out of " + |
| queuedFrames + " queued"); |
| } |
| warn(mDecoder.getWarnings()); |
| mDecoder.clearWarnings(); |
| mDecoder.flush(); |
| } |
| }); |
| if (sanity) { |
| i >>= 1; |
| } |
| } |
| |
| tests.add( |
| new Step("testing EOS & flush before seek - finally", this, c) { |
| public void run() { |
| try { |
| mDecoder.stop(); |
| } finally { |
| mDecoder.release(); |
| } |
| }}); |
| } |
| }; |
| |
| /** |
| * Similar to EosFlushSeekTest, but we change the media size between the steps. |
| * This is how dynamic resolution switching can be done on codecs that do not support |
| * adaptive playback. |
| */ |
| class ReconfigDrcTest extends ActivityTest { |
| Decoder mDecoder; // test state |
| public boolean isValid(Codec c) { |
| return getFormat(c) != null && c.mediaList.length > 1; |
| } |
| public void addTests(TestList tests, final Codec c) { |
| tests.add( |
| new Step("testing DRC with reconfigure - init", this, c) { |
| public void run() { |
| mDecoder = new Decoder(c.name); |
| }}); |
| |
| for (int i = NUM_FRAMES, ix = 0; i > 0; i--, ix++) { |
| final int queuedFrames = i; |
| final int mediaIx = ix % c.mediaList.length; |
| tests.add( |
| new Step("testing DRC with reconfigure after " + queuedFrames + " frames", |
| this, c, mediaIx) { |
| public void run() { |
| try { |
| //mDecoder.configureAndStart(stepFormat(), stepSurface()); |
| if(mDecoder.configureAndStart(stepFormat(), stepSurface())){ |
| int decodedFrames = -mDecoder.queueInputBufferRange( |
| stepMedia(), |
| 0 /* startFrame */, |
| queuedFrames, |
| true /* sendEos */, |
| true /* waitForEos */); |
| if (decodedFrames != queuedFrames) { |
| warn("decoded " + decodedFrames + " frames out of " + |
| queuedFrames + " queued"); |
| } |
| warn(mDecoder.getWarnings()); |
| mDecoder.clearWarnings(); |
| mDecoder.flush(); |
| } |
| } finally { |
| mDecoder.stop(); |
| } |
| } |
| }); |
| if (sanity) { |
| i >>= 1; |
| } |
| } |
| tests.add( |
| new Step("testing DRC with reconfigure - finally", this, c) { |
| public void run() { |
| mDecoder.release(); |
| }}); |
| } |
| }; |
| |
| /* ADAPTIVE-ONLY TESTS - only run on codecs that support adaptive playback */ |
| |
| /** |
| * Test dynamic resolution change support. Queue various sized media segments |
| * with different resolutions, verify that all queued frames were decoded. Here |
| * PTS will grow between segments. |
| */ |
| class AdaptiveDrcTest extends ActivityTest { |
| Decoder mDecoder; |
| int mAdjustTimeUs; |
| int mDecodedFrames; |
| int mQueuedFrames; |
| |
| public AdaptiveDrcTest() { |
| super(); |
| adaptive(); |
| } |
| public boolean isValid(Codec c) { |
| checkAdaptiveFormat(); |
| return c.adaptive && c.mediaList.length > 1; |
| } |
| public void addTests(TestList tests, final Codec c) { |
| tests.add( |
| new Step("testing DRC with no reconfigure - init", this, c) { |
| public void run() throws Throwable { |
| // FIXME wait 2 seconds to allow system to free up previous codecs |
| try { |
| Thread.sleep(2000); |
| } catch (InterruptedException e) {} |
| mDecoder = new Decoder(c.name); |
| mDecoder.configureAndStart(stepFormat(), stepSurface()); |
| mAdjustTimeUs = 0; |
| mDecodedFrames = 0; |
| mQueuedFrames = 0; |
| }}); |
| |
| for (int i = NUM_FRAMES, ix = 0; i >= MIN_FRAMES_BEFORE_DRC; i--, ix++) { |
| final int mediaIx = ix % c.mediaList.length; |
| final int segmentSize = i; |
| tests.add( |
| new Step("testing DRC with no reconfigure after " + i + " frames", |
| this, c, mediaIx) { |
| public void run() throws Throwable { |
| mQueuedFrames += segmentSize; |
| boolean lastSequence = segmentSize == MIN_FRAMES_BEFORE_DRC; |
| if (sanity) { |
| lastSequence = (segmentSize >> 1) <= MIN_FRAMES_BEFORE_DRC; |
| } |
| int frames = mDecoder.queueInputBufferRange( |
| stepMedia(), |
| 0 /* startFrame */, |
| segmentSize, |
| lastSequence /* sendEos */, |
| lastSequence /* expectEos */, |
| mAdjustTimeUs); |
| if (lastSequence && frames >= 0) { |
| warn("did not receive EOS, received " + frames + " frames"); |
| } else if (!lastSequence && frames < 0) { |
| warn("received EOS, received " + (-frames) + " frames"); |
| } |
| warn(mDecoder.getWarnings()); |
| mDecoder.clearWarnings(); |
| |
| mDecodedFrames += Math.abs(frames); |
| mAdjustTimeUs += 1 + stepMedia().getTimestampRangeValue( |
| 0, segmentSize, Media.RANGE_END); |
| }}); |
| if (sanity) { |
| i >>= 1; |
| } |
| } |
| tests.add( |
| new Step("testing DRC with no reconfigure - init", this, c) { |
| public void run() throws Throwable { |
| if (mDecodedFrames != mQueuedFrames) { |
| warn("decoded " + mDecodedFrames + " frames out of " + |
| mQueuedFrames + " queued"); |
| } |
| try { |
| mDecoder.stop(); |
| } finally { |
| mDecoder.release(); |
| } |
| } |
| }); |
| } |
| }; |
| |
| /** |
| * Queue EOS shortly after a dynamic resolution change. Test that all frames were |
| * decoded. |
| */ |
| class AdaptiveDrcEarlyEosTest extends ActivityTest { |
| public AdaptiveDrcEarlyEosTest() { |
| super(); |
| adaptive(); |
| } |
| public boolean isValid(Codec c) { |
| checkAdaptiveFormat(); |
| return c.adaptive && c.mediaList.length > 1; |
| } |
| public Step testStep(final Codec c, final int framesBeforeDrc, |
| final int framesBeforeEos) { |
| return new Step("testing DRC with no reconfigure after " + framesBeforeDrc + |
| " frames and subsequent EOS after " + framesBeforeEos + " frames", |
| this, c) { |
| public void run() throws Throwable { |
| Decoder decoder = new Decoder(c.name); |
| int queuedFrames = framesBeforeDrc + framesBeforeEos; |
| int framesA = 0; |
| int framesB = 0; |
| try { |
| decoder.configureAndStart(stepFormat(), stepSurface()); |
| Media media = c.mediaList[0]; |
| |
| framesA = decoder.queueInputBufferRange( |
| media, |
| 0 /* startFrame */, |
| framesBeforeDrc, |
| false /* sendEos */, |
| false /* expectEos */); |
| if (framesA < 0) { |
| warn("received unexpected EOS, received " + (-framesA) + " frames"); |
| } |
| long adjustTimeUs = 1 + media.getTimestampRangeValue( |
| 0, framesBeforeDrc, Media.RANGE_END); |
| |
| media = c.mediaList[1]; |
| framesB = decoder.queueInputBufferRange( |
| media, |
| 0 /* startFrame */, |
| framesBeforeEos, |
| true /* sendEos */, |
| true /* expectEos */, |
| adjustTimeUs); |
| if (framesB >= 0) { |
| warn("did not receive EOS, received " + (-framesB) + " frames"); |
| } |
| decoder.stop(); |
| warn(decoder.getWarnings()); |
| } finally { |
| int decodedFrames = Math.abs(framesA) + Math.abs(framesB); |
| if (decodedFrames != queuedFrames) { |
| warn("decoded " + decodedFrames + " frames out of " + queuedFrames + |
| " queued"); |
| } |
| decoder.release(); |
| } |
| } |
| }; |
| } |
| public void addTests(TestList tests, Codec c) { |
| for (int drcFrame = 6; drcFrame >= MIN_FRAMES_BEFORE_DRC; drcFrame--) { |
| for (int eosFrame = 6; eosFrame >= 1; eosFrame--) { |
| tests.add(testStep(c, drcFrame, eosFrame)); |
| } |
| } |
| } |
| }; |
| |
| /** |
| * Similar to AdaptiveDrcTest, but tests that PTS can change at adaptive boundaries both |
| * forward and backward without the need to flush. |
| */ |
| class AdaptiveSkipTest extends ActivityTest { |
| boolean forward; |
| public AdaptiveSkipTest(boolean fwd) { |
| forward = fwd; |
| adaptive(); |
| } |
| public boolean isValid(Codec c) { |
| checkAdaptiveFormat(); |
| return c.adaptive; |
| } |
| Decoder mDecoder; |
| int mAdjustTimeUs = 0; |
| int mDecodedFrames = 0; |
| int mQueuedFrames = 0; |
| public void addTests(TestList tests, final Codec c) { |
| tests.add( |
| new Step("testing flushless skipping - init", this, c) { |
| public void run() throws Throwable { |
| mDecoder = new Decoder(c.name); |
| mDecoder.configureAndStart(stepFormat(), stepSurface()); |
| mAdjustTimeUs = 0; |
| mDecodedFrames = 0; |
| mQueuedFrames = 0; |
| }}); |
| |
| for (int i = 2, ix = 0; i <= NUM_FRAMES; i++, ix++) { |
| final int mediaIx = ix % c.mediaList.length; |
| final int segmentSize = i; |
| final boolean lastSequence; |
| if (sanity) { |
| lastSequence = (segmentSize << 1) + 1 > NUM_FRAMES; |
| } else { |
| lastSequence = segmentSize >= NUM_FRAMES; |
| } |
| tests.add( |
| new Step("testing flushless skipping " + (forward ? "forward" : "backward") + |
| " after " + i + " frames", this, c) { |
| public void run() throws Throwable { |
| int frames = mDecoder.queueInputBufferRange( |
| stepMedia(), |
| 0 /* startFrame */, |
| segmentSize, |
| lastSequence /* sendEos */, |
| lastSequence /* expectEos */, |
| mAdjustTimeUs); |
| if (lastSequence && frames >= 0) { |
| warn("did not receive EOS, received " + frames + " frames"); |
| } else if (!lastSequence && frames < 0) { |
| warn("received unexpected EOS, received " + (-frames) + " frames"); |
| } |
| warn(mDecoder.getWarnings()); |
| mDecoder.clearWarnings(); |
| |
| mQueuedFrames += segmentSize; |
| mDecodedFrames += Math.abs(frames); |
| if (forward) { |
| mAdjustTimeUs += 10000000 + stepMedia().getTimestampRangeValue( |
| 0, segmentSize, Media.RANGE_DURATION); |
| } |
| }}); |
| if (sanity) { |
| i <<= 1; |
| } |
| } |
| |
| tests.add( |
| new Step("testing flushless skipping - finally", this, c) { |
| public void run() throws Throwable { |
| if (mDecodedFrames != mQueuedFrames) { |
| warn("decoded " + mDecodedFrames + " frames out of " + mQueuedFrames + |
| " queued"); |
| } |
| try { |
| mDecoder.stop(); |
| } finally { |
| mDecoder.release(); |
| } |
| }}); |
| } |
| }; |
| |
| // not yet used |
| static long checksum(ByteBuffer buf, int size, CRC32 crc) { |
| assertTrue(size >= 0); |
| assertTrue(size <= buf.capacity()); |
| crc.reset(); |
| if (buf.hasArray()) { |
| crc.update(buf.array(), buf.arrayOffset(), size); |
| } else { |
| int pos = buf.position(); |
| buf.rewind(); |
| final int rdsize = Math.min(4096, size); |
| byte bb[] = new byte[rdsize]; |
| int chk; |
| for (int i = 0; i < size; i += chk) { |
| chk = Math.min(rdsize, size - i); |
| buf.get(bb, 0, chk); |
| crc.update(bb, 0, chk); |
| } |
| buf.position(pos); |
| } |
| return crc.getValue(); |
| } |
| |
| CRC32 mCRC; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mCRC = new CRC32(); |
| } |
| |
| /* ====================================================================== */ |
| /* UTILITY FUNCTIONS */ |
| /* ====================================================================== */ |
| public static String collectionString(Collection<?> c) { |
| StringBuilder res = new StringBuilder("["); |
| boolean subsequent = false; |
| for (Object o: c) { |
| if (subsequent) { |
| res.append(", "); |
| } |
| res.append(o); |
| subsequent = true; |
| } |
| return res.append("]").toString(); |
| } |
| |
| static String byteBufferToString(ByteBuffer buf, int start, int len) { |
| int oldPosition = buf.position(); |
| buf.position(start); |
| int strlen = 2; // {} |
| boolean ellipsis = len < buf.limit(); |
| if (ellipsis) { |
| strlen += 3; // ... |
| } else { |
| len = buf.limit(); |
| } |
| strlen += 3 * len - (len > 0 ? 1 : 0); // XX,XX |
| char[] res = new char[strlen]; |
| res[0] = '{'; |
| res[strlen - 1] = '}'; |
| if (ellipsis) { |
| res[strlen - 2] = res[strlen - 3] = res[strlen - 4] = '.'; |
| } |
| for (int i = 1; i < len; i++) { |
| res[i * 3] = ','; |
| } |
| for (int i = 0; i < len; i++) { |
| byte b = buf.get(); |
| int d = (b >> 4) & 15; |
| res[i * 3 + 1] = (char)(d + (d > 9 ? 'a' - 10 : '0')); |
| d = (b & 15); |
| res[i * 3 + 2] = (char)(d + (d > 9 ? 'a' - 10 : '0')); |
| } |
| buf.position(oldPosition); |
| return new String(res); |
| } |
| |
| static <E> Iterable<E> chain(Iterable<E> ... iterables) { |
| /* simple chainer using ArrayList */ |
| ArrayList<E> items = new ArrayList<E>(); |
| for (Iterable<E> it: iterables) { |
| for (E el: it) { |
| items.add(el); |
| } |
| } |
| return items; |
| } |
| |
| class Decoder { |
| private final static String TAG = "AdaptiveDecoder"; |
| final long kTimeOutUs = 5000; |
| MediaCodec mCodec; |
| ByteBuffer[] mInputBuffers; |
| ByteBuffer[] mOutputBuffers; |
| TestSurface mSurface; |
| boolean mDoChecksum; |
| boolean mQueuedEos; |
| ArrayList<Long> mTimeStamps; |
| ArrayList<String> mWarnings; |
| |
| public Decoder(String codecName) { |
| MediaCodec codec = null; |
| try { |
| codec = MediaCodec.createByCodecName(codecName); |
| } catch (Exception e) { |
| throw new RuntimeException("couldn't create codec " + codecName, e); |
| } |
| Log.i(TAG, "using codec: " + codec.getName()); |
| mCodec = codec; |
| mDoChecksum = false; |
| mQueuedEos = false; |
| mTimeStamps = new ArrayList<Long>(); |
| mWarnings = new ArrayList<String>(); |
| } |
| |
| public String getName() { |
| return mCodec.getName(); |
| } |
| |
| public Iterable<String> getWarnings() { |
| return mWarnings; |
| } |
| |
| private void warn(String warning) { |
| mWarnings.add(warning); |
| Log.w(TAG, warning); |
| } |
| |
| public void clearWarnings() { |
| mWarnings.clear(); |
| } |
| |
| public boolean configureAndStart(MediaFormat format, TestSurface surface) { |
| mSurface = surface; |
| Log.i(TAG, "configure(" + format + ", " + mSurface.getSurface() + ")"); |
| try{ |
| mCodec.configure(format, mSurface.getSurface(), null /* crypto */, 0 /* flags */); |
| } |
| catch (Exception e){ |
| Log.i(TAG, "Unsupported Codec"); |
| return false; |
| } |
| Log.i(TAG, "start"); |
| mCodec.start(); |
| mInputBuffers = mCodec.getInputBuffers(); |
| mOutputBuffers = mCodec.getOutputBuffers(); |
| Log.i(TAG, "configured " + mInputBuffers.length + " input[" + |
| mInputBuffers[0].capacity() + "] and " + |
| mOutputBuffers.length + "output[" + |
| (mOutputBuffers[0] == null ? null : mOutputBuffers[0].capacity()) + "]"); |
| mQueuedEos = false; |
| return true; |
| } |
| |
| public void stop() { |
| Log.i(TAG, "stop"); |
| mCodec.stop(); |
| } |
| |
| public void flush() { |
| Log.i(TAG, "flush"); |
| mCodec.flush(); |
| mQueuedEos = false; |
| mTimeStamps.clear(); |
| } |
| |
| public String dequeueAndReleaseOutputBuffer(MediaCodec.BufferInfo info) { |
| int ix = mCodec.dequeueOutputBuffer(info, kTimeOutUs); |
| if (ix == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { |
| mOutputBuffers = mCodec.getOutputBuffers(); |
| Log.d(TAG, "output buffers have changed."); |
| return null; |
| } else if (ix == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
| MediaFormat format = mCodec.getOutputFormat(); |
| Log.d(TAG, "output format has changed to " + format); |
| int colorFormat = format.getInteger(MediaFormat.KEY_COLOR_FORMAT); |
| mDoChecksum = isRecognizedFormat(colorFormat); |
| return null; |
| } else if (ix < 0) { |
| Log.v(TAG, "no output"); |
| return null; |
| } |
| /* create checksum */ |
| long sum = 0; |
| |
| |
| Log.v(TAG, "dequeue #" + ix + " => { [" + info.size + "] flags=" + info.flags + |
| " @" + info.presentationTimeUs + "}"); |
| |
| // we get a nonzero size for valid decoded frames |
| boolean doRender = (info.size != 0); |
| if (mSurface.getSurface() == null) { |
| if (mDoChecksum) { |
| sum = checksum(mOutputBuffers[ix], info.size, mCRC); |
| } |
| mCodec.releaseOutputBuffer(ix, doRender); |
| } else if (doRender) { |
| // If using SurfaceTexture, as soon as we call releaseOutputBuffer, the |
| // buffer will be forwarded to SurfaceTexture to convert to a texture. |
| // The API doesn't guarantee that the texture will be available before |
| // the call returns, so we need to wait for the onFrameAvailable callback |
| // to fire. If we don't wait, we risk dropping frames. |
| mSurface.prepare(); |
| mCodec.releaseOutputBuffer(ix, doRender); |
| mSurface.waitForDraw(); |
| if (mDoChecksum) { |
| sum = mSurface.checksum(); |
| } |
| } else { |
| mCodec.releaseOutputBuffer(ix, doRender); |
| } |
| |
| if (doRender) { |
| if (!mTimeStamps.remove(info.presentationTimeUs)) { |
| warn("invalid timestamp " + info.presentationTimeUs + ", queued " + |
| collectionString(mTimeStamps)); |
| } |
| } |
| |
| return String.format(Locale.US, "{pts=%d, flags=%x, data=0x%x}", |
| info.presentationTimeUs, info.flags, sum); |
| } |
| |
| /* returns true iff queued a frame */ |
| public boolean queueInputBuffer(Media media, int frameIx, boolean EOS) { |
| return queueInputBuffer(media, frameIx, EOS, 0); |
| } |
| |
| public boolean queueInputBuffer(Media media, int frameIx, boolean EOS, long adjustTimeUs) { |
| if (mQueuedEos) { |
| return false; |
| } |
| |
| int ix = mCodec.dequeueInputBuffer(kTimeOutUs); |
| |
| if (ix < 0) { |
| return false; |
| } |
| |
| ByteBuffer buf = mInputBuffers[ix]; |
| Media.Frame frame = media.getFrame(frameIx); |
| buf.clear(); |
| |
| long presentationTimeUs = adjustTimeUs; |
| int flags = 0; |
| if (frame != null) { |
| buf.put((ByteBuffer)frame.buf.clear()); |
| presentationTimeUs += frame.presentationTimeUs; |
| flags = frame.flags; |
| } |
| |
| if (EOS) { |
| flags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM; |
| mQueuedEos = true; |
| } |
| |
| mTimeStamps.add(presentationTimeUs); |
| Log.v(TAG, "queue { [" + buf.position() + "]=" + byteBufferToString(buf, 0, 16) + |
| " flags=" + flags + " @" + presentationTimeUs + "} => #" + ix); |
| mCodec.queueInputBuffer( |
| ix, 0 /* offset */, buf.position(), presentationTimeUs, flags); |
| return true; |
| } |
| |
| /* returns number of frames received multiplied by -1 if received EOS, 1 otherwise */ |
| public int queueInputBufferRange( |
| Media media, int frameStartIx, int frameEndIx, boolean sendEosAtEnd, |
| boolean waitForEos) { |
| return queueInputBufferRange(media,frameStartIx,frameEndIx,sendEosAtEnd,waitForEos,0); |
| } |
| |
| public int queueInputBufferRange( |
| Media media, int frameStartIx, int frameEndIx, boolean sendEosAtEnd, |
| boolean waitForEos, long adjustTimeUs) { |
| MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); |
| int frameIx = frameStartIx; |
| int numFramesDecoded = 0; |
| boolean sawOutputEos = false; |
| int deadDecoderCounter = 0; |
| ArrayList<String> frames = new ArrayList<String>(); |
| while ((waitForEos && !sawOutputEos) || frameIx < frameEndIx) { |
| if (frameIx < frameEndIx) { |
| if (queueInputBuffer( |
| media, |
| frameIx, |
| sendEosAtEnd && (frameIx + 1 == frameEndIx), |
| adjustTimeUs)) { |
| frameIx++; |
| } |
| } |
| |
| String buf = dequeueAndReleaseOutputBuffer(info); |
| if (buf != null) { |
| // Some decoders output a 0-sized buffer at the end. Disregard those. |
| if (info.size > 0) { |
| deadDecoderCounter = 0; |
| numFramesDecoded++; |
| } |
| |
| if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
| Log.d(TAG, "saw output EOS."); |
| sawOutputEos = true; |
| } |
| } |
| if (++deadDecoderCounter >= 100) { |
| warn("have not received an output frame for a while"); |
| break; |
| } |
| } |
| |
| if (numFramesDecoded < frameEndIx - frameStartIx - 16) { |
| fail("Queued " + (frameEndIx - frameStartIx) + " frames but only received " + |
| numFramesDecoded); |
| } |
| return (sawOutputEos ? -1 : 1) * numFramesDecoded; |
| } |
| |
| void release() { |
| Log.i(TAG, "release"); |
| mCodec.release(); |
| mSurface.release(); |
| mInputBuffers = null; |
| mOutputBuffers = null; |
| mCodec = null; |
| mSurface = null; |
| } |
| |
| // don't fail on exceptions in release() |
| void releaseQuietly() { |
| try { |
| Log.i(TAG, "release"); |
| mCodec.release(); |
| } catch (Throwable e) { |
| Log.e(TAG, "Exception while releasing codec", e); |
| } |
| mSurface.release(); |
| mInputBuffers = null; |
| mOutputBuffers = null; |
| mCodec = null; |
| mSurface = null; |
| } |
| }; |
| |
| /* from EncodeDecodeTest */ |
| private static boolean isRecognizedFormat(int colorFormat) { |
| switch (colorFormat) { |
| // these are the formats we know how to handle for this test |
| case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: |
| case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: |
| case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: |
| case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: |
| case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| private int countFrames( |
| String codecName, MediaCodecInfo codecInfo, Media media, int eosframe, TestSurface s) |
| throws Exception { |
| Decoder codec = new Decoder(codecName); |
| codec.configureAndStart(media.getFormat(), s /* surface */); |
| |
| int numframes = codec.queueInputBufferRange( |
| media, 0, eosframe, true /* sendEos */, true /* waitForEos */); |
| if (numframes >= 0) { |
| Log.w(TAG, "Did not receive EOS"); |
| } else { |
| numframes *= -1; |
| } |
| |
| codec.stop(); |
| codec.release(); |
| return numframes; |
| } |
| } |
| |
| /* ====================================================================== */ |
| /* Video Media Asset */ |
| /* ====================================================================== */ |
| class Media { |
| private final static String TAG = "AdaptiveMedia"; |
| private MediaFormat mFormat; |
| private MediaFormat mAdaptiveFormat; |
| static class Frame { |
| long presentationTimeUs; |
| int flags; |
| ByteBuffer buf; |
| public Frame(long _pts, int _flags, ByteBuffer _buf) { |
| presentationTimeUs = _pts; |
| flags = _flags; |
| buf = _buf; |
| } |
| }; |
| private Frame[] mFrames; |
| |
| public Frame getFrame(int ix) { |
| /* this works even on short sample as frame is allocated as null */ |
| if (ix >= 0 && ix < mFrames.length) { |
| return mFrames[ix]; |
| } |
| return null; |
| } |
| private Media(MediaFormat format, MediaFormat adaptiveFormat, int numFrames) { |
| /* need separate copies of format as once we add adaptive flags to |
| MediaFormat, we cannot remove them */ |
| mFormat = format; |
| mAdaptiveFormat = adaptiveFormat; |
| mFrames = new Frame[numFrames]; |
| } |
| |
| public MediaFormat getFormat() { |
| return mFormat; |
| } |
| |
| public MediaFormat getAdaptiveFormat(int width, int height) { |
| mAdaptiveFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width); |
| mAdaptiveFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height); |
| return mAdaptiveFormat; |
| } |
| |
| public String getMime() { |
| return mFormat.getString(MediaFormat.KEY_MIME); |
| } |
| |
| public int getWidth() { |
| return mFormat.getInteger(MediaFormat.KEY_WIDTH); |
| } |
| |
| public int getHeight() { |
| return mFormat.getInteger(MediaFormat.KEY_HEIGHT); |
| } |
| |
| public final static int RANGE_START = 0; |
| public final static int RANGE_END = 1; |
| public final static int RANGE_DURATION = 2; |
| |
| public long getTimestampRangeValue(int frameStartIx, int frameEndIx, int kind) { |
| long min = Long.MAX_VALUE, max = Long.MIN_VALUE; |
| for (int frameIx = frameStartIx; frameIx < frameEndIx; frameIx++) { |
| Frame frame = getFrame(frameIx); |
| if (frame != null) { |
| if (min > frame.presentationTimeUs) { |
| min = frame.presentationTimeUs; |
| } |
| if (max < frame.presentationTimeUs) { |
| max = frame.presentationTimeUs; |
| } |
| } |
| } |
| if (kind == RANGE_START) { |
| return min; |
| } else if (kind == RANGE_END) { |
| return max; |
| } else if (kind == RANGE_DURATION) { |
| return max - min; |
| } else { |
| throw new IllegalArgumentException("kind is not valid: " + kind); |
| } |
| } |
| |
| public static Media read(Context context, int video, int numFrames) |
| throws java.io.IOException { |
| MediaExtractor extractor = new MediaExtractor(); |
| AssetFileDescriptor testFd = context.getResources().openRawResourceFd(video); |
| extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(), |
| testFd.getLength()); |
| |
| Media media = new Media( |
| extractor.getTrackFormat(0), extractor.getTrackFormat(0), numFrames); |
| extractor.selectTrack(0); |
| |
| Log.i(TAG, "format=" + media.getFormat()); |
| ArrayList<ByteBuffer> csds = new ArrayList<ByteBuffer>(); |
| for (String tag: new String[] { "csd-0", "csd-1" }) { |
| if (media.getFormat().containsKey(tag)) { |
| ByteBuffer csd = media.getFormat().getByteBuffer(tag); |
| Log.i(TAG, tag + "=" + AdaptivePlaybackTest.byteBufferToString(csd, 0, 16)); |
| csds.add(csd); |
| } |
| } |
| |
| ByteBuffer readBuf = ByteBuffer.allocate(200000); |
| for (int ix = 0; ix < numFrames; ix++) { |
| int sampleSize = extractor.readSampleData(readBuf, 0 /* offset */); |
| |
| if (sampleSize < 0) { |
| throw new IllegalArgumentException("media is too short at " + ix + " frames"); |
| } else { |
| readBuf.position(0).limit(sampleSize); |
| for (ByteBuffer csd: csds) { |
| sampleSize += csd.capacity(); |
| } |
| ByteBuffer buf = ByteBuffer.allocate(sampleSize); |
| for (ByteBuffer csd: csds) { |
| csd.clear(); |
| buf.put(csd); |
| csd.clear(); |
| Log.i(TAG, "csd[" + csd.capacity() + "]"); |
| } |
| Log.i(TAG, "frame-" + ix + "[" + sampleSize + "]"); |
| csds.clear(); |
| buf.put(readBuf); |
| media.mFrames[ix] = new Frame( |
| extractor.getSampleTime(), |
| extractor.getSampleFlags(), |
| buf); |
| extractor.advance(); |
| } |
| } |
| extractor.release(); |
| testFd.close(); |
| return media; |
| } |
| } |
| |
| /* ====================================================================== */ |
| /* Codec, CodecList and CodecFactory */ |
| /* ====================================================================== */ |
| class Codec { |
| private final static String TAG = "AdaptiveCodec"; |
| |
| public String name; |
| public CodecCapabilities capabilities; |
| public Media[] mediaList; |
| public boolean adaptive; |
| public Codec(String n, CodecCapabilities c, Media[] m) { |
| name = n; |
| capabilities = c; |
| mediaList = m; |
| |
| if (capabilities == null) { |
| adaptive = false; |
| } else { |
| Log.w(TAG, "checking capabilities of " + name + " for " + mediaList[0].getMime()); |
| adaptive = capabilities.isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback); |
| } |
| } |
| } |
| |
| class CodecList extends ArrayList<Codec> { }; |
| |
| /* all codecs of mime, plus named codec if exists */ |
| class CodecFamily extends CodecList { |
| private final static String TAG = "AdaptiveCodecFamily"; |
| private static final int NUM_FRAMES = AdaptivePlaybackTest.NUM_FRAMES; |
| |
| public CodecFamily(Context context, String mime, String explicitCodecName, int ... resources) { |
| try { |
| /* read all media */ |
| Media[] mediaList = new Media[resources.length]; |
| for (int i = 0; i < resources.length; i++) { |
| Log.v(TAG, "reading media " + resources[i]); |
| Media media = Media.read(context, resources[i], NUM_FRAMES); |
| assert media.getMime().equals(mime): |
| "test stream " + resources[i] + " has " + media.getMime() + |
| " mime type instead of " + mime; |
| |
| /* assuming the first timestamp is the smallest */ |
| long firstPTS = media.getFrame(0).presentationTimeUs; |
| long smallestPTS = media.getTimestampRangeValue(0, NUM_FRAMES, Media.RANGE_START); |
| |
| assert firstPTS == smallestPTS: |
| "first frame timestamp (" + firstPTS + ") is not smallest (" + |
| smallestPTS + ")"; |
| |
| mediaList[i] = media; |
| } |
| |
| /* enumerate codecs */ |
| MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| for (MediaCodecInfo codecInfo : mcl.getCodecInfos()) { |
| if (codecInfo.isEncoder()) { |
| continue; |
| } |
| for (String type : codecInfo.getSupportedTypes()) { |
| if (type.equals(mime)) { |
| /* mark the explicitly named codec as included */ |
| if (codecInfo.getName().equals(explicitCodecName)) { |
| explicitCodecName = null; |
| } |
| add(new Codec( |
| codecInfo.getName(), |
| codecInfo.getCapabilitiesForType(mime), |
| mediaList)); |
| break; |
| } |
| } |
| } |
| |
| /* test if the explicitly named codec is present on the system */ |
| if (explicitCodecName != null) { |
| try { |
| MediaCodec codec = MediaCodec.createByCodecName(explicitCodecName); |
| if (codec != null) { |
| codec.release(); |
| add(new Codec(explicitCodecName, null, mediaList)); |
| } |
| } catch (Exception e) {} |
| } |
| } catch (Throwable t) { |
| Log.wtf("Constructor failed", t); |
| throw new RuntimeException("constructor failed", t); |
| } |
| } |
| } |
| |
| /* named codec if exists */ |
| class CodecByName extends CodecList { |
| public CodecByName(Context context, String mime, String codecName, int ... resources) { |
| for (Codec c: new CodecFamily(context, mime, codecName, resources)) { |
| if (c.name.equals(codecName)) { |
| add(c); |
| } |
| } |
| } |
| } |
| |
| /* all codecs of mime, except named codec if exists */ |
| class CodecFamilyExcept extends CodecList { |
| public CodecFamilyExcept( |
| Context context, String mime, String exceptCodecName, int ... resources) { |
| for (Codec c: new CodecFamily(context, mime, null, resources)) { |
| if (!c.name.equals(exceptCodecName)) { |
| add(c); |
| } |
| } |
| } |
| } |
| |
| class CodecFactory { |
| protected boolean hasCodec(String codecName) { |
| MediaCodecList list = new MediaCodecList(MediaCodecList.ALL_CODECS); |
| for (MediaCodecInfo info : list.getCodecInfos()) { |
| if (codecName.equals(info.getName())) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public CodecList createCodecList( |
| Context context, String mime, String googleCodecName, int ...resources) { |
| if (!hasCodec(googleCodecName)) { |
| return null; |
| } |
| return new CodecFamily(context, mime, googleCodecName, resources); |
| } |
| } |
| |
| class SWCodecFactory extends CodecFactory { |
| public CodecList createCodecList( |
| Context context, String mime, String googleCodecName, int ...resources) { |
| if (!hasCodec(googleCodecName)) { |
| return null; |
| } |
| return new CodecByName(context, mime, googleCodecName, resources); |
| } |
| } |
| |
| class HWCodecFactory extends CodecFactory { |
| public CodecList createCodecList( |
| Context context, String mime, String googleCodecName, int ...resources) { |
| if (!hasCodec(googleCodecName)) { |
| return null; |
| } |
| return new CodecFamilyExcept(context, mime, googleCodecName, resources); |
| } |
| } |
| |
| /* ====================================================================== */ |
| /* Test Steps, Test (Case)s, and Test List */ |
| /* ====================================================================== */ |
| class StepRunner implements Runnable { |
| public StepRunner(Step s) { |
| mStep = s; |
| mThrowed = null; |
| } |
| public void run() { |
| try { |
| mStep.run(); |
| } catch (Throwable e) { |
| mThrowed = e; |
| } |
| } |
| public void throwThrowed() throws Throwable { |
| if (mThrowed != null) { |
| throw mThrowed; |
| } |
| } |
| private Throwable mThrowed; |
| private Step mStep; |
| } |
| |
| class TestList extends ArrayList<Step> { |
| private final static String TAG = "AdaptiveTestList"; |
| public void run() throws Throwable { |
| Throwable res = null; |
| for (Step step: this) { |
| try { |
| Log.i(TAG, step.getDescription()); |
| if (step.stepSurface().needsToRunInSeparateThread()) { |
| StepRunner runner = new StepRunner(step); |
| Thread th = new Thread(runner, "stepWrapper"); |
| th.start(); |
| th.join(); |
| runner.throwThrowed(); |
| } else { |
| step.run(); |
| } |
| } catch (Throwable e) { |
| Log.e(TAG, "while " + step.getDescription(), e); |
| res = e; |
| mFailedSteps++; |
| } finally { |
| mWarnings += step.getWarnings(); |
| } |
| } |
| if (res != null) { |
| throw new RuntimeException( |
| mFailedSteps + " failed steps, " + mWarnings + " warnings", |
| res); |
| } |
| } |
| public int getWarnings() { |
| return mWarnings; |
| } |
| public int getFailures() { |
| return mFailedSteps; |
| } |
| private int mFailedSteps; |
| private int mWarnings; |
| } |
| |
| abstract class Test { |
| public static final int FORMAT_ADAPTIVE_LARGEST = 1; |
| public static final int FORMAT_ADAPTIVE_FIRST = 2; |
| public static final int FORMAT_REGULAR = 3; |
| |
| protected int mFormatType; |
| protected boolean mUseSurface; |
| protected boolean mUseSurfaceTexture; |
| |
| public Test() { |
| mFormatType = FORMAT_REGULAR; |
| mUseSurface = true; |
| mUseSurfaceTexture = false; |
| } |
| |
| public Test adaptive() { |
| mFormatType = FORMAT_ADAPTIVE_LARGEST; |
| return this; |
| } |
| |
| public Test adaptiveSmall() { |
| mFormatType = FORMAT_ADAPTIVE_FIRST; |
| return this; |
| } |
| |
| public Test byteBuffer() { |
| mUseSurface = false; |
| mUseSurfaceTexture = false; |
| return this; |
| } |
| |
| public Test texture() { |
| mUseSurface = false; |
| mUseSurfaceTexture = true; |
| return this; |
| } |
| |
| public void checkAdaptiveFormat() { |
| assert mFormatType != FORMAT_REGULAR: |
| "must be used with adaptive format"; |
| } |
| |
| abstract protected TestSurface getSurface(); |
| |
| /* TRICKY: format is updated in each test run as we are actually reusing the |
| same 2 MediaFormat objects returned from MediaExtractor. Therefore, |
| format must be explicitly obtained in each test step. |
| |
| returns null if codec does not support the format. |
| */ |
| protected MediaFormat getFormat(Codec c) { |
| return getFormat(c, 0); |
| } |
| |
| protected MediaFormat getFormat(Codec c, int i) { |
| MediaFormat format = null; |
| if (mFormatType == FORMAT_REGULAR) { |
| format = c.mediaList[i].getFormat(); |
| } else if (mFormatType == FORMAT_ADAPTIVE_FIRST && c.adaptive) { |
| format = c.mediaList[i].getAdaptiveFormat( |
| c.mediaList[i].getWidth(), c.mediaList[i].getHeight()); |
| } else if (mFormatType == FORMAT_ADAPTIVE_LARGEST && c.adaptive) { |
| /* update adaptive format to max size used */ |
| format = c.mediaList[i].getAdaptiveFormat(0, 0); |
| for (Media media : c.mediaList) { |
| /* get the largest width, and the largest height independently */ |
| if (media.getWidth() > format.getInteger(MediaFormat.KEY_MAX_WIDTH)) { |
| format.setInteger(MediaFormat.KEY_MAX_WIDTH, media.getWidth()); |
| } |
| if (media.getHeight() > format.getInteger(MediaFormat.KEY_MAX_HEIGHT)) { |
| format.setInteger(MediaFormat.KEY_MAX_HEIGHT, media.getHeight()); |
| } |
| } |
| } |
| return format; |
| } |
| |
| public boolean isValid(Codec c) { return true; } |
| public abstract void addTests(TestList tests, Codec c); |
| } |
| |
| abstract class Step { |
| private static final String TAG = "AdaptiveStep"; |
| |
| public Step(String title, Test instance, Codec codec, Media media) { |
| mTest = instance; |
| mCodec = codec; |
| mMedia = media; |
| mDescription = title + " on " + stepSurface().getSurface() + " using " + |
| mCodec.name + " and " + stepFormat(); |
| } |
| public Step(String title, Test instance, Codec codec, int mediaIx) { |
| this(title, instance, codec, codec.mediaList[mediaIx]); |
| } |
| public Step(String title, Test instance, Codec codec) { |
| this(title, instance, codec, 0); |
| } |
| public Step(String description) { |
| mDescription = description; |
| } |
| public Step() { } |
| |
| public abstract void run() throws Throwable; |
| |
| private String mDescription; |
| private Test mTest; |
| private Codec mCodec; |
| private Media mMedia; |
| private int mWarnings; |
| |
| /* TRICKY: use non-standard getter names so that we don't conflict with the getters |
| in the Test classes, as most test Steps are defined as anonymous classes inside |
| the test classes. */ |
| public MediaFormat stepFormat() { |
| int ix = Arrays.asList(mCodec.mediaList).indexOf(mMedia); |
| return mTest.getFormat(mCodec, ix); |
| } |
| |
| public TestSurface stepSurface() { |
| return mTest.getSurface(); |
| } |
| |
| public Media stepMedia() { return mMedia; } |
| |
| public String getDescription() { return mDescription; } |
| public int getWarnings() { return mWarnings; } |
| |
| public void warn(String message) { |
| Log.e(TAG, "WARNING: " + message + " in " + getDescription()); |
| mWarnings++; |
| } |
| public void warn(String message, Throwable t) { |
| Log.e(TAG, "WARNING: " + message + " in " + getDescription(), t); |
| mWarnings++; |
| } |
| public void warn(Iterable<String> warnings) { |
| for (String warning: warnings) { |
| warn(warning); |
| } |
| } |
| } |
| |
| interface TestSurface { |
| public Surface getSurface(); |
| public long checksum(); |
| public void release(); |
| public void prepare(); // prepare surface prior to render |
| public void waitForDraw(); // wait for rendering to take place |
| public boolean needsToRunInSeparateThread(); |
| } |
| |
| class DecoderSurface extends OutputSurface implements TestSurface { |
| private ByteBuffer mBuf; |
| int mWidth; |
| int mHeight; |
| CRC32 mCRC; |
| |
| public DecoderSurface(int width, int height, CRC32 crc) { |
| super(width, height); |
| mWidth = width; |
| mHeight = height; |
| mCRC = crc; |
| mBuf = ByteBuffer.allocateDirect(4 * width * height); |
| } |
| |
| public void prepare() { |
| makeCurrent(); |
| } |
| |
| public void waitForDraw() { |
| awaitNewImage(); |
| drawImage(); |
| } |
| |
| public long checksum() { |
| mBuf.position(0); |
| GLES20.glReadPixels(0, 0, mWidth, mHeight, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, mBuf); |
| mBuf.position(0); |
| return AdaptivePlaybackTest.checksum(mBuf, mBuf.capacity(), mCRC); |
| } |
| |
| public void release() { |
| super.release(); |
| mBuf = null; |
| } |
| |
| public boolean needsToRunInSeparateThread() { |
| return true; |
| } |
| } |
| |
| class ActivitySurface implements TestSurface { |
| private Surface mSurface; |
| public ActivitySurface(Surface s) { |
| mSurface = s; |
| } |
| public Surface getSurface() { |
| return mSurface; |
| } |
| public void prepare() { } |
| public void waitForDraw() { } |
| public long checksum() { |
| return 0; |
| } |
| public void release() { |
| // don't release activity surface, as it is reusable |
| } |
| public boolean needsToRunInSeparateThread() { |
| return false; |
| } |
| } |
| |