blob: ff605e24d3c32f466bd7054dbbc13f8afcec19bd [file] [log] [blame]
/*
* 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 android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaPlayer;
import android.media.cts.R;
import android.media.cts.TestUtils.Monitor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.RequiresDevice;
import android.util.Log;
import android.view.Surface;
import android.webkit.cts.CtsTestServer;
import androidx.test.filters.SmallTest;
import com.android.compatibility.common.util.MediaUtils;
import org.apache.http.Header;
import org.apache.http.HttpRequest;
import org.apache.http.impl.DefaultHttpServerConnection;
import org.apache.http.impl.io.SocketOutputBuffer;
import org.apache.http.io.SessionOutputBuffer;
import org.apache.http.params.HttpParams;
import org.apache.http.util.CharArrayBuffer;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.zip.Adler32;
@SmallTest
@RequiresDevice
@AppModeFull(reason = "TODO: evaluate and port to instant")
public class NativeDecoderTest extends MediaPlayerTestBase {
private static final String TAG = "DecoderTest";
private static final int RESET_MODE_NONE = 0;
private static final int RESET_MODE_RECONFIGURE = 1;
private static final int RESET_MODE_FLUSH = 2;
private static final int RESET_MODE_EOS_FLUSH = 3;
private static final String[] CSD_KEYS = new String[] { "csd-0", "csd-1" };
private static final int CONFIG_MODE_NONE = 0;
private static final int CONFIG_MODE_QUEUE = 1;
private static Resources mResources;
short[] mMasterBuffer;
/** Load jni on initialization */
static {
Log.i("@@@", "before loadlibrary");
System.loadLibrary("ctsmediacodec_jni");
Log.i("@@@", "after loadlibrary");
}
@Override
protected void setUp() throws Exception {
super.setUp();
mResources = mContext.getResources();
}
// check that native extractor behavior matches java extractor
private void compareArrays(String message, int[] a1, int[] a2) {
if (a1 == a2) {
return;
}
assertNotNull(message + ": array 1 is null", a1);
assertNotNull(message + ": array 2 is null", a2);
assertEquals(message + ": arraylengths differ", a1.length, a2.length);
int length = a1.length;
for (int i = 0; i < length; i++)
if (a1[i] != a2[i]) {
Log.i("@@@@", Arrays.toString(a1));
Log.i("@@@@", Arrays.toString(a2));
fail(message + ": at index " + i);
}
}
public void testExtractor() throws Exception {
testExtractor(R.raw.sinesweepogg);
testExtractor(R.raw.sinesweepoggmkv);
testExtractor(R.raw.sinesweepoggmp4);
testExtractor(R.raw.sinesweepmp3lame);
testExtractor(R.raw.sinesweepmp3smpb);
testExtractor(R.raw.sinesweepopus);
testExtractor(R.raw.sinesweepopusmp4);
testExtractor(R.raw.sinesweepm4a);
testExtractor(R.raw.sinesweepflacmkv);
testExtractor(R.raw.sinesweepflac);
testExtractor(R.raw.sinesweepflacmp4);
testExtractor(R.raw.sinesweepwav);
testExtractor(R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz);
testExtractor(R.raw.bbb_s3_1280x720_webm_vp8_8mbps_60fps_opus_6ch_384kbps_48000hz);
testExtractor(R.raw.bbb_s4_1280x720_webm_vp9_0p31_4mbps_30fps_opus_stereo_128kbps_48000hz);
testExtractor(R.raw.video_1280x720_webm_av1_2000kbps_30fps_vorbis_stereo_128kbps_48000hz);
testExtractor(R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz);
testExtractor(R.raw.video_480x360_mp4_mpeg2_1500kbps_30fps_aac_stereo_128kbps_48000hz);
testExtractor(R.raw.video_480x360_mp4_mpeg4_860kbps_25fps_aac_stereo_128kbps_44100hz);
CtsTestServer foo = new CtsTestServer(mContext);
testExtractor(foo.getAssetUrl("noiseandchirps.ogg"));
testExtractor(foo.getAssetUrl("ringer.mp3"));
testExtractor(foo.getRedirectingAssetUrl("ringer.mp3"));
String[] keys = new String[] {"header0", "header1"};
String[] values = new String[] {"value0", "value1"};
testExtractor(foo.getAssetUrl("noiseandchirps.ogg"), keys, values);
HttpRequest req = foo.getLastRequest("noiseandchirps.ogg");
for (int i = 0; i < keys.length; i++) {
String key = keys[i];
String value = values[i];
Header[] header = req.getHeaders(key);
assertTrue("expecting " + key + ":" + value + ", saw " + Arrays.toString(header),
header.length == 1 && header[0].getValue().equals(value));
}
String[] emptyArray = new String[0];
testExtractor(foo.getAssetUrl("noiseandchirps.ogg"), emptyArray, emptyArray);
}
private void testExtractor(String path) throws Exception {
testExtractor(path, null, null);
}
/**
* |keys| and |values| should be arrays of the same length.
*
* If keys or values is null, test {@link MediaExtractor#setDataSource(String)}
* and NDK counter part, i.e. set data source without headers.
*
* If keys or values is zero length, test {@link MediaExtractor#setDataSource(String, Map))}
* and NDK counter part with null headers.
*
*/
private void testExtractor(String path, String[] keys, String[] values) throws Exception {
int[] jsizes = getSampleSizes(path, keys, values);
int[] nsizes = getSampleSizesNativePath(path, keys, values, /* testNativeSource = */ false);
int[] nsizes2 = getSampleSizesNativePath(path, keys, values, /* testNativeSource = */ true);
compareArrays("different samplesizes", jsizes, nsizes);
compareArrays("different samplesizes native source", jsizes, nsizes2);
}
private void testExtractor(int res) throws Exception {
AssetFileDescriptor fd = mResources.openRawResourceFd(res);
int[] jsizes = getSampleSizes(
fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
int[] nsizes = getSampleSizesNative(
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength());
fd.close();
compareArrays("different samples", jsizes, nsizes);
}
private static int[] getSampleSizes(String path, String[] keys, String[] values) throws IOException {
MediaExtractor ex = new MediaExtractor();
if (keys == null || values == null) {
ex.setDataSource(path);
} else {
Map<String, String> headers = null;
int numheaders = Math.min(keys.length, values.length);
for (int i = 0; i < numheaders; i++) {
if (headers == null) {
headers = new HashMap<>();
}
String key = keys[i];
String value = values[i];
headers.put(key, value);
}
ex.setDataSource(path, headers);
}
return getSampleSizes(ex);
}
private static int[] getSampleSizes(FileDescriptor fd, long offset, long size)
throws IOException {
MediaExtractor ex = new MediaExtractor();
ex.setDataSource(fd, offset, size);
return getSampleSizes(ex);
}
private static int[] getSampleSizes(MediaExtractor ex) {
ArrayList<Integer> foo = new ArrayList<Integer>();
ByteBuffer buf = ByteBuffer.allocate(1024*1024);
int numtracks = ex.getTrackCount();
assertTrue("no tracks", numtracks > 0);
foo.add(numtracks);
for (int i = 0; i < numtracks; i++) {
MediaFormat format = ex.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
foo.add(0);
foo.add(format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
foo.add(format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
} else if (mime.startsWith("video/")) {
foo.add(1);
foo.add(format.getInteger(MediaFormat.KEY_WIDTH));
foo.add(format.getInteger(MediaFormat.KEY_HEIGHT));
foo.add((int)format.getLong(MediaFormat.KEY_DURATION));
} else {
fail("unexpected mime type: " + mime);
}
ex.selectTrack(i);
}
while(true) {
int n = ex.readSampleData(buf, 0);
if (n < 0) {
break;
}
foo.add(n);
foo.add(ex.getSampleTrackIndex());
foo.add(ex.getSampleFlags());
foo.add((int)ex.getSampleTime()); // just the low bits should be OK
byte foobar[] = new byte[n];
buf.get(foobar, 0, n);
foo.add((int)adler32(foobar));
ex.advance();
}
int [] ret = new int[foo.size()];
for (int i = 0; i < ret.length; i++) {
ret[i] = foo.get(i);
}
return ret;
}
private static native int[] getSampleSizesNative(int fd, long offset, long size);
private static native int[] getSampleSizesNativePath(
String path, String[] keys, String[] values, boolean testNativeSource);
public void testExtractorFileDurationNative() throws Exception {
int res = R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz;
testExtractorFileDurationNative(res);
}
private void testExtractorFileDurationNative(int res) throws Exception {
AssetFileDescriptor fd = mResources.openRawResourceFd(res);
long durationUs = getExtractorFileDurationNative(
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength());
MediaExtractor ex = new MediaExtractor();
ex.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
int numtracks = ex.getTrackCount();
long aDurationUs = -1, vDurationUs = -1;
for (int i = 0; i < numtracks; i++) {
MediaFormat format = ex.getTrackFormat(i);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/")) {
aDurationUs = format.getLong(MediaFormat.KEY_DURATION);
} else if (mime.startsWith("video/")) {
vDurationUs = format.getLong(MediaFormat.KEY_DURATION);
}
}
assertTrue("duration inconsistency",
durationUs < 0 || durationUs >= aDurationUs && durationUs >= vDurationUs);
}
private static native long getExtractorFileDurationNative(int fd, long offset, long size);
public void testExtractorCachedDurationNative() throws Exception {
CtsTestServer foo = new CtsTestServer(mContext);
String url = foo.getAssetUrl("ringer.mp3");
long cachedDurationUs = getExtractorCachedDurationNative(url, /* testNativeSource = */ false);
assertTrue("cached duration negative", cachedDurationUs >= 0);
cachedDurationUs = getExtractorCachedDurationNative(url, /* testNativeSource = */ true);
assertTrue("cached duration negative native source", cachedDurationUs >= 0);
}
private static native long getExtractorCachedDurationNative(String uri, boolean testNativeSource);
public void testDecoder() throws Exception {
int testsRun =
testDecoder(R.raw.sinesweepogg) +
testDecoder(R.raw.sinesweepoggmkv) +
testDecoder(R.raw.sinesweepoggmp4) +
testDecoder(R.raw.sinesweepmp3lame) +
testDecoder(R.raw.sinesweepmp3smpb) +
testDecoder(R.raw.sinesweepopus) +
testDecoder(R.raw.sinesweepopusmp4) +
testDecoder(R.raw.sinesweepm4a) +
testDecoder(R.raw.sinesweepflacmkv) +
testDecoder(R.raw.sinesweepflac) +
testDecoder(R.raw.sinesweepflacmp4) +
testDecoder(R.raw.sinesweepwav) +
testDecoder(R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz) +
testDecoder(R.raw.bbb_s1_640x360_webm_vp8_2mbps_30fps_vorbis_5ch_320kbps_48000hz) +
testDecoder(R.raw.bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz) +
testDecoder(R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz) +
testDecoder(R.raw.video_480x360_mp4_mpeg2_1500kbps_30fps_aac_stereo_128kbps_48000hz);
testDecoder(R.raw.video_480x360_mp4_mpeg4_860kbps_25fps_aac_stereo_128kbps_44100hz);
if (testsRun == 0) {
MediaUtils.skipTest("no decoders found");
}
}
public void testDataSource() throws Exception {
int testsRun = testDecoder(R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz,
/* wrapFd */ true, /* useCallback */ false);
if (testsRun == 0) {
MediaUtils.skipTest("no decoders found");
}
}
public void testDataSourceAudioOnly() throws Exception {
int testsRun = testDecoder(
R.raw.loudsoftmp3,
/* wrapFd */ true, /* useCallback */ false) +
testDecoder(
R.raw.loudsoftaac,
/* wrapFd */ false, /* useCallback */ false);
if (testsRun == 0) {
MediaUtils.skipTest("no decoders found");
}
}
public void testDataSourceWithCallback() throws Exception {
int testsRun = testDecoder(R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz,
/* wrapFd */ true, /* useCallback */ true);
if (testsRun == 0) {
MediaUtils.skipTest("no decoders found");
}
}
private int testDecoder(int res) throws Exception {
return testDecoder(res, /* wrapFd */ false, /* useCallback */ false);
}
private int testDecoder(int res, boolean wrapFd, boolean useCallback) throws Exception {
if (!MediaUtils.hasCodecsForResource(mContext, res)) {
return 0; // skip
}
AssetFileDescriptor fd = mResources.openRawResourceFd(res);
int[] jdata1 = getDecodedData(
fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
int[] jdata2 = getDecodedData(
fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
int[] ndata1 = getDecodedDataNative(
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength(), wrapFd,
useCallback);
int[] ndata2 = getDecodedDataNative(
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength(), wrapFd,
useCallback);
fd.close();
compareArrays("inconsistent java decoder", jdata1, jdata2);
compareArrays("inconsistent native decoder", ndata1, ndata2);
compareArrays("different decoded data", jdata1, ndata1);
return 1;
}
private static int[] getDecodedData(FileDescriptor fd, long offset, long size)
throws IOException {
MediaExtractor ex = new MediaExtractor();
ex.setDataSource(fd, offset, size);
return getDecodedData(ex);
}
private static int[] getDecodedData(MediaExtractor ex) throws IOException {
int numtracks = ex.getTrackCount();
assertTrue("no tracks", numtracks > 0);
ArrayList<Integer>[] trackdata = new ArrayList[numtracks];
MediaCodec[] codec = new MediaCodec[numtracks];
MediaFormat[] format = new MediaFormat[numtracks];
ByteBuffer[][] inbuffers = new ByteBuffer[numtracks][];
ByteBuffer[][] outbuffers = new ByteBuffer[numtracks][];
for (int i = 0; i < numtracks; i++) {
format[i] = ex.getTrackFormat(i);
String mime = format[i].getString(MediaFormat.KEY_MIME);
if (mime.startsWith("audio/") || mime.startsWith("video/")) {
codec[i] = MediaCodec.createDecoderByType(mime);
codec[i].configure(format[i], null, null, 0);
codec[i].start();
inbuffers[i] = codec[i].getInputBuffers();
outbuffers[i] = codec[i].getOutputBuffers();
trackdata[i] = new ArrayList<Integer>();
} else {
fail("unexpected mime type: " + mime);
}
ex.selectTrack(i);
}
boolean[] sawInputEOS = new boolean[numtracks];
boolean[] sawOutputEOS = new boolean[numtracks];
int eosCount = 0;
BufferInfo info = new BufferInfo();
while(eosCount < numtracks) {
int t = ex.getSampleTrackIndex();
if (t >= 0) {
assertFalse("saw input EOS twice", sawInputEOS[t]);
int bufidx = codec[t].dequeueInputBuffer(5000);
if (bufidx >= 0) {
Log.i("@@@@", "track " + t + " buffer " + bufidx);
ByteBuffer buf = inbuffers[t][bufidx];
int sampleSize = ex.readSampleData(buf, 0);
Log.i("@@@@", "read " + sampleSize + " @ " + ex.getSampleTime());
if (sampleSize < 0) {
sampleSize = 0;
sawInputEOS[t] = true;
Log.i("@@@@", "EOS");
//break;
}
long presentationTimeUs = ex.getSampleTime();
codec[t].queueInputBuffer(bufidx, 0, sampleSize, presentationTimeUs,
sawInputEOS[t] ? MediaCodec.BUFFER_FLAG_END_OF_STREAM : 0);
ex.advance();
}
} else {
Log.i("@@@@", "no more input samples");
for (int tt = 0; tt < codec.length; tt++) {
if (!sawInputEOS[tt]) {
// we ran out of samples without ever signaling EOS to the codec,
// so do that now
int bufidx = codec[tt].dequeueInputBuffer(5000);
if (bufidx >= 0) {
codec[tt].queueInputBuffer(bufidx, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
sawInputEOS[tt] = true;
}
}
}
}
// see if any of the codecs have data available
for (int tt = 0; tt < codec.length; tt++) {
if (!sawOutputEOS[tt]) {
int status = codec[tt].dequeueOutputBuffer(info, 1);
if (status >= 0) {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.i("@@@@", "EOS on track " + tt);
sawOutputEOS[tt] = true;
eosCount++;
}
Log.i("@@@@", "got decoded buffer for track " + tt + ", size " + info.size);
if (info.size > 0) {
addSampleData(trackdata[tt], outbuffers[tt][status], info.size, format[tt]);
}
codec[tt].releaseOutputBuffer(status, false);
} else if (status == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
Log.i("@@@@", "output buffers changed for track " + tt);
outbuffers[tt] = codec[tt].getOutputBuffers();
} else if (status == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
format[tt] = codec[tt].getOutputFormat();
Log.i("@@@@", "format changed for track " + t + ": " + format[tt].toString());
} else if (status == MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.i("@@@@", "no buffer right now for track " + tt);
} else {
Log.i("@@@@", "unexpected info code for track " + tt + ": " + status);
}
} else {
Log.i("@@@@", "already at EOS on track " + tt);
}
}
}
int totalsize = 0;
for (int i = 0; i < numtracks; i++) {
totalsize += trackdata[i].size();
}
int[] trackbytes = new int[totalsize];
int idx = 0;
for (int i = 0; i < numtracks; i++) {
ArrayList<Integer> src = trackdata[i];
int tracksize = src.size();
for (int j = 0; j < tracksize; j++) {
trackbytes[idx++] = src.get(j);
}
}
for (int i = 0; i < codec.length; i++) {
codec[i].release();
}
return trackbytes;
}
static void addSampleData(ArrayList<Integer> dst,
ByteBuffer buf, int size, MediaFormat format) throws IOException{
Log.i("@@@", "addsample " + dst.size() + "/" + size);
int width = format.getInteger(MediaFormat.KEY_WIDTH, size);
int stride = format.getInteger(MediaFormat.KEY_STRIDE, width);
int height = format.getInteger(MediaFormat.KEY_HEIGHT, 1);
byte[] bb = new byte[width * height];
int offset = buf.position();
for (int i = 0; i < height; i++) {
buf.position(i * stride + offset);
buf.get(bb, i * width, width);
}
// bb is filled with data
long sum = adler32(bb);
dst.add( (int) (sum & 0xffffffff));
}
private final static Adler32 checksummer = new Adler32();
// simple checksum computed over every decoded buffer
static int adler32(byte[] input) {
checksummer.reset();
checksummer.update(input);
int ret = (int) checksummer.getValue();
Log.i("@@@", "adler " + input.length + "/" + ret);
return ret;
}
private static native int[] getDecodedDataNative(int fd, long offset, long size, boolean wrapFd,
boolean useCallback)
throws IOException;
public void testVideoPlayback() throws Exception {
int testsRun =
testVideoPlayback(
R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz) +
testVideoPlayback(
R.raw.bbb_s1_640x360_webm_vp8_2mbps_30fps_vorbis_5ch_320kbps_48000hz) +
testVideoPlayback(
R.raw.bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz) +
testVideoPlayback(
R.raw.video_640x360_webm_av1_470kbps_30fps_vorbis_stereo_128kbps_48000hz) +
testVideoPlayback(
R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_mono_24kbps_11025hz) +
testVideoPlayback(
R.raw.video_176x144_mp4_mpeg2_105kbps_25fps_aac_stereo_128kbps_44100hz) +
testVideoPlayback(
R.raw.video_480x360_mp4_mpeg4_860kbps_25fps_aac_stereo_128kbps_44100hz);
if (testsRun == 0) {
MediaUtils.skipTest("no decoders found");
}
}
private int testVideoPlayback(int res) throws Exception {
if (!MediaUtils.checkCodecsForResource(mContext, res)) {
return 0; // skip
}
AssetFileDescriptor fd = mResources.openRawResourceFd(res);
boolean ret = testPlaybackNative(mActivity.getSurfaceHolder().getSurface(),
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength());
assertTrue("native playback error", ret);
return 1;
}
private static native boolean testPlaybackNative(Surface surface,
int fd, long startOffset, long length);
public void testMuxerAvc() throws Exception {
// IMPORTANT: this file must not have B-frames
testMuxer(R.raw.video_1280x720_mp4_h264_1000kbps_25fps_aac_stereo_128kbps_44100hz, false);
}
public void testMuxerH263() throws Exception {
// IMPORTANT: this file must not have B-frames
testMuxer(R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz, false);
}
public void testMuxerHevc() throws Exception {
// IMPORTANT: this file must not have B-frames
testMuxer(R.raw.video_640x360_mp4_hevc_450kbps_no_b, false);
}
public void testMuxerVp8() throws Exception {
testMuxer(R.raw.bbb_s1_640x360_webm_vp8_2mbps_30fps_vorbis_5ch_320kbps_48000hz, true);
}
public void testMuxerVp9() throws Exception {
testMuxer(
R.raw.video_1280x720_webm_vp9_csd_309kbps_25fps_vorbis_stereo_128kbps_48000hz,
true);
}
public void testMuxerVp9NoCsd() throws Exception {
testMuxer(
R.raw.bbb_s1_640x360_webm_vp9_0p21_1600kbps_30fps_vorbis_stereo_128kbps_48000hz,
true);
}
public void testMuxerVp9Hdr() throws Exception {
testMuxer(R.raw.video_256x144_webm_vp9_hdr_83kbps_24fps, true);
}
// We do not support MPEG-2 muxing as of yet
public void SKIP_testMuxerMpeg2() throws Exception {
// IMPORTANT: this file must not have B-frames
testMuxer(R.raw.video_176x144_mp4_mpeg2_105kbps_25fps_aac_stereo_128kbps_44100hz, false);
}
public void testMuxerMpeg4() throws Exception {
// IMPORTANT: this file must not have B-frames
testMuxer(R.raw.video_176x144_mp4_mpeg4_300kbps_25fps_aac_stereo_128kbps_44100hz, false);
}
private void testMuxer(int res, boolean webm) throws Exception {
if (!MediaUtils.checkCodecsForResource(mContext, res)) {
return; // skip
}
AssetFileDescriptor infd = mResources.openRawResourceFd(res);
File base = mContext.getExternalFilesDir(null);
String tmpFile = base.getPath() + "/tmp.dat";
Log.i("@@@", "using tmp file " + tmpFile);
new File(tmpFile).delete();
ParcelFileDescriptor out = ParcelFileDescriptor.open(new File(tmpFile),
ParcelFileDescriptor.MODE_READ_WRITE | ParcelFileDescriptor.MODE_CREATE);
assertTrue("muxer failed", testMuxerNative(
infd.getParcelFileDescriptor().getFd(), infd.getStartOffset(), infd.getLength(),
out.getFd(), webm));
// compare the original with the remuxed
MediaExtractor org = new MediaExtractor();
org.setDataSource(infd.getFileDescriptor(),
infd.getStartOffset(), infd.getLength());
MediaExtractor remux = new MediaExtractor();
remux.setDataSource(out.getFileDescriptor());
assertEquals("mismatched numer of tracks", org.getTrackCount(), remux.getTrackCount());
// allow duration mismatch for webm files as ffmpeg does not consider the duration of the
// last frame while libwebm (and our framework) does.
final long maxDurationDiffUs = webm ? 50000 : 0; // 50ms for webm
for (int i = 0; i < org.getTrackCount(); i++) {
MediaFormat format1 = org.getTrackFormat(i);
MediaFormat format2 = remux.getTrackFormat(i);
Log.i("@@@", "org: " + format1);
Log.i("@@@", "remux: " + format2);
assertTrue("different formats", compareFormats(format1, format2, maxDurationDiffUs));
}
org.release();
remux.release();
MediaPlayer player1 = MediaPlayer.create(mContext, res);
MediaPlayer player2 = MediaPlayer.create(mContext, Uri.parse("file://" + tmpFile));
assertEquals("duration is different",
player1.getDuration(), player2.getDuration(), maxDurationDiffUs * 0.001);
player1.release();
player2.release();
new File(tmpFile).delete();
}
private String hexString(ByteBuffer buf) {
if (buf == null) {
return "(null)";
}
final char digits[] =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
StringBuilder hex = new StringBuilder();
for (int i = buf.position(); i < buf.limit(); ++i) {
byte c = buf.get(i);
hex.append(digits[(c >> 4) & 0xf]);
hex.append(digits[c & 0xf]);
}
return hex.toString();
}
/** returns: null if key is in neither formats, true if they match and false otherwise */
private Boolean compareByteBufferInFormats(MediaFormat f1, MediaFormat f2, String key) {
ByteBuffer bufF1 = f1.containsKey(key) ? f1.getByteBuffer(key) : null;
ByteBuffer bufF2 = f2.containsKey(key) ? f2.getByteBuffer(key) : null;
if (bufF1 == null && bufF2 == null) {
return null;
}
if (bufF1 == null || !bufF1.equals(bufF2)) {
Log.i("@@@", "org " + key + ": " + hexString(bufF1));
Log.i("@@@", "rmx " + key + ": " + hexString(bufF2));
return false;
}
return true;
}
private boolean compareFormats(MediaFormat f1, MediaFormat f2, long maxDurationDiffUs) {
final String KEY_DURATION = MediaFormat.KEY_DURATION;
// allow some difference in durations
if (maxDurationDiffUs > 0
&& f1.containsKey(KEY_DURATION) && f2.containsKey(KEY_DURATION)
&& Math.abs(f1.getLong(KEY_DURATION)
- f2.getLong(KEY_DURATION)) <= maxDurationDiffUs) {
f2.setLong(KEY_DURATION, f1.getLong(KEY_DURATION));
}
// verify hdr-static-info
if (Boolean.FALSE.equals(compareByteBufferInFormats(f1, f2, "hdr-static-info"))) {
return false;
}
// verify CSDs
for (int i = 0;; ++i) {
String key = "csd-" + i;
Boolean match = compareByteBufferInFormats(f1, f2, key);
if (match == null) {
break;
} else if (match == false) {
return false;
}
}
// there's no good way to compare two MediaFormats, so compare their string
// representation
return f1.toString().equals(f2.toString());
}
private static native boolean testMuxerNative(int in, long inoffset, long insize,
int out, boolean webm);
public void testFormat() throws Exception {
assertTrue("media format fail, see log for details", testFormatNative());
}
private static native boolean testFormatNative();
public void testPssh() throws Exception {
testPssh(R.raw.psshtest);
}
private void testPssh(int res) throws Exception {
AssetFileDescriptor fd = mResources.openRawResourceFd(res);
MediaExtractor ex = new MediaExtractor();
ex.setDataSource(fd.getParcelFileDescriptor().getFileDescriptor(),
fd.getStartOffset(), fd.getLength());
testPssh(ex);
ex.release();
boolean ret = testPsshNative(
fd.getParcelFileDescriptor().getFd(), fd.getStartOffset(), fd.getLength());
assertTrue("native pssh error", ret);
}
private static void testPssh(MediaExtractor ex) {
Map<UUID, byte[]> map = ex.getPsshInfo();
Set<UUID> keys = map.keySet();
for (UUID uuid: keys) {
Log.i("@@@", "uuid: " + uuid + ", data size " +
map.get(uuid).length);
}
}
private static native boolean testPsshNative(int fd, long offset, long size);
public void testCryptoInfo() throws Exception {
assertTrue("native cryptoinfo failed, see log for details", testCryptoInfoNative());
}
private static native boolean testCryptoInfoNative();
public void testMediaFormat() throws Exception {
assertTrue("native mediaformat failed, see log for details", testMediaFormatNative());
}
private static native boolean testMediaFormatNative();
public void testAMediaDataSourceClose() throws Throwable {
final CtsTestServer slowServer = new SlowCtsTestServer();
final String url = slowServer.getAssetUrl("noiseandchirps.ogg");
final long ds = createAMediaDataSource(url);
final long ex = createAMediaExtractor();
try {
setAMediaExtractorDataSourceAndFailIfAnr(ex, ds);
} finally {
slowServer.shutdown();
deleteAMediaExtractor(ex);
deleteAMediaDataSource(ds);
}
}
private void setAMediaExtractorDataSourceAndFailIfAnr(final long ex, final long ds)
throws Throwable {
final Monitor setAMediaExtractorDataSourceDone = new Monitor();
final int HEAD_START_MILLIS = 1000;
final int ANR_TIMEOUT_MILLIS = 2500;
final int JOIN_TIMEOUT_MILLIS = 1500;
Thread setAMediaExtractorDataSourceThread = new Thread() {
public void run() {
setAMediaExtractorDataSource(ex, ds);
setAMediaExtractorDataSourceDone.signal();
}
};
try {
setAMediaExtractorDataSourceThread.start();
Thread.sleep(HEAD_START_MILLIS);
closeAMediaDataSource(ds);
boolean closed = setAMediaExtractorDataSourceDone.waitForSignal(ANR_TIMEOUT_MILLIS);
assertTrue("close took longer than " + ANR_TIMEOUT_MILLIS, closed);
} finally {
setAMediaExtractorDataSourceThread.join(JOIN_TIMEOUT_MILLIS);
}
}
private class SlowCtsTestServer extends CtsTestServer {
private static final int SERVER_DELAY_MILLIS = 5000;
private final CountDownLatch mDisconnected = new CountDownLatch(1);
SlowCtsTestServer() throws Exception {
super(mContext);
}
@Override
protected DefaultHttpServerConnection createHttpServerConnection() {
return new SlowHttpServerConnection(mDisconnected, SERVER_DELAY_MILLIS);
}
@Override
public void shutdown() {
mDisconnected.countDown();
super.shutdown();
}
}
private static class SlowHttpServerConnection extends DefaultHttpServerConnection {
private final CountDownLatch mDisconnected;
private final int mDelayMillis;
public SlowHttpServerConnection(CountDownLatch disconnected, int delayMillis) {
mDisconnected = disconnected;
mDelayMillis = delayMillis;
}
@Override
protected SessionOutputBuffer createHttpDataTransmitter(
Socket socket, int buffersize, HttpParams params) throws IOException {
return createSessionOutputBuffer(socket, buffersize, params);
}
SessionOutputBuffer createSessionOutputBuffer(
Socket socket, int buffersize, HttpParams params) throws IOException {
return new SocketOutputBuffer(socket, buffersize, params) {
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
while (len-- > 0) {
write(b[off++]);
}
}
@Override
public void writeLine(String s) throws IOException {
delay();
super.writeLine(s);
}
@Override
public void writeLine(CharArrayBuffer buffer) throws IOException {
delay();
super.writeLine(buffer);
}
@Override
public void write(int b) throws IOException {
delay();
super.write(b);
}
private void delay() throws IOException {
try {
mDisconnected.await(mDelayMillis, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Ignored
}
}
};
}
}
private static native long createAMediaExtractor();
private static native long createAMediaDataSource(String url);
private static native int setAMediaExtractorDataSource(long ex, long ds);
private static native void closeAMediaDataSource(long ds);
private static native void deleteAMediaExtractor(long ex);
private static native void deleteAMediaDataSource(long ds);
}