diff --git a/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorContractTest.java b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorContractTest.java new file mode 100644 index 0000000000..ac5b2f1215 --- /dev/null +++ b/libraries/exoplayer/src/androidTest/java/androidx/media3/exoplayer/MediaExtractorContractTest.java @@ -0,0 +1,482 @@ +/* + * Copyright 2025 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 androidx.media3.exoplayer; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.MediaCodec; +import android.media.MediaDataSource; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.metrics.LogSessionId; +import android.net.Uri; +import android.os.PersistableBundle; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.base.Function; +import com.google.common.collect.ImmutableList; +import java.io.FileDescriptor; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.UUID; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** + * Contract tests for verifying consistent behavior across {@link MediaExtractor} implementations. + * + *

This tests both platform {@link MediaExtractor} and its compat implementation {@link + * MediaExtractorCompat}. + */ +@RunWith(Parameterized.class) +public class MediaExtractorContractTest { + + @Parameters(name = "{0}") + public static ImmutableList> + mediaExtractorProxyFactories() { + return ImmutableList.of( + new Function() { + @Override + public MediaExtractorProxy apply(Context context) { + return new FrameworkMediaExtractorProxy(); + } + + @Override + public String toString() { + return FrameworkMediaExtractorProxy.class.getSimpleName(); + } + }, + new Function() { + @Override + public MediaExtractorProxy apply(Context context) { + return new CompatMediaExtractorProxy(context); + } + + @Override + public String toString() { + return CompatMediaExtractorProxy.class.getSimpleName(); + } + }); + } + + @Parameter public Function mediaExtractorProxyFactory; + + private MediaExtractorProxy mediaExtractorProxy; + + @Before + public void setUp() { + mediaExtractorProxy = + mediaExtractorProxyFactory.apply(ApplicationProvider.getApplicationContext()); + } + + @After + public void tearDown() { + mediaExtractorProxy.release(); + } + + @Test + public void setDataSource_withAssetFileDescriptor_returnsCorrectTrackCount() throws IOException { + AssetFileDescriptor afd = + ApplicationProvider.getApplicationContext().getAssets().openFd("media/mp4/sample.mp4"); + + mediaExtractorProxy.setDataSource(afd); + + assertThat(mediaExtractorProxy.getTrackCount()).isEqualTo(2); + } + + private static class FrameworkMediaExtractorProxy implements MediaExtractorProxy { + + private final MediaExtractor mediaExtractor; + + public FrameworkMediaExtractorProxy() { + this.mediaExtractor = new MediaExtractor(); + } + + @Override + public boolean advance() { + return mediaExtractor.advance(); + } + + @Override + public long getCachedDuration() { + return mediaExtractor.getCachedDuration(); + } + + @Override + @RequiresApi(24) + public Object getDrmInitData() { + return mediaExtractor.getDrmInitData(); + } + + @Override + @RequiresApi(31) + public LogSessionId getLogSessionId() { + return mediaExtractor.getLogSessionId(); + } + + @Override + @RequiresApi(26) + public PersistableBundle getMetrics() { + return mediaExtractor.getMetrics(); + } + + @Override + public Map getPsshInfo() { + return mediaExtractor.getPsshInfo(); + } + + @Override + public boolean getSampleCryptoInfo(MediaCodec.CryptoInfo info) { + return mediaExtractor.getSampleCryptoInfo(info); + } + + @Override + public int getSampleFlags() { + return mediaExtractor.getSampleFlags(); + } + + @Override + @RequiresApi(28) + public long getSampleSize() { + return mediaExtractor.getSampleSize(); + } + + @Override + public long getSampleTime() { + return mediaExtractor.getSampleTime(); + } + + @Override + public int getSampleTrackIndex() { + return mediaExtractor.getSampleTrackIndex(); + } + + @Override + public int getTrackCount() { + return mediaExtractor.getTrackCount(); + } + + @Override + public MediaFormat getTrackFormat(int trackIndex) { + return mediaExtractor.getTrackFormat(trackIndex); + } + + @Override + public boolean hasCacheReachedEndOfStream() { + return mediaExtractor.hasCacheReachedEndOfStream(); + } + + @Override + public int readSampleData(ByteBuffer buffer, int offset) { + return mediaExtractor.readSampleData(buffer, offset); + } + + @Override + public void release() { + mediaExtractor.release(); + } + + @Override + public void seekTo(long timeUs, int mode) { + mediaExtractor.seekTo(timeUs, mode); + } + + @Override + public void selectTrack(int trackIndex) { + mediaExtractor.selectTrack(trackIndex); + } + + @Override + @RequiresApi(24) + public void setDataSource(AssetFileDescriptor assetFileDescriptor) throws IOException { + mediaExtractor.setDataSource(assetFileDescriptor); + } + + @Override + public void setDataSource(FileDescriptor fileDescriptor) throws IOException { + mediaExtractor.setDataSource(fileDescriptor); + } + + @Override + @RequiresApi(23) + public void setDataSource(MediaDataSource mediaDataSource) throws IOException { + mediaExtractor.setDataSource(mediaDataSource); + } + + @Override + public void setDataSource(Context context, Uri uri, @Nullable Map headers) + throws IOException { + mediaExtractor.setDataSource(context, uri, headers); + } + + @Override + public void setDataSource(FileDescriptor fileDescriptor, long offset, long length) + throws IOException { + mediaExtractor.setDataSource(fileDescriptor, offset, length); + } + + @Override + public void setDataSource(String path) throws IOException { + mediaExtractor.setDataSource(path); + } + + @Override + public void setDataSource(String path, @Nullable Map headers) + throws IOException { + mediaExtractor.setDataSource(path, headers); + } + + @Override + @RequiresApi(31) + public void setLogSessionId(LogSessionId logSessionId) { + mediaExtractor.setLogSessionId(logSessionId); + } + + @Override + public void unselectTrack(int trackIndex) { + mediaExtractor.unselectTrack(trackIndex); + } + } + + private static class CompatMediaExtractorProxy implements MediaExtractorProxy { + + private final MediaExtractorCompat mediaExtractorCompat; + + public CompatMediaExtractorProxy(Context context) { + this.mediaExtractorCompat = new MediaExtractorCompat(context); + } + + @Override + public boolean advance() { + return mediaExtractorCompat.advance(); + } + + @Override + public long getCachedDuration() { + return mediaExtractorCompat.getCachedDuration(); + } + + @Override + @RequiresApi(24) + public Object getDrmInitData() { + return mediaExtractorCompat.getDrmInitData(); + } + + @Override + @RequiresApi(31) + public LogSessionId getLogSessionId() { + return mediaExtractorCompat.getLogSessionId(); + } + + @Override + @RequiresApi(26) + public PersistableBundle getMetrics() { + return mediaExtractorCompat.getMetrics(); + } + + @Override + public Map getPsshInfo() { + return mediaExtractorCompat.getPsshInfo(); + } + + @Override + public boolean getSampleCryptoInfo(MediaCodec.CryptoInfo info) { + return mediaExtractorCompat.getSampleCryptoInfo(info); + } + + @Override + public int getSampleFlags() { + return mediaExtractorCompat.getSampleFlags(); + } + + @Override + @RequiresApi(28) + public long getSampleSize() { + return mediaExtractorCompat.getSampleSize(); + } + + @Override + public long getSampleTime() { + return mediaExtractorCompat.getSampleTime(); + } + + @Override + public int getSampleTrackIndex() { + return mediaExtractorCompat.getSampleTrackIndex(); + } + + @Override + public int getTrackCount() { + return mediaExtractorCompat.getTrackCount(); + } + + @Override + public MediaFormat getTrackFormat(int trackIndex) { + return mediaExtractorCompat.getTrackFormat(trackIndex); + } + + @Override + public boolean hasCacheReachedEndOfStream() { + return mediaExtractorCompat.hasCacheReachedEndOfStream(); + } + + @Override + public int readSampleData(ByteBuffer buffer, int offset) { + return mediaExtractorCompat.readSampleData(buffer, offset); + } + + @Override + public void release() { + mediaExtractorCompat.release(); + } + + @Override + public void seekTo(long timeUs, int mode) { + mediaExtractorCompat.seekTo(timeUs, mode); + } + + @Override + public void selectTrack(int trackIndex) { + mediaExtractorCompat.selectTrack(trackIndex); + } + + @Override + @RequiresApi(24) + public void setDataSource(AssetFileDescriptor assetFileDescriptor) throws IOException { + mediaExtractorCompat.setDataSource(assetFileDescriptor); + } + + @Override + public void setDataSource(FileDescriptor fileDescriptor) throws IOException { + mediaExtractorCompat.setDataSource(fileDescriptor); + } + + @Override + @RequiresApi(23) + public void setDataSource(MediaDataSource mediaDataSource) throws IOException { + mediaExtractorCompat.setDataSource(mediaDataSource); + } + + @Override + public void setDataSource(Context context, Uri uri, @Nullable Map headers) + throws IOException { + mediaExtractorCompat.setDataSource(context, uri, headers); + } + + @Override + public void setDataSource(FileDescriptor fileDescriptor, long offset, long length) + throws IOException { + mediaExtractorCompat.setDataSource(fileDescriptor, offset, length); + } + + @Override + public void setDataSource(String path) throws IOException { + mediaExtractorCompat.setDataSource(path); + } + + @Override + public void setDataSource(String path, @Nullable Map headers) + throws IOException { + mediaExtractorCompat.setDataSource(path, headers); + } + + @Override + @RequiresApi(31) + public void setLogSessionId(LogSessionId logSessionId) { + mediaExtractorCompat.setLogSessionId(logSessionId); + } + + @Override + public void unselectTrack(int trackIndex) { + mediaExtractorCompat.unselectTrack(trackIndex); + } + } + + @SuppressWarnings("unused") // TODO(b/392566318): Remove after adding tests for all methods. + private interface MediaExtractorProxy { + + boolean advance(); + + long getCachedDuration(); + + @RequiresApi(24) + Object getDrmInitData(); + + @RequiresApi(31) + LogSessionId getLogSessionId(); + + @RequiresApi(26) + PersistableBundle getMetrics(); + + Map getPsshInfo(); + + boolean getSampleCryptoInfo(MediaCodec.CryptoInfo info); + + int getSampleFlags(); + + @RequiresApi(28) + long getSampleSize(); + + long getSampleTime(); + + int getSampleTrackIndex(); + + int getTrackCount(); + + MediaFormat getTrackFormat(int trackIndex); + + boolean hasCacheReachedEndOfStream(); + + int readSampleData(ByteBuffer buffer, int offset); + + void release(); + + void seekTo(long timeUs, @MediaExtractorCompat.SeekMode int mode); + + void selectTrack(int trackIndex); + + @RequiresApi(24) + void setDataSource(AssetFileDescriptor assetFileDescriptor) throws IOException; + + void setDataSource(FileDescriptor fileDescriptor) throws IOException; + + @RequiresApi(23) + void setDataSource(MediaDataSource mediaDataSource) throws IOException; + + void setDataSource(Context context, Uri uri, @Nullable Map headers) + throws IOException; + + void setDataSource(FileDescriptor fileDescriptor, long offset, long length) throws IOException; + + void setDataSource(String path) throws IOException; + + void setDataSource(String path, @Nullable Map headers) throws IOException; + + @RequiresApi(31) + void setLogSessionId(LogSessionId logSessionId); + + void unselectTrack(int trackIndex); + } +}