diff --git a/libraries/datasource/src/androidTest/java/androidx/media3/datasource/MediaDataSourceAdapterContractTest.java b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/MediaDataSourceAdapterContractTest.java new file mode 100644 index 0000000000..d2dd77c654 --- /dev/null +++ b/libraries/datasource/src/androidTest/java/androidx/media3/datasource/MediaDataSourceAdapterContractTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 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.datasource; + +import static java.lang.Math.min; + +import android.media.MediaDataSource; +import android.net.Uri; +import androidx.media3.common.C; +import androidx.media3.test.utils.DataSourceContractTest; +import androidx.media3.test.utils.TestUtil; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** {@link DataSource} contract tests for {@link MediaDataSourceAdapter}. */ +@RunWith(AndroidJUnit4.class) +public class MediaDataSourceAdapterContractTest extends DataSourceContractTest { + + private static final byte[] DATA = TestUtil.buildTestData(20); + + @Override + protected DataSource createDataSource() { + MediaDataSource mediaDataSource = + new MediaDataSource() { + @Override + public int readAt(long position, byte[] buffer, int offset, int size) { + if (size == 0) { + return 0; + } + + if (position > getSize()) { + return C.RESULT_END_OF_INPUT; + } + + size = min(size, (int) (getSize() - position)); + + System.arraycopy(DATA, (int) position, buffer, offset, size); + return size; + } + + @Override + public long getSize() { + return DATA.length; + } + + @Override + public void close() {} + }; + return new MediaDataSourceAdapter(mediaDataSource, /* isNetwork= */ false); + } + + @Override + protected ImmutableList getTestResources() { + return ImmutableList.of( + new TestResource.Builder() + .setName("simple") + .setUri(Uri.EMPTY) + .setExpectedBytes(DATA) + .build()); + } + + @Override + protected Uri getNotFoundUri() { + throw new UnsupportedOperationException(); + } + + @Override + @Test + @Ignore + public void resourceNotFound() {} + + @Override + @Test + @Ignore + public void resourceNotFound_transferListenerCallbacks() {} + + @Override + @Test + @Ignore + public void getUri_resourceNotFound_returnsNullIfNotOpened() {} + + @Override + @Test + @Ignore + public void getResponseHeaders_resourceNotFound_isEmptyWhileNotOpen() {} +} diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/MediaDataSourceAdapter.java b/libraries/datasource/src/main/java/androidx/media3/datasource/MediaDataSourceAdapter.java new file mode 100644 index 0000000000..f72c2a64fe --- /dev/null +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/MediaDataSourceAdapter.java @@ -0,0 +1,126 @@ +/* + * Copyright 2024 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.datasource; + +import static java.lang.Math.min; + +import android.media.MediaDataSource; +import android.net.Uri; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.media3.common.C; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.util.UnstableApi; +import java.io.IOException; + +/** + * A {@link DataSource} for reading from a {@link MediaDataSource}. + * + *

An adapter that allows to read media data supplied by an implementation of {@link + * MediaDataSource}. + */ +@RequiresApi(23) +@UnstableApi +public class MediaDataSourceAdapter extends BaseDataSource { + + private final MediaDataSource mediaDataSource; + + @Nullable private Uri uri; + private long position; + private long bytesRemaining; + private boolean opened; + + /** + * Creates an instance. + * + * @param mediaDataSource The {@link MediaDataSource} from which to read. + * @param isNetwork Whether the data source loads data through a network. + */ + public MediaDataSourceAdapter(MediaDataSource mediaDataSource, boolean isNetwork) { + super(isNetwork); + this.mediaDataSource = mediaDataSource; + } + + @Override + public long open(DataSpec dataSpec) throws IOException { + uri = dataSpec.uri; + position = dataSpec.position; + transferInitializing(dataSpec); + + if (mediaDataSource.getSize() != C.LENGTH_UNSET && position > mediaDataSource.getSize()) { + throw new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE); + } + + if (mediaDataSource.getSize() == C.LENGTH_UNSET) { + bytesRemaining = C.LENGTH_UNSET; + } else { + bytesRemaining = mediaDataSource.getSize() - position; + } + + if (dataSpec.length != C.LENGTH_UNSET) { + bytesRemaining = + bytesRemaining == C.LENGTH_UNSET ? dataSpec.length : min(bytesRemaining, dataSpec.length); + } + + opened = true; + transferStarted(dataSpec); + return dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : bytesRemaining; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws DataSourceException { + if (length == 0) { + return 0; + } else if (bytesRemaining == 0) { + return C.RESULT_END_OF_INPUT; + } + + int bytesToRead = bytesRemaining == C.LENGTH_UNSET ? length : (int) min(bytesRemaining, length); + int bytesRead; + try { + bytesRead = mediaDataSource.readAt(position, buffer, offset, bytesToRead); + } catch (IOException e) { + throw new DataSourceException(e, PlaybackException.ERROR_CODE_IO_UNSPECIFIED); + } + + if (bytesRead == -1) { + return C.RESULT_END_OF_INPUT; + } + + position += bytesRead; + if (bytesRemaining != C.LENGTH_UNSET) { + bytesRemaining -= bytesRead; + } + + bytesTransferred(bytesRead); + return bytesRead; + } + + @Nullable + @Override + public Uri getUri() { + return uri; + } + + @Override + public void close() throws IOException { + uri = null; + if (opened) { + opened = false; + transferEnded(); + } + } +} diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java index a3c7b43e2b..026c6d76da 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/MediaExtractorCompat.java @@ -24,12 +24,14 @@ import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; +import android.media.MediaDataSource; import android.media.MediaExtractor; import android.media.MediaFormat; import android.net.Uri; import android.util.SparseArray; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -44,6 +46,7 @@ import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.FileDescriptorDataSource; +import androidx.media3.datasource.MediaDataSourceAdapter; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.mediacodec.MediaCodecUtil; import androidx.media3.exoplayer.source.SampleQueue; @@ -321,6 +324,24 @@ public final class MediaExtractorCompat { dataSourceFactory.createDataSource(), buildDataSpec(Uri.parse(path), /* position= */ 0)); } + /** + * Sets the data source using the media stream obtained from the given {@link MediaDataSource}. + * + * @param mediaDataSource The {@link MediaDataSource} to extract media from. + * @throws IOException If an error occurs while extracting the media. + * @throws UnrecognizedInputFormatException If none of the available extractors successfully + * sniffs the input. + * @throws IllegalStateException If this method is called twice on the same instance. + */ + @RequiresApi(23) + public void setDataSource(MediaDataSource mediaDataSource) throws IOException { + // MediaDataSourceAdapter is created privately here, so TransferListeners cannot be registered. + // Therefore, the isNetwork parameter is hardcoded to false as it has no effect. + MediaDataSourceAdapter mediaDataSourceAdapter = + new MediaDataSourceAdapter(mediaDataSource, /* isNetwork= */ false); + prepareDataSource(mediaDataSourceAdapter, buildDataSpec(Uri.EMPTY, /* position= */ 0)); + } + private void prepareDataSource(DataSource dataSource, DataSpec dataSpec) throws IOException { // Assert that this instance is not being re-prepared, which is not currently supported. Assertions.checkState(!hasBeenPrepared);