| /* |
| * 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.muxer.cts; |
| |
| import static org.junit.Assert.assertArrayEquals; |
| |
| import android.content.Context; |
| import android.content.res.AssetFileDescriptor; |
| import android.media.MediaCodec.BufferInfo; |
| import android.media.MediaExtractor; |
| import android.media.MediaFormat; |
| import android.media.MediaMetadataRetriever; |
| import android.media.MediaMuxer; |
| import android.media.cts.Preconditions; |
| import android.os.Build; |
| import android.os.ParcelFileDescriptor; |
| import android.platform.test.annotations.AppModeFull; |
| import android.test.AndroidTestCase; |
| import android.util.Log; |
| |
| import com.android.compatibility.common.util.MediaUtils; |
| |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Vector; |
| import java.util.concurrent.ExecutionException; |
| import java.util.function.Function; |
| 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 = 1024 * 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 static final String MEDIA_DIR = WorkDir.getMediaDirString(); |
| |
| private final boolean mAndroid11 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; |
| |
| @Override |
| public void setContext(Context context) { |
| super.setContext(context); |
| } |
| |
| protected AssetFileDescriptor getAssetFileDescriptorFor(final String res) |
| throws FileNotFoundException { |
| Preconditions.assertTestFileExists(MEDIA_DIR + res); |
| File inpFile = new File(MEDIA_DIR + res); |
| ParcelFileDescriptor parcelFD = |
| ParcelFileDescriptor.open(inpFile, ParcelFileDescriptor.MODE_READ_ONLY); |
| return new AssetFileDescriptor(parcelFD, 0, parcelFD.getStatSize()); |
| } |
| |
| public void testWebmOutput() throws Exception { |
| final String source = |
| "video_480x360_webm_vp9_333kbps_25fps_vorbis_stereo_128kbps_48000hz.webm"; |
| String outputFilePath = File.createTempFile("testWebmOutput", ".webm") |
| .getAbsolutePath(); |
| cloneAndVerify(source, outputFilePath, 2, 90, MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM); |
| } |
| |
| /** |
| * Test: make sure the muxer handles dovi profile 8.4 video track only file correctly. |
| */ |
| public void testDolbyVisionVideoOnlyP8() throws Exception { |
| final String source = "video_dovi_1920x1080_60fps_dvhe_08_04.mp4"; |
| String outputFilePath = File.createTempFile("MediaMuxerTest_dolbyvisionP8videoOnly", ".mp4") |
| .getAbsolutePath(); |
| try { |
| cloneAndVerify(source, outputFilePath, 2 /* expectedTrackCount */, 180 /* degrees */, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, |
| MediaMuxerTest::filterOutNonDolbyVisionFormat); |
| } finally { |
| new File(outputFilePath).delete(); |
| } |
| } |
| |
| /** |
| * Test: make sure the muxer handles dovi profile 9.2 video track only file correctly. |
| */ |
| public void testDolbyVisionVideoOnlyP9() throws Exception { |
| final String source = "video_dovi_1920x1080_60fps_dvav_09_02.mp4"; |
| String outputFilePath = File.createTempFile("MediaMuxerTest_dolbyvisionP9videoOnly", ".mp4") |
| .getAbsolutePath(); |
| try { |
| cloneAndVerify(source, outputFilePath, 2 /* expectedTrackCount */, 180 /* degrees */, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, |
| MediaMuxerTest::filterOutNonDolbyVisionFormat); |
| } finally { |
| new File(outputFilePath).delete(); |
| } |
| } |
| |
| private static MediaFormat filterOutNonDolbyVisionFormat(MediaFormat format) { |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| return mime.equals(MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION) ? format : null; |
| } |
| |
| /** |
| * 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 { |
| final String source = "video_h264_main_b_frames.mp4"; |
| String outputFilePath = File.createTempFile( |
| "MediaMuxerTest_testSimulateAudioBVideoFramesDropIssues", ".mp4").getAbsolutePath(); |
| try { |
| simulateVideoFramesDropIssuesAndMux(source, outputFilePath, 2 /* track index */, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); |
| verifyAFewSamplesTimestamp(source, outputFilePath); |
| verifySamplesMatch(source, outputFilePath, 66667 /* sample around 0 sec */, 0); |
| verifySamplesMatch( |
| source, outputFilePath, 8033333 /* sample around 8 sec */, OFFSET_TIME_US); |
| } 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 { |
| final String source = "video_480x360_mp4_h264_bframes_495kbps_30fps_editlist.mp4"; |
| 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(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, true /* has B frames */, 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 { |
| final String source = "video_480x360_mp4_h264_bframes_495kbps_30fps_editlist.mp4"; |
| 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(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, true /* has B frames */, 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 { |
| final String source = "video_h264_main_b_frames.mp4"; |
| 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(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, true /* has B frames */, 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 { |
| final String source = "video_h264_main_b_frames.mp4"; |
| 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(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, samplesDropSet, null); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, true /* has B frames */, 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 { |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 400000us. |
| startOffsetUsVect.add(400000); |
| // Audio starts at 0us. |
| startOffsetUsVect.add(0); |
| checkTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames |
| * when video and audio samples start after zero, video later than audio. |
| */ |
| public void testTimestampsAudioBVideoStartOffsetVideoAudio() throws Exception { |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 400000us. |
| startOffsetUsVect.add(400000); |
| // Audio starts at 200000us. |
| startOffsetUsVect.add(200000); |
| checkTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames |
| * when video and audio samples start after zero, audio later than video. |
| */ |
| public void testTimestampsAudioBVideoStartOffsetAudioVideo() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 200000us. |
| startOffsetUsVect.add(200000); |
| // Audio starts at 400000us. |
| startOffsetUsVect.add(400000); |
| checkTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames |
| * when video starts after zero and audio starts before zero. |
| */ |
| public void testTimestampsAudioBVideoStartOffsetNegativeAudioVideo() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 200000us. |
| startOffsetUsVect.add(200000); |
| // Audio starts at -23220us, multiple of duration of one frame (1024/44100hz) |
| startOffsetUsVect.add(-23220); |
| checkTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * 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 { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 0us. |
| startOffsetUsVect.add(0); |
| // Audio starts at 400000us. |
| startOffsetUsVect.add(400000); |
| checkTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: make sure if audio/video muxing works good with different start offsets for |
| * audio and video, audio later than video at 0us. |
| */ |
| public void testTimestampsStartOffsetAudio() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 0us. |
| startOffsetUsVect.add(0); |
| // Audio starts at 500000us. |
| startOffsetUsVect.add(500000); |
| checkTimestampsWithStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: make sure if audio/video muxing works good with different start offsets for |
| * audio and video, video later than audio at 0us. |
| */ |
| public void testTimestampsStartOffsetVideo() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 500000us. |
| startOffsetUsVect.add(500000); |
| // Audio starts at 0us. |
| startOffsetUsVect.add(0); |
| checkTimestampsWithStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: make sure if audio/video muxing works good with different start offsets for |
| * audio and video, audio later than video, positive offsets for both. |
| */ |
| public void testTimestampsStartOffsetVideoAudio() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 250000us. |
| startOffsetUsVect.add(250000); |
| // Audio starts at 500000us. |
| startOffsetUsVect.add(500000); |
| checkTimestampsWithStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: make sure if audio/video muxing works good with different start offsets for |
| * audio and video, video later than audio, positive offets for both. |
| */ |
| public void testTimestampsStartOffsetAudioVideo() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 500000us. |
| startOffsetUsVect.add(500000); |
| // Audio starts at 250000us. |
| startOffsetUsVect.add(250000); |
| checkTimestampsWithStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: make sure if audio/video muxing works good with different start offsets for |
| * audio and video, video later than audio, audio before zero. |
| */ |
| public void testTimestampsStartOffsetNegativeAudioVideo() throws Exception { |
| if (!MediaUtils.check(mAndroid11, "test needs Android 11")) return; |
| |
| Vector<Integer> startOffsetUsVect = new Vector<Integer>(); |
| // Video starts at 50000us. |
| startOffsetUsVect.add(50000); |
| // Audio starts at -23220us, multiple of duration of one frame (1024/44100hz) |
| startOffsetUsVect.add(-23220); |
| checkTimestampsWithStartOffsets(startOffsetUsVect); |
| } |
| |
| /** |
| * Test: makes sure if audio/video muxing using MPEG4Writer works with B Frames |
| * when video and audio samples start after different times. |
| */ |
| private void checkTimestampsAudioBVideoDiffStartOffsets(Vector<Integer> startOffsetUs) |
| throws Exception { |
| MPEG4CheckTimestampsAudioBVideoDiffStartOffsets(startOffsetUs); |
| // TODO: uncomment webm testing once bugs related to timestamps in webmwriter are fixed. |
| // WebMCheckTimestampsAudioBVideoDiffStartOffsets(startOffsetUsVect); |
| } |
| |
| private void MPEG4CheckTimestampsAudioBVideoDiffStartOffsets(Vector<Integer> startOffsetUs) |
| throws Exception { |
| if (VERBOSE) { |
| Log.v(TAG, "MPEG4CheckTimestampsAudioBVideoDiffStartOffsets"); |
| } |
| final String source = "video_h264_main_b_frames.mp4"; |
| String outputFilePath = File.createTempFile( |
| "MediaMuxerTest_testTimestampsAudioBVideoDiffStartOffsets", ".mp4").getAbsolutePath(); |
| try { |
| cloneMediaWithSamplesDropAndStartOffsets(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, null, startOffsetUs); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, true /* has B frames */, outputFilePath, null, startOffsetUs); |
| } finally { |
| new File(outputFilePath).delete(); |
| } |
| } |
| |
| /* |
| * Check if timestamps are written consistently across all formats supported by MediaMuxer. |
| */ |
| private void checkTimestampsWithStartOffsets(Vector<Integer> startOffsetUsVect) |
| throws Exception { |
| MPEG4CheckTimestampsWithStartOffsets(startOffsetUsVect); |
| // TODO: uncomment webm testing once bugs related to timestamps in webmwriter are fixed. |
| // WebMCheckTimestampsWithStartOffsets(startOffsetUsVect); |
| // TODO: need to add other formats, OGG, AAC, AMR |
| } |
| |
| /** |
| * Make sure if audio/video muxing using MPEG4Writer works good with different start |
| * offsets for audio and video. |
| */ |
| private void MPEG4CheckTimestampsWithStartOffsets(Vector<Integer> startOffsetUsVect) |
| throws Exception { |
| if (VERBOSE) { |
| Log.v(TAG, "MPEG4CheckTimestampsWithStartOffsets"); |
| } |
| final String source = "video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz.mp4"; |
| String outputFilePath = |
| File.createTempFile("MediaMuxerTest_MPEG4CheckTimestampsWithStartOffsets", ".mp4") |
| .getAbsolutePath(); |
| try { |
| cloneMediaWithSamplesDropAndStartOffsets(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4, null, startOffsetUsVect); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, false /* no B frames */, outputFilePath, null, startOffsetUsVect); |
| } finally { |
| new File(outputFilePath).delete(); |
| } |
| } |
| |
| /** |
| * Make sure if audio/video muxing using WebMWriter works good with different start |
| * offsets for audio and video. |
| */ |
| private void WebMCheckTimestampsWithStartOffsets(Vector<Integer> startOffsetUsVect) |
| throws Exception { |
| if (VERBOSE) { |
| Log.v(TAG, "WebMCheckTimestampsWithStartOffsets"); |
| } |
| final String source = |
| "video_480x360_webm_vp9_333kbps_25fps_vorbis_stereo_128kbps_48000hz.webm"; |
| String outputFilePath = |
| File.createTempFile("MediaMuxerTest_WebMCheckTimestampsWithStartOffsets", ".webm") |
| .getAbsolutePath(); |
| try { |
| cloneMediaWithSamplesDropAndStartOffsets(source, outputFilePath, |
| MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM, null, startOffsetUsVect); |
| verifyTSWithSamplesDropAndStartOffset( |
| source, false /* no B frames */, 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(final String srcMedia, String outputMediaFile, |
| int expectedTrackCount, int degrees, int fmt) throws IOException { |
| cloneAndVerify(srcMedia, outputMediaFile, expectedTrackCount, degrees, fmt, |
| Function.identity()); |
| } |
| |
| /** |
| * Clones a given file using MediaMuxer and verifies the output matches the input. |
| * |
| * <p>See {@link #cloneMediaUsingMuxer} for information about the parameters. |
| */ |
| private void cloneAndVerify(final String srcMedia, String outputMediaFile, |
| int expectedTrackCount, int degrees, int fmt, |
| Function<MediaFormat, MediaFormat> muxerInputTrackFormatTransformer) |
| throws IOException { |
| try { |
| cloneMediaUsingMuxer( |
| srcMedia, |
| outputMediaFile, |
| expectedTrackCount, |
| degrees, |
| fmt, |
| muxerInputTrackFormatTransformer); |
| 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. |
| verifyTSWithSamplesDropAndStartOffset( |
| srcMedia, false /* no B frames */,outputMediaFile, null, null); |
| } finally { |
| new File(outputMediaFile).delete(); |
| } |
| } |
| |
| |
| /** |
| * Clones a given file using MediaMuxer. |
| * |
| * @param srcMedia Input file path passed to extractor |
| * @param dstMediaPath Output file path passed to muxer |
| * @param expectedTrackCount Expected number of tracks in the input file |
| * @param degrees orientation hint in degrees |
| * @param fmt one of the values defined in {@link MediaMuxer.OutputFormat}. |
| * @param muxerInputTrackFormatTransformer Function applied on the MediaMuxer input formats. |
| * If the function returns null for a given MediaFormat, |
| * the corresponding track is discarded and not passed |
| * to MediaMuxer. |
| * @throws IOException if muxer failed to open output file for write. |
| */ |
| private void cloneMediaUsingMuxer( |
| final String srcMedia, |
| String dstMediaPath, |
| int expectedTrackCount, |
| int degrees, |
| int fmt, |
| Function<MediaFormat, MediaFormat> muxerInputTrackFormatTransformer) |
| throws IOException { |
| // Set up MediaExtractor to read from the source. |
| AssetFileDescriptor srcFd = getAssetFileDescriptorFor(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++) { |
| MediaFormat format = extractor.getTrackFormat(i); |
| MediaFormat muxedFormat = muxerInputTrackFormatTransformer.apply(format); |
| if (muxedFormat != null) { |
| extractor.selectTrack(i); |
| int dstIndex = muxer.addTrack(muxedFormat); |
| 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(final String srcMedia, String testMediaPath, |
| int degrees) throws IOException { |
| AssetFileDescriptor testFd = getAssetFileDescriptorFor(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); |
| |
| //TODO: need to check each individual track's duration also. |
| 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(final String srcMedia, String testMediaPath, int seekToUs, |
| long offsetTimeUs) throws IOException { |
| AssetFileDescriptor testFd = getAssetFileDescriptorFor(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(final String srcMedia, String dstMediaPath, |
| int expectedTrackCount, int fmt) throws IOException { |
| // Set up MediaExtractor to read from the source. |
| AssetFileDescriptor srcFd = getAssetFileDescriptorFor(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.v(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(final String srcMedia, String testMediaPath) |
| throws IOException { |
| final int numFramesTSCheck = 10; // Num frames to be checked for its timestamps |
| |
| AssetFileDescriptor srcFd = getAssetFileDescriptorFor(srcMedia); |
| 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(final String srcMedia, String dstMediaPath, |
| int fmt, HashSet<Integer> samplesDropSet, Vector<Integer> startOffsetUsVect) |
| throws IOException { |
| // Set up MediaExtractor to read from the source. |
| AssetFileDescriptor srcFd = getAssetFileDescriptorFor(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 (VERBOSE) { |
| Log.v(TAG, "TrackIndex:" + trackIndex + " PresentationTimeUs:" + |
| bufferInfo.presentationTimeUs + " Flags:" + bufferInfo.flags + |
| " Size(bytes)" + bufferInfo.size); |
| } |
| if (trackIndex == videoTrackIndex) { |
| ++videoSampleCount; |
| if (VERBOSE) { |
| Log.v(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.v(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 verifyTSWithSamplesDropAndStartOffset(final String srcMedia, boolean hasBframes, |
| String testMediaPath, HashSet<Integer> samplesDropSet, |
| Vector<Integer> startOffsetUsVect) throws IOException { |
| AssetFileDescriptor srcFd = getAssetFileDescriptorFor(srcMedia); |
| 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 minStartOffsetUs = Integer.MAX_VALUE; |
| int trackCount = extractorSrc.getTrackCount(); |
| |
| /* |
| * When all track's start offsets are positive, MPEG4Writer makes the start timestamp of the |
| * earliest track as zero and adjusts all other tracks' timestamp accordingly. |
| */ |
| // TODO: need to confirm if the above logic holds good with all others writers we support. |
| if (startOffsetUsVect != null) { |
| for (int startOffsetUs : startOffsetUsVect) { |
| minStartOffsetUs = Math.min(startOffsetUs, minStartOffsetUs); |
| } |
| } else { |
| minStartOffsetUs = 0; |
| } |
| |
| if (minStartOffsetUs < 0) { |
| /* |
| * Atleast one of the start offsets were negative. We have some test cases with negative |
| * offsets for audio, minStartOffset has to be reset as Writer won't adjust any of the |
| * track's timestamps. |
| */ |
| minStartOffsetUs = 0; |
| } |
| |
| // 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, hasBframes, extractorTest, samplesDropSet, |
| videoStartOffsetUs - minStartOffsetUs); |
| 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 - minStartOffsetUs); |
| } |
| } |
| |
| extractorSrc.release(); |
| extractorTest.release(); |
| srcFd.close(); |
| } |
| |
| // Check timestamps of all video samples. |
| private void checkVideoSamplesTimeStamps(MediaExtractor extractorSrc, boolean hasBFrames, |
| 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); |
| |
| if (VERBOSE) { |
| Log.v(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() + |
| " testTrackIndex:" + extractorTest.getSampleTrackIndex()); |
| Log.v(TAG, "videoStartOffsetUs:" + videoStartOffsetUs); |
| } |
| |
| do { |
| ++videoSampleCount; |
| srcSampleTimeUs = extractorSrc.getSampleTime(); |
| testSampleTimeUs = extractorTest.getSampleTime(); |
| if (VERBOSE) { |
| Log.v(TAG, "videoSampleCount:" + videoSampleCount); |
| Log.i(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs); |
| } |
| if (samplesDropSet == null || !samplesDropSet.contains(videoSampleCount)) { |
| if (srcSampleTimeUs == -1 || testSampleTimeUs == -1) { |
| if (VERBOSE) { |
| Log.v(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.v(TAG, "Fail:video timestamps didn't match"); |
| Log.v(TAG, |
| "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() |
| + " testTrackIndex:" + extractorTest.getSampleTrackIndex()); |
| Log.v(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs); |
| } |
| fail("video timestamps didn't match"); |
| } |
| testAdvance = extractorTest.advance(); |
| } |
| srcAdvance = extractorSrc.advance(); |
| } while (srcAdvance && testAdvance); |
| if (srcAdvance != testAdvance) { |
| if (VERBOSE) { |
| Log.v(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); |
| if (audioStartOffsetUs >= 0) { |
| // Added edit list support for maintaining only the diff in start offsets of tracks. |
| // TODO: Remove this once we add support for preserving absolute timestamps as well. |
| extractorTest.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC); |
| } else { |
| extractorTest.seekTo(audioStartOffsetUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC); |
| } |
| if (VERBOSE) { |
| Log.v(TAG, "audioStartOffsetUs:" + audioStartOffsetUs); |
| Log.v(TAG, "srcTrackIndex:" + extractorSrc.getSampleTrackIndex() + |
| " testTrackIndex:" + extractorTest.getSampleTrackIndex()); |
| } |
| // Check timestamps of all audio samples. |
| do { |
| ++audioSampleCount; |
| srcSampleTimeUs = extractorSrc.getSampleTime(); |
| testSampleTimeUs = extractorTest.getSampleTime(); |
| if (VERBOSE) { |
| Log.v(TAG, "audioSampleCount:" + audioSampleCount); |
| Log.v(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs); |
| } |
| |
| if (srcSampleTimeUs == -1 || testSampleTimeUs == -1) { |
| if (VERBOSE) { |
| Log.v(TAG, "srcTSus:" + srcSampleTimeUs + " testTSus:" + testSampleTimeUs); |
| } |
| fail("either source or test track reached end of stream"); |
| } |
| // > 1us to ignore any round off errors. |
| else if (Math.abs(srcSampleTimeUs + audioStartOffsetUs - testSampleTimeUs) > 1) { |
| 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"); |
| } |
| } |
| } |
| |