| /* |
| * Copyright 2015 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 static org.junit.Assert.assertNotEquals; |
| |
| import android.content.res.AssetFileDescriptor; |
| import android.content.res.Resources; |
| import android.icu.util.ULocale; |
| import android.media.AudioFormat; |
| import android.media.AudioPresentation; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaDataSource; |
| import android.media.MediaExtractor; |
| import android.media.MediaFormat; |
| import static android.media.MediaFormat.MIMETYPE_VIDEO_DOLBY_VISION; |
| import android.media.cts.R; |
| import android.os.PersistableBundle; |
| import android.platform.test.annotations.AppModeFull; |
| import android.test.AndroidTestCase; |
| import android.util.Log; |
| import android.webkit.cts.CtsTestServer; |
| |
| import androidx.test.filters.SmallTest; |
| |
| import com.android.compatibility.common.util.MediaUtils; |
| |
| import java.io.BufferedReader; |
| import java.io.Closeable; |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.StreamTokenizer; |
| import java.nio.ByteBuffer; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.SortedMap; |
| import java.util.TreeMap; |
| |
| public class MediaExtractorTest extends AndroidTestCase { |
| private static final String TAG = "MediaExtractorTest"; |
| |
| protected Resources mResources; |
| protected MediaExtractor mExtractor; |
| |
| @Override |
| protected void setUp() throws Exception { |
| super.setUp(); |
| mResources = getContext().getResources(); |
| mExtractor = new MediaExtractor(); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| super.tearDown(); |
| mExtractor.release(); |
| } |
| |
| protected TestMediaDataSource getDataSourceFor(int resid) throws Exception { |
| AssetFileDescriptor afd = mResources.openRawResourceFd(resid); |
| return TestMediaDataSource.fromAssetFd(afd); |
| } |
| |
| protected TestMediaDataSource setDataSource(int resid) throws Exception { |
| TestMediaDataSource ds = getDataSourceFor(resid); |
| mExtractor.setDataSource(ds); |
| return ds; |
| } |
| |
| public void testNullMediaDataSourceIsRejected() throws Exception { |
| try { |
| mExtractor.setDataSource((MediaDataSource)null); |
| fail("Expected IllegalArgumentException."); |
| } catch (IllegalArgumentException ex) { |
| // Expected, test passed. |
| } |
| } |
| |
| public void testMediaDataSourceIsClosedOnRelease() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.testvideo); |
| mExtractor.release(); |
| assertTrue(dataSource.isClosed()); |
| } |
| |
| public void testExtractorFailsIfMediaDataSourceThrows() throws Exception { |
| TestMediaDataSource dataSource = getDataSourceFor(R.raw.testvideo); |
| dataSource.throwFromReadAt(); |
| try { |
| mExtractor.setDataSource(dataSource); |
| fail("Expected IOException."); |
| } catch (IOException e) { |
| // Expected. |
| } |
| } |
| |
| public void testExtractorFailsIfMediaDataSourceReturnsAnError() throws Exception { |
| TestMediaDataSource dataSource = getDataSourceFor(R.raw.testvideo); |
| dataSource.returnFromReadAt(-2); |
| try { |
| mExtractor.setDataSource(dataSource); |
| fail("Expected IOException."); |
| } catch (IOException e) { |
| // Expected. |
| } |
| } |
| |
| // Smoke test MediaExtractor reading from a DataSource. |
| public void testExtractFromAMediaDataSource() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.testvideo); |
| checkExtractorSamplesAndMetrics(); |
| } |
| |
| // Smoke test MediaExtractor reading from an AssetFileDescriptor. |
| public void testExtractFromAssetFileDescriptor() throws Exception { |
| AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.testvideo); |
| mExtractor.setDataSource(afd); |
| checkExtractorSamplesAndMetrics(); |
| afd.close(); |
| } |
| |
| // DolbyVisionMediaExtractor for profile-level (DvheDtr/Fhd30). |
| public void testDolbyVisionMediaExtractorProfileDvheDtr() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.video_dovi_1920x1080_30fps_dvhe_04); |
| |
| assertTrue("There should be either 1 or 2 tracks", |
| 0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormat = mExtractor.getTrackFormat(0); |
| int trackCountForDolbyVision = 1; |
| |
| // Handle the case where there is a Dolby Vision extractor |
| // Note that it may or may not have a Dolby Vision Decoder |
| if (mExtractor.getTrackCount() == 2) { |
| if (trackFormat.getString(MediaFormat.KEY_MIME) |
| .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| trackFormat = mExtractor.getTrackFormat(1); |
| trackCountForDolbyVision = 0; |
| } |
| } |
| |
| if (MediaUtils.hasDecoder(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormatForDolbyVision = |
| mExtractor.getTrackFormat(trackCountForDolbyVision); |
| |
| final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/dolby-vision", mimeType); |
| |
| int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr, profile); |
| |
| int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd30, level); |
| |
| final int trackIdForDolbyVision = |
| trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID); |
| |
| final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID); |
| assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat); |
| } |
| |
| // The backward-compatible track should have mime video/hevc |
| final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/hevc", mimeType); |
| } |
| |
| // DolbyVisionMediaExtractor for profile-level (DvheStn/Fhd60). |
| public void testDolbyVisionMediaExtractorProfileDvheStn() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.video_dovi_1920x1080_60fps_dvhe_05); |
| |
| if (MediaUtils.hasDecoder(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| // DvheStn exposes only a single non-backward compatible Dolby Vision HDR track. |
| assertEquals("There must be 1 track", 1, mExtractor.getTrackCount()); |
| final MediaFormat trackFormat = mExtractor.getTrackFormat(0); |
| |
| final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/dolby-vision", mimeType); |
| |
| final int profile = trackFormat.getInteger(MediaFormat.KEY_PROFILE); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheStn, profile); |
| |
| final int level = trackFormat.getInteger(MediaFormat.KEY_LEVEL); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60, level); |
| } else { |
| MediaUtils.skipTest("Device does not provide a Dolby Vision decoder"); |
| } |
| } |
| |
| // DolbyVisionMediaExtractor for profile-level (DvheSt/Fhd60). |
| public void testDolbyVisionMediaExtractorProfileDvheSt() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.video_dovi_1920x1080_60fps_dvhe_08); |
| |
| assertTrue("There should be either 1 or 2 tracks", |
| 0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormat = mExtractor.getTrackFormat(0); |
| int trackCountForDolbyVision = 1; |
| |
| // Handle the case where there is a Dolby Vision extractor |
| // Note that it may or may not have a Dolby Vision Decoder |
| if (mExtractor.getTrackCount() == 2) { |
| if (trackFormat.getString(MediaFormat.KEY_MIME) |
| .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| trackFormat = mExtractor.getTrackFormat(1); |
| trackCountForDolbyVision = 0; |
| } |
| } |
| |
| if (MediaUtils.hasDecoder(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormatForDolbyVision = |
| mExtractor.getTrackFormat(trackCountForDolbyVision); |
| |
| final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/dolby-vision", mimeType); |
| |
| int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt, profile); |
| |
| int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60, level); |
| |
| final int trackIdForDolbyVision = |
| trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID); |
| |
| final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID); |
| assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat); |
| } |
| |
| // The backward-compatible track should have mime video/hevc |
| final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/hevc", mimeType); |
| } |
| |
| // DolbyVisionMediaExtractor for profile-level (DvavSe/Fhd60). |
| public void testDolbyVisionMediaExtractorProfileDvavSe() throws Exception { |
| TestMediaDataSource dataSource = setDataSource(R.raw.video_dovi_1920x1080_60fps_dvav_09); |
| |
| assertTrue("There should be either 1 or 2 tracks", |
| 0 < mExtractor.getTrackCount() && 3 > mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormat = mExtractor.getTrackFormat(0); |
| int trackCountForDolbyVision = 1; |
| |
| // Handle the case where there is a Dolby Vision extractor |
| // Note that it may or may not have a Dolby Vision Decoder |
| if (mExtractor.getTrackCount() == 2) { |
| if (trackFormat.getString(MediaFormat.KEY_MIME) |
| .equalsIgnoreCase(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| trackFormat = mExtractor.getTrackFormat(1); |
| trackCountForDolbyVision = 0; |
| } |
| } |
| |
| if (MediaUtils.hasDecoder(MIMETYPE_VIDEO_DOLBY_VISION)) { |
| assertEquals("There must be 2 tracks", 2, mExtractor.getTrackCount()); |
| |
| MediaFormat trackFormatForDolbyVision = |
| mExtractor.getTrackFormat(trackCountForDolbyVision); |
| |
| final String mimeType = trackFormatForDolbyVision.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/dolby-vision", mimeType); |
| |
| int profile = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_PROFILE); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvavSe, profile); |
| |
| int level = trackFormatForDolbyVision.getInteger(MediaFormat.KEY_LEVEL); |
| assertEquals(MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd60, level); |
| |
| final int trackIdForDolbyVision = |
| trackFormatForDolbyVision.getInteger(MediaFormat.KEY_TRACK_ID); |
| |
| final int trackIdForBackwardCompat = trackFormat.getInteger(MediaFormat.KEY_TRACK_ID); |
| assertEquals(trackIdForDolbyVision, trackIdForBackwardCompat); |
| } |
| |
| // The backward-compatible track should have mime video/avc |
| final String mimeType = trackFormat.getString(MediaFormat.KEY_MIME); |
| assertEquals("video/avc", mimeType); |
| } |
| |
| private void checkExtractorSamplesAndMetrics() { |
| // 1MB is enough for any sample. |
| final ByteBuffer buf = ByteBuffer.allocate(1024*1024); |
| final int trackCount = mExtractor.getTrackCount(); |
| |
| for (int i = 0; i < trackCount; i++) { |
| mExtractor.selectTrack(i); |
| } |
| |
| for (int i = 0; i < trackCount; i++) { |
| assertTrue(mExtractor.readSampleData(buf, 0) > 0); |
| assertTrue(mExtractor.advance()); |
| } |
| |
| // verify some getMetrics() behaviors while we're here. |
| PersistableBundle metrics = mExtractor.getMetrics(); |
| if (metrics == null) { |
| fail("getMetrics() returns no data"); |
| } else { |
| // ensure existence of some known fields |
| int tracks = metrics.getInt(MediaExtractor.MetricsConstants.TRACKS, -1); |
| if (tracks != trackCount) { |
| fail("getMetrics() trackCount expect " + trackCount + " got " + tracks); |
| } |
| } |
| } |
| |
| static boolean audioPresentationSetMatchesReference( |
| Map<Integer, AudioPresentation> reference, |
| List<AudioPresentation> actual) { |
| if (reference.size() != actual.size()) { |
| Log.w(TAG, "AudioPresentations set size is invalid, expected: " + |
| reference.size() + ", actual: " + actual.size()); |
| return false; |
| } |
| for (AudioPresentation ap : actual) { |
| AudioPresentation refAp = reference.get(ap.getPresentationId()); |
| if (refAp == null) { |
| Log.w(TAG, "AudioPresentation not found in the reference set, presentation id=" + |
| ap.getPresentationId()); |
| return false; |
| } |
| if (!refAp.equals(ap)) { |
| Log.w(TAG, "AudioPresentations are different, reference: " + |
| refAp + ", actual: " + ap); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| public void testGetAudioPresentations() throws Exception { |
| final int resid = R.raw.MultiLangPerso_1PID_PC0_Select_AC4_H265_DVB_50fps_Audio_Only; |
| TestMediaDataSource dataSource = setDataSource(resid); |
| int ac4TrackIndex = -1; |
| for (int i = 0; i < mExtractor.getTrackCount(); i++) { |
| MediaFormat format = mExtractor.getTrackFormat(i); |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| if (MediaFormat.MIMETYPE_AUDIO_AC4.equals(mime)) { |
| ac4TrackIndex = i; |
| break; |
| } |
| } |
| |
| // Not all devices support AC4 |
| if (ac4TrackIndex == -1) { |
| List<AudioPresentation> presentations = |
| mExtractor.getAudioPresentations(0 /*trackIndex*/); |
| assertNotNull(presentations); |
| assertTrue(presentations.isEmpty()); |
| return; |
| } |
| |
| // The test file has two sets of audio presentations. The presentation set |
| // changes for every 100 audio presentation descriptors between two presentations. |
| // Instead of attempting to count the presentation descriptors, the test assumes |
| // a particular order of the presentations and advances to the next reference set |
| // once getAudioPresentations returns a set that doesn't match the current reference set. |
| // Thus the test can match the set 0 several times, then it encounters set 1, |
| // advances the reference set index, matches set 1 until it encounters set 2 etc. |
| // At the end it verifies that all the reference sets were met. |
| List<Map<Integer, AudioPresentation>> refPresentations = Arrays.asList( |
| new HashMap<Integer, AudioPresentation>() {{ // First set. |
| put(10, new AudioPresentation.Builder(10) |
| .setLocale(ULocale.ENGLISH) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| put(11, new AudioPresentation.Builder(11) |
| .setLocale(ULocale.ENGLISH) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasAudioDescription(true) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| put(12, new AudioPresentation.Builder(12) |
| .setLocale(ULocale.FRENCH) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| }}, |
| new HashMap<Integer, AudioPresentation>() {{ // Second set. |
| put(10, new AudioPresentation.Builder(10) |
| .setLocale(ULocale.GERMAN) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasAudioDescription(true) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| put(11, new AudioPresentation.Builder(11) |
| .setLocale(new ULocale("es")) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasSpokenSubtitles(true) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| put(12, new AudioPresentation.Builder(12) |
| .setLocale(ULocale.ENGLISH) |
| .setMasteringIndication(AudioPresentation.MASTERED_FOR_SURROUND) |
| .setHasDialogueEnhancement(true) |
| .build()); |
| }}, |
| null, |
| null |
| ); |
| refPresentations.set(2, refPresentations.get(0)); |
| refPresentations.set(3, refPresentations.get(1)); |
| boolean[] presentationsMatched = new boolean[refPresentations.size()]; |
| mExtractor.selectTrack(ac4TrackIndex); |
| for (int i = 0; i < refPresentations.size(); ) { |
| List<AudioPresentation> presentations = mExtractor.getAudioPresentations(ac4TrackIndex); |
| assertNotNull(presentations); |
| // Assumes all presentation sets have the same number of presentations. |
| assertEquals(refPresentations.get(i).size(), presentations.size()); |
| if (!audioPresentationSetMatchesReference(refPresentations.get(i), presentations)) { |
| // Time to advance to the next presentation set. |
| i++; |
| continue; |
| } |
| Log.d(TAG, "Matched presentation " + i); |
| presentationsMatched[i] = true; |
| // No need to wait for another switch after the last presentation has been matched. |
| if (i == presentationsMatched.length - 1 || !mExtractor.advance()) { |
| break; |
| } |
| } |
| for (int i = 0; i < presentationsMatched.length; i++) { |
| assertTrue("Presentation set " + i + " was not found in the stream", |
| presentationsMatched[i]); |
| } |
| } |
| |
| @AppModeFull(reason = "Instant apps cannot bind sockets.") |
| public void testExtractorGetCachedDuration() throws Exception { |
| CtsTestServer foo = new CtsTestServer(getContext()); |
| String url = foo.getAssetUrl("ringer.mp3"); |
| mExtractor.setDataSource(url); |
| long cachedDurationUs = mExtractor.getCachedDuration(); |
| assertTrue("cached duration should be non-negative", cachedDurationUs >= 0); |
| } |
| |
| public void testExtractorHasCacheReachedEndOfStream() throws Exception { |
| // Using file source to get deterministic result. |
| AssetFileDescriptor afd = mResources.openRawResourceFd(R.raw.testvideo); |
| mExtractor.setDataSource(afd); |
| assertTrue(mExtractor.hasCacheReachedEndOfStream()); |
| afd.close(); |
| } |
| |
| /* |
| * Makes sure if PTS(order) of a video file with BFrames matches the expected values in |
| * the corresponding text file with just PTS values. |
| */ |
| public void testVideoPresentationTimeStampsMatch() throws Exception { |
| setDataSource(R.raw.binary_counter_320x240_30fps_600frames); |
| // Select the only video track present in the file. |
| final int trackCount = mExtractor.getTrackCount(); |
| for (int i = 0; i < trackCount; i++) { |
| mExtractor.selectTrack(i); |
| } |
| |
| Reader txtRdr = new BufferedReader(new InputStreamReader(mResources.openRawResource( |
| R.raw.timestamps_binary_counter_320x240_30fps_600frames))); |
| StreamTokenizer strTok = new StreamTokenizer(txtRdr); |
| strTok.parseNumbers(); |
| |
| boolean srcAdvance = false; |
| long srcSampleTimeUs = -1; |
| long testSampleTimeUs = -1; |
| |
| strTok.nextToken(); |
| do { |
| srcSampleTimeUs = mExtractor.getSampleTime(); |
| testSampleTimeUs = (long) strTok.nval; |
| |
| // Ignore round-off error if any. |
| if (Math.abs(srcSampleTimeUs - testSampleTimeUs) > 1) { |
| Log.d(TAG, "srcSampleTimeUs:" + srcSampleTimeUs + " testSampleTimeUs:" + |
| testSampleTimeUs); |
| fail("video presentation timestamp not equal"); |
| } |
| |
| srcAdvance = mExtractor.advance(); |
| strTok.nextToken(); |
| } while (srcAdvance); |
| } |
| |
| /* package */ static class ByteBufferDataSource extends MediaDataSource { |
| private final long mSize; |
| private TreeMap<Long, ByteBuffer> mMap = new TreeMap<Long, ByteBuffer>(); |
| |
| public ByteBufferDataSource(MediaCodecTest.ByteBufferStream bufferStream) |
| throws IOException { |
| long size = 0; |
| while (true) { |
| final ByteBuffer buffer = bufferStream.read(); |
| if (buffer == null) break; |
| final int limit = buffer.limit(); |
| if (limit == 0) continue; |
| size += limit; |
| mMap.put(size - 1, buffer); // key: last byte of validity for the buffer. |
| } |
| mSize = size; |
| } |
| |
| @Override |
| public long getSize() { |
| return mSize; |
| } |
| |
| @Override |
| public int readAt(long position, byte[] buffer, int offset, int size) { |
| Log.v(TAG, "reading at " + position + " offset " + offset + " size " + size); |
| |
| // This chooses all buffers with key >= position (e.g. valid buffers) |
| final SortedMap<Long, ByteBuffer> map = mMap.tailMap(position); |
| int copied = 0; |
| for (Map.Entry<Long, ByteBuffer> e : map.entrySet()) { |
| // Get a read-only version of the byte buffer. |
| final ByteBuffer bb = e.getValue().asReadOnlyBuffer(); |
| // Convert read position to an offset within that byte buffer, bboffs. |
| final long bboffs = position - e.getKey() + bb.limit() - 1; |
| if (bboffs >= bb.limit() || bboffs < 0) { |
| break; // (negative position)? |
| } |
| bb.position((int)bboffs); // cast is safe as bb.limit is int. |
| final int tocopy = Math.min(size, bb.remaining()); |
| if (tocopy == 0) { |
| break; // (size == 0)? |
| } |
| bb.get(buffer, offset, tocopy); |
| copied += tocopy; |
| size -= tocopy; |
| offset += tocopy; |
| position += tocopy; |
| if (size == 0) { |
| break; // finished copying. |
| } |
| } |
| if (copied == 0) { |
| copied = -1; // signal end of file |
| } |
| return copied; |
| } |
| |
| @Override |
| public void close() { |
| mMap = null; |
| } |
| } |
| |
| /* package */ static class MediaExtractorStream |
| extends MediaCodecTest.ByteBufferStream implements Closeable { |
| public boolean mIsFloat; |
| public boolean mSawOutputEOS; |
| public MediaFormat mFormat; |
| |
| private MediaExtractor mExtractor; |
| private MediaCodecTest.MediaCodecStream mDecoderStream; |
| |
| public MediaExtractorStream( |
| String inMime, String outMime, |
| MediaDataSource dataSource) throws Exception { |
| mExtractor = new MediaExtractor(); |
| mExtractor.setDataSource(dataSource); |
| final int numTracks = mExtractor.getTrackCount(); |
| // Single track? |
| // assertEquals("Number of tracks should be 1", 1, numTracks); |
| for (int i = 0; i < numTracks; ++i) { |
| final MediaFormat format = mExtractor.getTrackFormat(i); |
| final String actualMime = format.getString(MediaFormat.KEY_MIME); |
| mExtractor.selectTrack(i); |
| mFormat = format; |
| if (outMime.equals(actualMime)) { |
| break; |
| } else { // no matching mime, try to use decoder |
| mDecoderStream = new MediaCodecTest.MediaCodecStream( |
| mExtractor, mFormat); |
| Log.w(TAG, "fallback to input mime type with decoder"); |
| } |
| } |
| assertNotNull("MediaExtractor cannot find mime type " + inMime, mFormat); |
| mIsFloat = mFormat.getInteger( |
| MediaFormat.KEY_PCM_ENCODING, AudioFormat.ENCODING_PCM_16BIT) |
| == AudioFormat.ENCODING_PCM_FLOAT; |
| } |
| |
| public MediaExtractorStream( |
| String inMime, String outMime, |
| MediaCodecTest.ByteBufferStream inputStream) throws Exception { |
| this(inMime, outMime, new ByteBufferDataSource(inputStream)); |
| } |
| |
| @Override |
| public ByteBuffer read() throws IOException { |
| if (mSawOutputEOS) { |
| return null; |
| } |
| if (mDecoderStream != null) { |
| return mDecoderStream.read(); |
| } |
| // To preserve codec-like behavior, we create ByteBuffers |
| // equal to the media sample size. |
| final long size = mExtractor.getSampleSize(); |
| if (size >= 0) { |
| final ByteBuffer inputBuffer = ByteBuffer.allocate((int)size); |
| final int red = mExtractor.readSampleData(inputBuffer, 0 /* offset */); // sic |
| if (red >= 0) { |
| assertEquals("position must be zero", 0, inputBuffer.position()); |
| assertEquals("limit must be read bytes", red, inputBuffer.limit()); |
| mExtractor.advance(); |
| return inputBuffer; |
| } |
| } |
| mSawOutputEOS = true; |
| return null; |
| } |
| |
| @Override |
| public void close() throws IOException { |
| if (mExtractor != null) { |
| mExtractor.release(); |
| mExtractor = null; |
| } |
| mFormat = null; |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| if (mExtractor != null) { |
| Log.w(TAG, "MediaExtractorStream wasn't closed"); |
| mExtractor.release(); |
| } |
| mFormat = null; |
| } |
| } |
| |
| @SmallTest |
| public void testFlacIdentity() throws Exception { |
| final int PCM_FRAMES = 1152 * 4; // FIXME: requires 4 flac frames to work with OMX codecs. |
| final int CHANNEL_COUNT = 2; |
| final int SAMPLES = PCM_FRAMES * CHANNEL_COUNT; |
| final int[] SAMPLE_RATES = {44100, 192000}; // ensure 192kHz supported. |
| |
| for (int sampleRate : SAMPLE_RATES) { |
| final MediaFormat format = new MediaFormat(); |
| format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_FLAC); |
| format.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate); |
| format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNEL_COUNT); |
| |
| Log.d(TAG, "Trying sample rate: " + sampleRate |
| + " channel count: " + CHANNEL_COUNT); |
| format.setInteger(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL, 5); |
| |
| // TODO: Add float mode when MediaExtractor supports float configuration. |
| final MediaCodecTest.PcmAudioBufferStream audioStream = |
| new MediaCodecTest.PcmAudioBufferStream( |
| SAMPLES, sampleRate, 1000 /* frequency */, 100 /* sweep */, |
| false /* useFloat */); |
| |
| final MediaCodecTest.MediaCodecStream rawToFlac = |
| new MediaCodecTest.MediaCodecStream( |
| new MediaCodecTest.ByteBufferInputStream(audioStream), |
| format, true /* encode */); |
| final MediaExtractorStream flacToRaw = |
| new MediaExtractorStream(MediaFormat.MIMETYPE_AUDIO_FLAC /* inMime */, |
| MediaFormat.MIMETYPE_AUDIO_RAW /* outMime */, rawToFlac); |
| |
| // Note: the existence of signed zero (as well as NAN) may make byte |
| // comparisons invalid for floating point output. In our case, since the |
| // floats come through integer to float conversion, it does not matter. |
| assertEquals("Audio data not identical after compression", |
| audioStream.sizeInBytes(), |
| MediaCodecTest.compareStreams(new MediaCodecTest.ByteBufferInputStream(flacToRaw), |
| new MediaCodecTest.ByteBufferInputStream( |
| new MediaCodecTest.PcmAudioBufferStream(audioStream)))); |
| } |
| } |
| |
| public void testFlacMovExtraction() throws Exception { |
| AssetFileDescriptor testFd = mResources.openRawResourceFd(R.raw.sinesweepalac); |
| |
| MediaExtractor extractor = new MediaExtractor(); |
| extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(), |
| testFd.getLength()); |
| testFd.close(); |
| extractor.selectTrack(0); |
| boolean lastAdvanceResult = true; |
| boolean lastReadResult = true; |
| ByteBuffer buf = ByteBuffer.allocate(2*1024*1024); |
| int totalSize = 0; |
| while(true) { |
| int n = extractor.readSampleData(buf, 0); |
| if (n > 0) { |
| totalSize += n; |
| } |
| if (!extractor.advance()) { |
| break; |
| } |
| } |
| assertTrue("could not read alac mov", totalSize > 0); |
| } |
| |
| private void doTestAdvance(int res) throws Exception { |
| AssetFileDescriptor testFd = mResources.openRawResourceFd(res); |
| |
| MediaExtractor extractor = new MediaExtractor(); |
| extractor.setDataSource(testFd.getFileDescriptor(), testFd.getStartOffset(), |
| testFd.getLength()); |
| testFd.close(); |
| extractor.selectTrack(0); |
| boolean lastAdvanceResult = true; |
| boolean lastReadResult = true; |
| ByteBuffer buf = ByteBuffer.allocate(2*1024*1024); |
| while(lastAdvanceResult || lastReadResult) { |
| int n = extractor.readSampleData(buf, 0); |
| if (lastAdvanceResult) { |
| // previous advance() was successful, so readSampleData() should succeed |
| assertTrue("readSampleData() failed after successful advance()", n >= 0); |
| assertTrue("getSampleTime() failed after succesful advance()", |
| extractor.getSampleTime() >= 0); |
| assertTrue("getSampleSize() failed after succesful advance()", |
| extractor.getSampleSize() >= 0); |
| assertTrue("getSampleTrackIndex() failed after succesful advance()", |
| extractor.getSampleTrackIndex() >= 0); |
| } else { |
| // previous advance() failed, so readSampleData() should fail too |
| assertTrue("readSampleData() succeeded after failed advance()", n < 0); |
| assertTrue("getSampleTime() succeeded after failed advance()", |
| extractor.getSampleTime() < 0); |
| assertTrue("getSampleSize() succeeded after failed advance()", |
| extractor.getSampleSize() < 0); |
| assertTrue("getSampleTrackIndex() succeeded after failed advance()", |
| extractor.getSampleTrackIndex() < 0); |
| } |
| lastReadResult = (n >= 0); |
| lastAdvanceResult = extractor.advance(); |
| } |
| extractor.release(); |
| } |
| |
| public void testAdvance() throws Exception { |
| // audio-only |
| doTestAdvance(R.raw.sinesweepm4a); |
| doTestAdvance(R.raw.sinesweepmp3lame); |
| doTestAdvance(R.raw.sinesweepmp3smpb); |
| doTestAdvance(R.raw.sinesweepwav); |
| doTestAdvance(R.raw.sinesweepflac); |
| doTestAdvance(R.raw.sinesweepogg); |
| doTestAdvance(R.raw.sinesweepoggmkv); |
| |
| // video-only |
| doTestAdvance(R.raw.swirl_144x136_mpeg4); |
| doTestAdvance(R.raw.video_640x360_mp4_hevc_450kbps_no_b); |
| |
| // audio+video |
| doTestAdvance(R.raw.video_480x360_mp4_h264_500kbps_30fps_aac_stereo_128kbps_44100hz); |
| doTestAdvance(R.raw.video_1280x720_mkv_h265_500kbps_25fps_aac_stereo_128kbps_44100hz); |
| } |
| } |