blob: c873691a7b61457d7afbfd7a5cde479f2d617e5f [file] [log] [blame]
/*
* Copyright (C) 2013 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.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.media.MediaMuxer;
import android.os.ParcelFileDescriptor;
import android.platform.test.annotations.AppModeFull;
import android.test.AndroidTestCase;
import android.util.Log;
import android.media.cts.R;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Vector;
import java.util.stream.IntStream;
@AppModeFull(reason = "No interaction with system server")
public class MediaMuxerTest extends AndroidTestCase {
private static final String TAG = "MediaMuxerTest";
private static final boolean VERBOSE = false;
private static final int MAX_SAMPLE_SIZE = 256 * 1024;
private static final float LATITUDE = 0.0000f;
private static final float LONGITUDE = -180.0f;
private static final float BAD_LATITUDE = 91.0f;
private static final float BAD_LONGITUDE = -181.0f;
private static final float TOLERANCE = 0.0002f;
private static final long OFFSET_TIME_US = 29 * 60 * 1000000L; // 29 minutes
private Resources mResources;
@Override
public void setContext(Context context) {
super.setContext(context);
mResources = context.getResources();
}
/**
* Test: make sure the muxer handles both video and audio tracks correctly.
*/
public void testVideoAudio() throws Exception {
int source = R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz;
String outputFile = File.createTempFile("MediaMuxerTest_testAudioVideo", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
public void testDualVideoTrack() throws Exception {
int source = R.raw.video_176x144_h264_408kbps_30fps_352x288_h264_122kbps_30fps;
String outputFile = File.createTempFile("MediaMuxerTest_testDualVideo", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
public void testDualAudioTrack() throws Exception {
int source = R.raw.audio_aac_mono_70kbs_44100hz_aac_mono_70kbs_44100hz;
String outputFile = File.createTempFile("MediaMuxerTest_testDualAudio", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
public void testDualVideoAndAudioTrack() throws Exception {
int source = R.raw.video_h264_30fps_video_h264_30fps_aac_44100hz_aac_44100hz;
String outputFile = File.createTempFile("MediaMuxerTest_testDualVideoAudio", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 4, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
/**
* Test: make sure the muxer handles video, audio and non standard compliant metadata tracks
* that generated before API29 correctly. This test will use extractor to extract the video
* track, audio and the non standard compliant metadata track from the source file, then
* remuxes them into a new file with standard compliant metadata track. Finally, it will check
* to make sure the new file's metadata track matches the source file's metadata track for the
* mime format and data payload.
*/
public void testVideoAudioMedatadataWithNonCompliantMetadataTrack() throws Exception {
int source =
R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_non_compliant;
String outputFile = File.createTempFile("MediaMuxerTest_testAudioVideoMetadata", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 3, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
/**
* Test: make sure the muxer handles video, audio and standard compliant metadata tracks that
* generated from API29 correctly. This test will use extractor to extract the video track,
* audio and the standard compliant metadata track from the source file, then remuxes them
* into a new file with standard compliant metadata track. Finally, it will check to make sure
* the new file's metadata track matches the source file's metadata track for the mime format
* and data payload.
*/
public void testVideoAudioMedatadataWithCompliantMetadataTrack() throws Exception {
int source =
R.raw.video_176x144_3gp_h263_300kbps_25fps_aac_stereo_128kbps_11025hz_metadata_gyro_compliant;
String outputFile = File.createTempFile("MediaMuxerTest_testAudioVideoMetadata", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 3, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
/**
* Test: make sure the muxer handles audio track only file correctly.
*/
public void testAudioOnly() throws Exception {
int source = R.raw.sinesweepm4a;
String outputFile = File.createTempFile("MediaMuxerTest_testAudioOnly", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 1, -1, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
/**
* Test: make sure the muxer handles video track only file correctly.
*/
public void testVideoOnly() throws Exception {
int source = R.raw.video_only_176x144_3gp_h263_25fps;
String outputFile = File.createTempFile("MediaMuxerTest_videoOnly", ".mp4")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 1, 180, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
}
public void testWebmOutput() throws Exception {
int source = R.raw.video_480x360_webm_vp9_333kbps_25fps_vorbis_stereo_128kbps_48000hz;
String outputFile = File.createTempFile("testWebmOutput", ".webm")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM);
}
public void testThreegppOutput() throws Exception {
int source = R.raw.video_176x144_3gp_h263_300kbps_12fps_aac_stereo_128kbps_22050hz;
String outputFile = File.createTempFile("testThreegppOutput", ".3gp")
.getAbsolutePath();
cloneAndVerify(source, outputFile, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP);
}
/**
* Tests: make sure the muxer handles exceptions correctly.
* <br> Throws exception b/c start() is not called.
* <br> Throws exception b/c 2 video tracks were added.
* <br> Throws exception b/c 2 audio tracks were added.
* <br> Throws exception b/c 3 tracks were added.
* <br> Throws exception b/c no tracks was added.
* <br> Throws exception b/c a wrong format.
*/
public void testIllegalStateExceptions() throws IOException {
String outputFile = File.createTempFile("MediaMuxerTest_testISEs", ".mp4")
.getAbsolutePath();
MediaMuxer muxer;
// Throws exception b/c start() is not called.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
try {
muxer.stop();
fail("should throw IllegalStateException.");
} catch (IllegalStateException e) {
// expected
} finally {
muxer.release();
}
// Should not throw exception when 2 video tracks were added.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
try {
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
} catch (IllegalStateException e) {
fail("should not throw IllegalStateException.");
} finally {
muxer.release();
}
// Should not throw exception when 2 audio tracks were added.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
muxer.addTrack(MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 48000, 1));
try {
muxer.addTrack(MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 48000, 1));
} catch (IllegalStateException e) {
fail("should not throw IllegalStateException.");
} finally {
muxer.release();
}
// Should not throw exception when 3 tracks were added.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
muxer.addTrack(MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 48000, 1));
try {
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
} catch (IllegalStateException e) {
fail("should not throw IllegalStateException.");
} finally {
muxer.release();
}
// Throws exception b/c no tracks was added.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
try {
muxer.start();
fail("should throw IllegalStateException.");
} catch (IllegalStateException e) {
// expected
} finally {
muxer.release();
}
// Throws exception b/c a wrong format.
muxer = new MediaMuxer(outputFile, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
try {
muxer.addTrack(MediaFormat.createVideoFormat("vidoe/mp4", 480, 320));
fail("should throw IllegalStateException.");
} catch (IllegalStateException e) {
// expected
} finally {
muxer.release();
}
// Test FileDescriptor Constructor expect sucess.
RandomAccessFile file = null;
try {
file = new RandomAccessFile(outputFile, "rws");
muxer = new MediaMuxer(file.getFD(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
muxer.addTrack(MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 480, 320));
} finally {
file.close();
muxer.release();
}
// Test FileDescriptor Constructor expect exception with read only mode.
RandomAccessFile file2 = null;
try {
file2 = new RandomAccessFile(outputFile, "r");
muxer = new MediaMuxer(file2.getFD(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
fail("should throw IOException.");
} catch (IOException e) {
// expected
} finally {
file2.close();
// No need to release the muxer.
}
// Test FileDescriptor Constructor expect NO exception with write only mode.
ParcelFileDescriptor out = null;
try {
out = ParcelFileDescriptor.open(new File(outputFile),
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_CREATE);
muxer = new MediaMuxer(out.getFileDescriptor(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IllegalArgumentException e) {
fail("should not throw IllegalArgumentException.");
} catch (IOException e) {
fail("should not throw IOException.");
} finally {
out.close();
muxer.release();
}
new File(outputFile).delete();
}
/**
* Test: makes sure if audio and video muxing using MPEG4Writer works well when there are frame
* drops as in b/63590381 and b/64949961 while B Frames encoding is enabled.
*/
public void testSimulateAudioBVideoFramesDropIssues() throws Exception {
int sourceId = R.raw.video_h264_main_b_frames;
String outputFile = File.createTempFile(
"MediaMuxerTest_testSimulateAudioBVideoFramesDropIssues", ".mp4").getAbsolutePath();
try {
simulateVideoFramesDropIssuesAndMux(sourceId, outputFile, 2 /* track index */,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
verifyAFewSamplesTimestamp(sourceId, outputFile);
verifySamplesMatch(sourceId, outputFile, 66667 /* sample around 0 sec */, 0);
verifySamplesMatch(
sourceId, outputFile, 8033333 /* sample around 8 sec */, OFFSET_TIME_US);
} finally {
new File(outputFile).delete();
}
}
/**
* Test: makes sure if video only muxing using MPEG4Writer works well when there are B Frames.
*/
public void testAllTimestampsBVideoOnly() throws Exception {
int sourceId = R.raw.video_480x360_mp4_h264_bframes_495kbps_30fps_editlist;
String outputFilePath = File.createTempFile("MediaMuxerTest_testAllTimestampsBVideoOnly",
".mp4").getAbsolutePath();
try {
// No samples to drop in this case.
// No start offsets for any track.
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, null, null);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, null, null);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure muxing works well when video with B Frames are muxed using MPEG4Writer
* and a few frames drop.
*/
public void testTimestampsBVideoOnlyFramesDropOnce() throws Exception {
int sourceId = R.raw.video_480x360_mp4_h264_bframes_495kbps_30fps_editlist;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsBVideoOnlyFramesDropOnce", ".mp4").getAbsolutePath();
try {
HashSet<Integer> samplesDropSet = new HashSet<Integer>();
// Drop frames from sample index 56 to 76, I frame at 56.
IntStream.rangeClosed(56, 76).forEach(samplesDropSet::add);
// No start offsets for any track.
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, samplesDropSet, null);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure if video muxing while framedrops occurs twice using MPEG4Writer
* works with B Frames.
*/
public void testTimestampsBVideoOnlyFramesDropTwice() throws Exception {
int sourceId = R.raw.video_480x360_mp4_h264_bframes_495kbps_30fps_editlist;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsBVideoOnlyFramesDropTwice", ".mp4").getAbsolutePath();
try {
HashSet<Integer> samplesDropSet = new HashSet<Integer>();
// Drop frames with sample index 57 to 67, P frame at 57.
IntStream.rangeClosed(57, 67).forEach(samplesDropSet::add);
// Drop frames with sample index 173 to 200, B frame at 173.
IntStream.rangeClosed(173, 200).forEach(samplesDropSet::add);
// No start offsets for any track.
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, samplesDropSet, null);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure if audio/video muxing while framedrops once using MPEG4Writer
* works with B Frames.
*/
public void testTimestampsAudioBVideoFramesDropOnce() throws Exception {
int sourceId = R.raw.video_h264_main_b_frames;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsAudioBVideoFramesDropOnce", ".mp4").getAbsolutePath();
try {
HashSet<Integer> samplesDropSet = new HashSet<Integer>();
// Drop frames from sample index 56 to 76, I frame at 56.
IntStream.rangeClosed(56, 76).forEach(samplesDropSet::add);
// No start offsets for any track.
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, samplesDropSet, null);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure if audio/video muxing while framedrops twice using MPEG4Writer
* works with B Frames.
*/
public void testTimestampsAudioBVideoFramesDropTwice() throws Exception {
int sourceId = R.raw.video_h264_main_b_frames;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsAudioBVideoFramesDropTwice", ".mp4").getAbsolutePath();
try {
HashSet<Integer> samplesDropSet = new HashSet<Integer>();
// Drop frames with sample index 57 to 67, P frame at 57.
IntStream.rangeClosed(57, 67).forEach(samplesDropSet::add);
// Drop frames with sample index 173 to 200, B frame at 173.
IntStream.rangeClosed(173, 200).forEach(samplesDropSet::add);
// No start offsets for any track.
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, samplesDropSet, null);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames
* when video frames start later than audio.
*/
public void testTimestampsAudioBVideoStartOffsetVideo() throws Exception {
int sourceId = R.raw.video_h264_main_b_frames;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsAudioBVideoStartOffsetVideo", ".mp4").getAbsolutePath();
try {
Vector<Integer> startOffsetUsVect = new Vector<Integer>();
// Video starts at 400000us.
startOffsetUsVect.add(400000);
// Audio starts at 0us.
startOffsetUsVect.add(0);
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, null, startOffsetUsVect);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, null, startOffsetUsVect);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames when audio
* samples start later than video.
*/
public void testTimestampsAudioBVideoStartOffsetAudio() throws Exception {
int sourceId = R.raw.video_h264_main_b_frames;
String outputFilePath = File.createTempFile(
"MediaMuxerTest_testTimestampsAudioBVideoStartOffsetAudio", ".mp4").getAbsolutePath();
try {
Vector<Integer> startOffsetUsVect = new Vector<Integer>();
// Video starts at 0us.
startOffsetUsVect.add(0);
// Audio starts at 400000us.
startOffsetUsVect.add(400000);
cloneMediaWithSamplesDropAndStartOffsets(sourceId, outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, null, startOffsetUsVect);
verifyTimestampsWithSamplesDropSet(sourceId, outputFilePath, null, startOffsetUsVect);
} finally {
new File(outputFilePath).delete();
}
}
/**
* Clones a media file and then compares against the source file to make
* sure they match.
*/
private void cloneAndVerify(int srcMedia, String outputMediaFile,
int expectedTrackCount, int degrees, int fmt) throws IOException {
try {
cloneMediaUsingMuxer(srcMedia, outputMediaFile, expectedTrackCount,
degrees, fmt);
if (fmt == MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 ||
fmt == MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) {
verifyAttributesMatch(srcMedia, outputMediaFile, degrees);
verifyLocationInFile(outputMediaFile);
}
// Verify timestamp of all samples.
verifyTimestampsWithSamplesDropSet(srcMedia, outputMediaFile, null, null);
} finally {
new File(outputMediaFile).delete();
}
}
/**
* Using the MediaMuxer to clone a media file.
*/
private void cloneMediaUsingMuxer(int srcMedia, String dstMediaPath,
int expectedTrackCount, int degrees, int fmt)
throws IOException {
// Set up MediaExtractor to read from the source.
AssetFileDescriptor srcFd = mResources.openRawResourceFd(srcMedia);
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(srcFd.getFileDescriptor(), srcFd.getStartOffset(),
srcFd.getLength());
int trackCount = extractor.getTrackCount();
assertEquals("wrong number of tracks", expectedTrackCount, trackCount);
// Set up MediaMuxer for the destination.
MediaMuxer muxer;
muxer = new MediaMuxer(dstMediaPath, fmt);
// Set up the tracks.
HashMap<Integer, Integer> indexMap = new HashMap<Integer, Integer>(trackCount);
for (int i = 0; i < trackCount; i++) {
extractor.selectTrack(i);
MediaFormat format = extractor.getTrackFormat(i);
int dstIndex = muxer.addTrack(format);
indexMap.put(i, dstIndex);
}
// Copy the samples from MediaExtractor to MediaMuxer.
boolean sawEOS = false;
int bufferSize = MAX_SAMPLE_SIZE;
int frameCount = 0;
int offset = 100;
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
BufferInfo bufferInfo = new BufferInfo();
if (degrees >= 0) {
muxer.setOrientationHint(degrees);
}
if (fmt == MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4 ||
fmt == MediaMuxer.OutputFormat.MUXER_OUTPUT_3GPP) {
// Test setLocation out of bound cases
try {
muxer.setLocation(BAD_LATITUDE, LONGITUDE);
fail("setLocation succeeded with bad argument: [" + BAD_LATITUDE + "," + LONGITUDE
+ "]");
} catch (IllegalArgumentException e) {
// Expected
}
try {
muxer.setLocation(LATITUDE, BAD_LONGITUDE);
fail("setLocation succeeded with bad argument: [" + LATITUDE + "," + BAD_LONGITUDE
+ "]");
} catch (IllegalArgumentException e) {
// Expected
}
muxer.setLocation(LATITUDE, LONGITUDE);
}
muxer.start();
while (!sawEOS) {
bufferInfo.offset = offset;
bufferInfo.size = extractor.readSampleData(dstBuf, offset);
if (bufferInfo.size < 0) {
if (VERBOSE) {
Log.d(TAG, "saw input EOS.");
}
sawEOS = true;
bufferInfo.size = 0;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
int trackIndex = extractor.getSampleTrackIndex();
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf,
bufferInfo);
extractor.advance();
frameCount++;
if (VERBOSE) {
Log.d(TAG, "Frame (" + frameCount + ") " +
"PresentationTimeUs:" + bufferInfo.presentationTimeUs +
" Flags:" + bufferInfo.flags +
" TrackIndex:" + trackIndex +
" Size(KB) " + bufferInfo.size / 1024);
}
}
}
muxer.stop();
muxer.release();
extractor.release();
srcFd.close();
return;
}
/**
* Compares some attributes using MediaMetadataRetriever to make sure the
* cloned media file matches the source file.
*/
private void verifyAttributesMatch(int srcMedia, String testMediaPath,
int degrees) throws IOException {
AssetFileDescriptor testFd = mResources.openRawResourceFd(srcMedia);
MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever();
retrieverSrc.setDataSource(testFd.getFileDescriptor(),
testFd.getStartOffset(), testFd.getLength());
MediaMetadataRetriever retrieverTest = new MediaMetadataRetriever();
retrieverTest.setDataSource(testMediaPath);
String testDegrees = retrieverTest.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
if (testDegrees != null) {
assertEquals("Different degrees", degrees,
Integer.parseInt(testDegrees));
}
String heightSrc = retrieverSrc.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
String heightTest = retrieverTest.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
assertEquals("Different height", heightSrc,
heightTest);
String widthSrc = retrieverSrc.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
String widthTest = retrieverTest.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
assertEquals("Different width", widthSrc,
widthTest);
String durationSrc = retrieverSrc.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION);
String durationTest = retrieverTest.extractMetadata(
MediaMetadataRetriever.METADATA_KEY_DURATION);
assertEquals("Different duration", durationSrc,
durationTest);
retrieverSrc.release();
retrieverTest.release();
testFd.close();
}
private void verifyLocationInFile(String fileName) {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
retriever.setDataSource(fileName);
String location = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
assertNotNull("No location information found in file " + fileName, location);
// parsing String location and recover the location information in floats
// Make sure the tolerance is very small - due to rounding errors.
// Trim the trailing slash, if any.
int lastIndex = location.lastIndexOf('/');
if (lastIndex != -1) {
location = location.substring(0, lastIndex);
}
// Get the position of the -/+ sign in location String, which indicates
// the beginning of the longitude.
int minusIndex = location.lastIndexOf('-');
int plusIndex = location.lastIndexOf('+');
assertTrue("+ or - is not found or found only at the beginning [" + location + "]",
(minusIndex > 0 || plusIndex > 0));
int index = Math.max(minusIndex, plusIndex);
float latitude = Float.parseFloat(location.substring(0, index));
float longitude = Float.parseFloat(location.substring(index));
assertTrue("Incorrect latitude: " + latitude + " [" + location + "]",
Math.abs(latitude - LATITUDE) <= TOLERANCE);
assertTrue("Incorrect longitude: " + longitude + " [" + location + "]",
Math.abs(longitude - LONGITUDE) <= TOLERANCE);
retriever.release();
}
/**
* Uses 2 MediaExtractor, seeking to the same position, reads the sample and
* makes sure the samples match.
*/
private void verifySamplesMatch(int srcMedia, String testMediaPath, int seekToUs,
long offsetTimeUs) throws IOException {
AssetFileDescriptor testFd = mResources.openRawResourceFd(srcMedia);
MediaExtractor extractorSrc = new MediaExtractor();
extractorSrc.setDataSource(testFd.getFileDescriptor(),
testFd.getStartOffset(), testFd.getLength());
int trackCount = extractorSrc.getTrackCount();
final int videoTrackIndex = 0;
MediaExtractor extractorTest = new MediaExtractor();
extractorTest.setDataSource(testMediaPath);
assertEquals("wrong number of tracks", trackCount,
extractorTest.getTrackCount());
// Make sure the format is the same and select them
for (int i = 0; i < trackCount; i++) {
MediaFormat formatSrc = extractorSrc.getTrackFormat(i);
MediaFormat formatTest = extractorTest.getTrackFormat(i);
String mimeIn = formatSrc.getString(MediaFormat.KEY_MIME);
String mimeOut = formatTest.getString(MediaFormat.KEY_MIME);
if (!(mimeIn.equals(mimeOut))) {
fail("format didn't match on track No." + i +
formatSrc.toString() + "\n" + formatTest.toString());
}
extractorSrc.selectTrack(videoTrackIndex);
extractorTest.selectTrack(videoTrackIndex);
// Pick a time and try to compare the frame.
extractorSrc.seekTo(seekToUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
extractorTest.seekTo(seekToUs + offsetTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
int bufferSize = MAX_SAMPLE_SIZE;
ByteBuffer byteBufSrc = ByteBuffer.allocate(bufferSize);
ByteBuffer byteBufTest = ByteBuffer.allocate(bufferSize);
int srcBufSize = extractorSrc.readSampleData(byteBufSrc, 0);
int testBufSize = extractorTest.readSampleData(byteBufTest, 0);
if (!(byteBufSrc.equals(byteBufTest))) {
if (VERBOSE) {
Log.d(TAG,
"srcTrackIndex:" + extractorSrc.getSampleTrackIndex()
+ " testTrackIndex:" + extractorTest.getSampleTrackIndex());
Log.d(TAG,
"srcTSus:" + extractorSrc.getSampleTime()
+ " testTSus:" + extractorTest.getSampleTime());
Log.d(TAG, "srcBufSize:" + srcBufSize + "testBufSize:" + testBufSize);
}
fail("byteBuffer didn't match");
}
extractorSrc.unselectTrack(i);
extractorTest.unselectTrack(i);
}
extractorSrc.release();
extractorTest.release();
testFd.close();
}
/**
* Using MediaMuxer and MediaExtractor to mux a media file from another file while skipping
* some video frames as in the issues b/63590381 and b/64949961.
*/
private void simulateVideoFramesDropIssuesAndMux(int srcMedia, String dstMediaPath,
int expectedTrackCount, int fmt) throws IOException {
// Set up MediaExtractor to read from the source.
AssetFileDescriptor srcFd = mResources.openRawResourceFd(srcMedia);
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(srcFd.getFileDescriptor(), srcFd.getStartOffset(),
srcFd.getLength());
int trackCount = extractor.getTrackCount();
assertEquals("wrong number of tracks", expectedTrackCount, trackCount);
// Set up MediaMuxer for the destination.
MediaMuxer muxer;
muxer = new MediaMuxer(dstMediaPath, fmt);
// Set up the tracks.
HashMap<Integer, Integer> indexMap = new HashMap<Integer, Integer>(trackCount);
for (int i = 0; i < trackCount; i++) {
extractor.selectTrack(i);
MediaFormat format = extractor.getTrackFormat(i);
int dstIndex = muxer.addTrack(format);
indexMap.put(i, dstIndex);
}
// Copy the samples from MediaExtractor to MediaMuxer.
boolean sawEOS = false;
int bufferSize = MAX_SAMPLE_SIZE;
int sampleCount = 0;
int offset = 0;
int videoSampleCount = 0;
// Counting frame index values starting from 1
final int muxAllTypeVideoFramesUntilIndex = 136; // I/P/B frames passed as it is until this
final int muxAllTypeVideoFramesFromIndex = 171; // I/P/B frames passed as it is from this
final int pFrameBeforeARandomBframeIndex = 137;
final int bFrameAfterPFrameIndex = pFrameBeforeARandomBframeIndex+1;
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while (!sawEOS) {
bufferInfo.offset = 0;
bufferInfo.size = extractor.readSampleData(dstBuf, offset);
if (bufferInfo.size < 0) {
if (VERBOSE) {
Log.d(TAG, "saw input EOS.");
}
sawEOS = true;
bufferInfo.size = 0;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
int trackIndex = extractor.getSampleTrackIndex();
// Video track at index 0, skip some video frames while muxing.
if (trackIndex == 0) {
++videoSampleCount;
if (VERBOSE) {
Log.i(TAG, "videoSampleCount : " + videoSampleCount);
}
if (videoSampleCount <= muxAllTypeVideoFramesUntilIndex
|| videoSampleCount == bFrameAfterPFrameIndex) {
// Write frame as it is.
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
} else if (videoSampleCount == pFrameBeforeARandomBframeIndex
|| videoSampleCount >= muxAllTypeVideoFramesFromIndex) {
// Adjust time stamp for this P frame to a few frames later, say ~5seconds
bufferInfo.presentationTimeUs += OFFSET_TIME_US;
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
} else {
// Skip frames after bFrameAfterPFrameIndex
// and before muxAllTypeVideoFramesFromIndex.
if (VERBOSE) {
Log.i(TAG, "skipped this frame");
}
}
} else {
// write audio data as it is continuously
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
}
extractor.advance();
sampleCount++;
if (VERBOSE) {
Log.d(TAG, "Frame (" + sampleCount + ") " +
"PresentationTimeUs:" + bufferInfo.presentationTimeUs +
" Flags:" + bufferInfo.flags +
" TrackIndex:" + trackIndex +
" Size(bytes) " + bufferInfo.size );
}
}
}
muxer.stop();
muxer.release();
extractor.release();
srcFd.close();
return;
}
/* Uses two MediaExtractor's and checks whether timestamps of first few and another few
* from last sync frame matches
*/
private void verifyAFewSamplesTimestamp(int srcMediaId, String testMediaPath)
throws IOException {
final int numFramesTSCheck = 10; // Num frames to be checked for its timestamps
AssetFileDescriptor srcFd = mResources.openRawResourceFd(srcMediaId);
MediaExtractor extractorSrc = new MediaExtractor();
extractorSrc.setDataSource(srcFd.getFileDescriptor(),
srcFd.getStartOffset(), srcFd.getLength());
MediaExtractor extractorTest = new MediaExtractor();
extractorTest.setDataSource(testMediaPath);
int trackCount = extractorSrc.getTrackCount();
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractorSrc.getTrackFormat(i);
extractorSrc.selectTrack(i);
extractorTest.selectTrack(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
// Check time stamps for numFramesTSCheck frames from 33333us.
checkNumFramesTimestamp(33333, 0, numFramesTSCheck, extractorSrc, extractorTest);
// Check time stamps for numFramesTSCheck frames from 9333333 -
// sync frame after framedrops at index 172 of video track.
checkNumFramesTimestamp(
9333333, OFFSET_TIME_US, numFramesTSCheck, extractorSrc, extractorTest);
} else if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
// Check timestamps for all audio frames. Test file has 427 audio frames.
checkNumFramesTimestamp(0, 0, 427, extractorSrc, extractorTest);
}
extractorSrc.unselectTrack(i);
extractorTest.unselectTrack(i);
}
extractorSrc.release();
extractorTest.release();
srcFd.close();
}
private void checkNumFramesTimestamp(long seekTimeUs, long offsetTimeUs, int numFrames,
MediaExtractor extractorSrc, MediaExtractor extractorTest) {
long srcSampleTimeUs = -1;
long testSampleTimeUs = -1;
extractorSrc.seekTo(seekTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
extractorTest.seekTo(seekTimeUs + offsetTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
while (numFrames-- > 0 ) {
srcSampleTimeUs = extractorSrc.getSampleTime();
testSampleTimeUs = extractorTest.getSampleTime();
if(srcSampleTimeUs == -1 || testSampleTimeUs == -1){
fail("either of tracks reached end of stream");
}
if ((srcSampleTimeUs + offsetTimeUs) != testSampleTimeUs) {
if (VERBOSE) {
Log.d(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() +
" testTrackIndex:" + extractorTest.getSampleTrackIndex());
Log.d(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs);
}
fail("timestamps didn't match");
}
extractorSrc.advance();
extractorTest.advance();
}
}
/**
* Using MediaMuxer and MediaExtractor to mux a media file from another file while skipping
* 0 or more video frames and desired start offsets for each track.
* startOffsetUsVect : order of tracks is the same as in the input file
*/
private void cloneMediaWithSamplesDropAndStartOffsets(int srcMedia, String dstMediaPath,
int fmt, HashSet<Integer> samplesDropSet, Vector<Integer> startOffsetUsVect)
throws IOException {
// Set up MediaExtractor to read from the source.
AssetFileDescriptor srcFd = mResources.openRawResourceFd(srcMedia);
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(srcFd.getFileDescriptor(), srcFd.getStartOffset(),
srcFd.getLength());
int trackCount = extractor.getTrackCount();
// Set up MediaMuxer for the destination.
MediaMuxer muxer;
muxer = new MediaMuxer(dstMediaPath, fmt);
// Set up the tracks.
HashMap<Integer, Integer> indexMap = new HashMap<Integer, Integer>(trackCount);
int videoTrackIndex = 100;
int videoStartOffsetUs = 0;
int audioTrackIndex = 100;
int audioStartOffsetUs = 0;
for (int i = 0; i < trackCount; i++) {
extractor.selectTrack(i);
MediaFormat format = extractor.getTrackFormat(i);
int dstIndex = muxer.addTrack(format);
indexMap.put(i, dstIndex);
if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
videoTrackIndex = i;
// Make sure there's an entry for video track.
if (startOffsetUsVect != null && (videoTrackIndex < startOffsetUsVect.size())) {
videoStartOffsetUs = startOffsetUsVect.get(videoTrackIndex);
}
}
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
audioTrackIndex = i;
// Make sure there's an entry for audio track.
if (startOffsetUsVect != null && (audioTrackIndex < startOffsetUsVect.size())) {
audioStartOffsetUs = startOffsetUsVect.get(audioTrackIndex);
}
}
}
// Copy the samples from MediaExtractor to MediaMuxer.
boolean sawEOS = false;
int bufferSize = MAX_SAMPLE_SIZE;
int sampleCount = 0;
int offset = 0;
int videoSampleCount = 0;
ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize);
BufferInfo bufferInfo = new BufferInfo();
muxer.start();
while (!sawEOS) {
bufferInfo.offset = 0;
bufferInfo.size = extractor.readSampleData(dstBuf, offset);
if (bufferInfo.size < 0) {
if (VERBOSE) {
Log.d(TAG, "saw input EOS.");
}
sawEOS = true;
bufferInfo.size = 0;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
bufferInfo.flags = extractor.getSampleFlags();
int trackIndex = extractor.getSampleTrackIndex();
if (trackIndex == videoTrackIndex) {
++videoSampleCount;
if (VERBOSE) {
Log.i(TAG, "videoSampleCount : " + videoSampleCount);
}
if (samplesDropSet == null || (!samplesDropSet.contains(videoSampleCount))) {
// Write video frame with start offset adjustment.
bufferInfo.presentationTimeUs += videoStartOffsetUs;
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
}
else {
if (VERBOSE) {
Log.i(TAG, "skipped this frame");
}
}
} else {
// write audio sample with start offset adjustment.
bufferInfo.presentationTimeUs += audioStartOffsetUs;
muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, bufferInfo);
}
extractor.advance();
sampleCount++;
if (VERBOSE) {
Log.i(TAG, "Sample (" + sampleCount + ")" +
" TrackIndex:" + trackIndex +
" PresentationTimeUs:" + bufferInfo.presentationTimeUs +
" Flags:" + bufferInfo.flags +
" Size(bytes)" + bufferInfo.size );
}
}
}
muxer.stop();
muxer.release();
extractor.release();
srcFd.close();
return;
}
/*
* Uses MediaExtractors and checks whether timestamps of all samples except in samplesDropSet
* and with start offsets adjustments for each track match.
*/
private void verifyTimestampsWithSamplesDropSet(int srcMediaId, String testMediaPath,
HashSet<Integer> samplesDropSet, Vector<Integer> startOffsetUsVect) throws IOException {
AssetFileDescriptor srcFd = mResources.openRawResourceFd(srcMediaId);
MediaExtractor extractorSrc = new MediaExtractor();
extractorSrc.setDataSource(srcFd.getFileDescriptor(),
srcFd.getStartOffset(), srcFd.getLength());
MediaExtractor extractorTest = new MediaExtractor();
extractorTest.setDataSource(testMediaPath);
int videoTrackIndex = -1;
int videoStartOffsetUs = 0;
int trackCount = extractorSrc.getTrackCount();
// Select video track.
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractorSrc.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
videoTrackIndex = i;
if (startOffsetUsVect != null && videoTrackIndex < startOffsetUsVect.size()) {
videoStartOffsetUs = startOffsetUsVect.get(videoTrackIndex);
}
extractorSrc.selectTrack(videoTrackIndex);
extractorTest.selectTrack(videoTrackIndex);
checkVideoSamplesTimeStamps(extractorSrc, extractorTest, samplesDropSet,
videoStartOffsetUs);
extractorSrc.unselectTrack(videoTrackIndex);
extractorTest.unselectTrack(videoTrackIndex);
}
}
int audioTrackIndex = -1;
int audioSampleCount = 0;
int audioStartOffsetUs = 0;
//select audio track
for (int i = 0; i < trackCount; i++) {
MediaFormat format = extractorSrc.getTrackFormat(i);
if (format.getString(MediaFormat.KEY_MIME).startsWith("audio/")) {
audioTrackIndex = i;
if (startOffsetUsVect != null && audioTrackIndex < startOffsetUsVect.size()) {
audioStartOffsetUs = startOffsetUsVect.get(audioTrackIndex);
}
extractorSrc.selectTrack(audioTrackIndex);
extractorTest.selectTrack(audioTrackIndex);
checkAudioSamplesTimestamps(extractorSrc, extractorTest, audioStartOffsetUs);
}
}
extractorSrc.release();
extractorTest.release();
srcFd.close();
}
// Check timestamps of all video samples.
private void checkVideoSamplesTimeStamps(MediaExtractor extractorSrc,
MediaExtractor extractorTest, HashSet<Integer> samplesDropSet,
int videoStartOffsetUs) {
long srcSampleTimeUs = -1;
long testSampleTimeUs = -1;
boolean srcAdvance = false;
boolean testAdvance = false;
int videoSampleCount = 0;
extractorSrc.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
extractorTest.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
do {
++videoSampleCount;
srcSampleTimeUs = extractorSrc.getSampleTime();
testSampleTimeUs = extractorTest.getSampleTime();
if (VERBOSE) {
Log.i(TAG, "videoSampleCount:" + videoSampleCount);
Log.i(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() +
" testTrackIndex:" + extractorTest.getSampleTrackIndex());
Log.i(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs);
}
if (samplesDropSet == null || !samplesDropSet.contains(videoSampleCount)) {
if (srcSampleTimeUs == -1 || testSampleTimeUs == -1) {
if (VERBOSE) {
Log.d(TAG, "videoSampleCount:" + videoSampleCount);
Log.d(TAG, "srcUs:" + srcSampleTimeUs + "testUs:" + testSampleTimeUs);
}
fail("either source or test track reached end of stream");
}
// Stts values within 0.1ms(100us) difference are fudged to save too many
// stts entries in MPEG4Writer.
else if (Math.abs(srcSampleTimeUs + videoStartOffsetUs - testSampleTimeUs) > 100) {
if (VERBOSE) {
Log.d(TAG, "Fail:video timestamps didn't match");
Log.d(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() +
" testTrackIndex:" + extractorTest.getSampleTrackIndex());
Log.d(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs);
Log.d(TAG, "videoSampleCount:" + videoSampleCount);
}
fail("video timestamps didn't match");
}
testAdvance = extractorTest.advance();
}
srcAdvance = extractorSrc.advance();
} while(srcAdvance && testAdvance);
if (srcAdvance != testAdvance) {
if (VERBOSE) {
Log.d(TAG, "videoSampleCount:" + videoSampleCount);
}
fail("either video track has not reached its last sample");
}
}
private void checkAudioSamplesTimestamps(MediaExtractor extractorSrc,
MediaExtractor extractorTest, int audioStartOffsetUs) {
long srcSampleTimeUs = -1;
long testSampleTimeUs = -1;
boolean srcAdvance = false;
boolean testAdvance = false;
int audioSampleCount = 0;
extractorSrc.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
extractorTest.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
// Check timestamps of all audio samples.
do {
++audioSampleCount;
srcSampleTimeUs = extractorSrc.getSampleTime();
testSampleTimeUs = extractorTest.getSampleTime();
if(VERBOSE) {
Log.d(TAG, "audioSampleCount:" + audioSampleCount);
Log.v(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() +
" testTrackIndex:" + extractorTest.getSampleTrackIndex());
Log.v(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs);
}
if (srcSampleTimeUs == -1 || testSampleTimeUs == -1) {
if (VERBOSE) {
Log.d(TAG, "audioSampleCount:" + audioSampleCount);
Log.d(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs);
}
fail("either source or test track reached end of stream");
}
// First audio sample would have zero timestamp and its start offset is implemented
// by assigning the first audio sample's duration as the offset. Second sample onwards
// would play after the offset. But video offset is achieved by edit list entry for
// video tracks with BFrames. Need to revert the conditional check for first
// audio sample once we implement empty edit list entry for audio.
else if ((audioSampleCount > 1 &&
(srcSampleTimeUs + audioStartOffsetUs) != testSampleTimeUs) ||
(audioSampleCount == 1 && srcSampleTimeUs != testSampleTimeUs)) {
fail("audio timestamps didn't match");
}
testAdvance = extractorTest.advance();
srcAdvance = extractorSrc.advance();
} while(srcAdvance && testAdvance);
if (srcAdvance != testAdvance) {
fail("either audio track has not reached its last sample");
}
}
}