Add APIs to set data source using content URI, file path or HTTP URL

Added three `setDataSource` APIs in `MediaExtractorCompat`:
- `setDataSource(Context context, Uri uri, @Nullable Map<String, String> headers)` to set data source with a content URI and optional headers.
- `setDataSource(String path)` to set data source using a file path or HTTP URL.
- `setDataSource(String path, @Nullable Map<String, String> headers)` to set data source using a file path or HTTP URL with optional headers.

PiperOrigin-RevId: 657563973
This commit is contained in:
rohks 2024-07-30 06:10:00 -07:00 committed by Copybara-Service
parent ca5a26a409
commit 867e9ea2da
3 changed files with 164 additions and 10 deletions

View File

@ -21,6 +21,7 @@ import static androidx.media3.exoplayer.source.SampleStream.FLAG_OMIT_SAMPLE_DAT
import static androidx.media3.exoplayer.source.SampleStream.FLAG_PEEK; import static androidx.media3.exoplayer.source.SampleStream.FLAG_PEEK;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.content.res.AssetFileDescriptor; import android.content.res.AssetFileDescriptor;
import android.media.MediaExtractor; import android.media.MediaExtractor;
@ -70,6 +71,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import java.io.EOFException; import java.io.EOFException;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -77,6 +79,7 @@ import java.nio.ByteBuffer;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf; import org.checkerframework.checker.nullness.qual.EnsuresNonNullIf;
@ -132,6 +135,7 @@ public final class MediaExtractorCompat {
@Nullable private SeekMap seekMap; @Nullable private SeekMap seekMap;
private boolean tracksEnded; private boolean tracksEnded;
private int upstreamFormatsCount; private int upstreamFormatsCount;
@Nullable private Map<String, String> httpRequestHeaders;
/** Creates a new instance. */ /** Creates a new instance. */
public MediaExtractorCompat(Context context) { public MediaExtractorCompat(Context context) {
@ -151,6 +155,10 @@ public final class MediaExtractorCompat {
* <li>{@link #setDataSource(FileDescriptor)} * <li>{@link #setDataSource(FileDescriptor)}
* <li>{@link #setDataSource(FileDescriptor, long, long)} * <li>{@link #setDataSource(FileDescriptor, long, long)}
* </ul> * </ul>
*
* <p>Note: The {@link DataSource.Factory} provided may not be used to generate {@link DataSource}
* when setting data source using method {@link #setDataSource(Context, Uri, Map)} as the behavior
* depends on the fallthrough logic related to the scheme of the provided URI.
*/ */
public MediaExtractorCompat( public MediaExtractorCompat(
ExtractorsFactory extractorsFactory, DataSource.Factory dataSourceFactory) { ExtractorsFactory extractorsFactory, DataSource.Factory dataSourceFactory) {
@ -241,8 +249,76 @@ public final class MediaExtractorCompat {
throws IOException { throws IOException {
FileDescriptorDataSource fileDescriptorDataSource = FileDescriptorDataSource fileDescriptorDataSource =
new FileDescriptorDataSource(fileDescriptor, offset, length); new FileDescriptorDataSource(fileDescriptor, offset, length);
DataSpec dataSpec = new DataSpec(Uri.EMPTY); prepareDataSource(fileDescriptorDataSource, buildDataSpec(Uri.EMPTY, /* position= */ 0));
prepareDataSource(fileDescriptorDataSource, dataSpec); }
/**
* Sets the data source using the media stream obtained from the given {@linkplain Uri content
* URI} and optional HTTP request headers.
*
* @param context The {@link Context} used to resolve the {@link Uri}.
* @param uri The {@linkplain Uri content URI} of the media to extract from.
* @param headers An optional {@link Map} of HTTP request headers to include when fetching the
* data, or {@code null} if no headers are to be included.
* @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.
*/
public void setDataSource(Context context, Uri uri, @Nullable Map<String, String> headers)
throws IOException {
String scheme = uri.getScheme();
String path = uri.getPath();
if ((scheme == null || scheme.equals("file")) && path != null) {
// If the URI scheme is null or file, treat it as a local file path
setDataSource(path);
return;
}
ContentResolver resolver = context.getContentResolver();
try (AssetFileDescriptor assetFileDescriptor = resolver.openAssetFileDescriptor(uri, "r")) {
if (assetFileDescriptor != null) {
// If the URI points to a content provider resource, use the AssetFileDescriptor
setDataSource(assetFileDescriptor);
return;
}
} catch (SecurityException | FileNotFoundException e) {
// Fall back to using the URI as a string if the file is not found or the mode is invalid
}
// Assume the URI is an HTTP URL and use it with optional headers
setDataSource(uri.toString(), headers);
}
/**
* Sets the data source using the media stream obtained from the given file path or HTTP URL.
*
* @param path The path of the file, or the HTTP URL, 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.
*/
public void setDataSource(String path) throws IOException {
setDataSource(path, /* headers= */ null);
}
/**
* Sets the data source using the media stream obtained from the given file path or HTTP URL, with
* optional HTTP request headers.
*
* @param path The path of the file, or the HTTP URL, to extract media from.
* @param headers An optional {@link Map} of HTTP request headers to include when fetching the
* data, or {@code null} if no headers are to be included.
* @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.
*/
public void setDataSource(String path, @Nullable Map<String, String> headers) throws IOException {
httpRequestHeaders = headers;
prepareDataSource(
dataSourceFactory.createDataSource(), buildDataSpec(Uri.parse(path), /* position= */ 0));
} }
private void prepareDataSource(DataSource dataSource, DataSpec dataSpec) throws IOException { private void prepareDataSource(DataSource dataSource, DataSpec dataSpec) throws IOException {
@ -658,13 +734,18 @@ public final class MediaExtractorCompat {
* <p>The created {@link DataSpec} disables caching if the content length cannot be resolved, * <p>The created {@link DataSpec} disables caching if the content length cannot be resolved,
* since this is indicative of a progressive live stream. * since this is indicative of a progressive live stream.
*/ */
private static DataSpec buildDataSpec(Uri uri, long position) { private DataSpec buildDataSpec(Uri uri, long position) {
return new DataSpec.Builder() DataSpec.Builder dataSpec =
new DataSpec.Builder()
.setUri(uri) .setUri(uri)
.setPosition(position) .setPosition(position)
.setFlags( .setFlags(
DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION) DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN
.build(); | DataSpec.FLAG_ALLOW_CACHE_FRAGMENTATION);
if (httpRequestHeaders != null) {
dataSpec.setHttpRequestHeaders(httpRequestHeaders);
}
return dataSpec.build();
} }
private final class ExtractorOutputImpl implements ExtractorOutput { private final class ExtractorOutputImpl implements ExtractorOutput {

View File

@ -14,6 +14,15 @@
limitations under the License. limitations under the License.
--> -->
<manifest package="androidx.media3.exoplayer.test"> <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="androidx.media3.exoplayer.test">
<uses-sdk/> <uses-sdk/>
<application
android:allowBackup="false"
android:usesCleartextTraffic="true"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
<provider
android:authorities="androidx.media3.test.utils.AssetContentProvider"
android:name="androidx.media3.test.utils.AssetContentProvider"/>
</application>
</manifest> </manifest>

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.net.Uri; import android.net.Uri;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -36,23 +37,37 @@ import androidx.media3.extractor.SeekMap;
import androidx.media3.extractor.SeekMap.SeekPoints; import androidx.media3.extractor.SeekMap.SeekPoints;
import androidx.media3.extractor.SeekPoint; import androidx.media3.extractor.SeekPoint;
import androidx.media3.extractor.TrackOutput; import androidx.media3.extractor.TrackOutput;
import androidx.media3.test.utils.AssetContentProvider;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okio.Buffer;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
/** Tests for {@link MediaExtractorCompat}. */ /** Tests for {@link MediaExtractorCompat}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public class MediaExtractorCompatTest { public class MediaExtractorCompatTest {
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
/** /**
* Placeholder data URI which saves us from mocking the data source which MediaExtractorCompat * Placeholder data URI which saves us from mocking the data source which MediaExtractorCompat
* uses. * uses.
@ -484,6 +499,55 @@ public class MediaExtractorCompatTest {
assertThat(mediaExtractorCompat.getSampleTime()).isEqualTo(7); assertThat(mediaExtractorCompat.getSampleTime()).isEqualTo(7);
} }
@Test
public void
setDataSourceUsingMethodExpectingContentUri_useAbsoluteFilePathAsUri_setsTrackCountCorrectly()
throws IOException {
Context context = ApplicationProvider.getApplicationContext();
byte[] fileData = TestUtil.getByteArray(context, /* fileName= */ "media/mp4/sample.mp4");
File file = tempFolder.newFile();
Files.write(Paths.get(file.getAbsolutePath()), fileData);
MediaExtractorCompat mediaExtractorCompat = new MediaExtractorCompat(context);
mediaExtractorCompat.setDataSource(
context, Uri.parse(file.getAbsolutePath()), /* headers= */ null);
assertThat(mediaExtractorCompat.getTrackCount()).isEqualTo(2);
}
@Test
public void
setDataSourceUsingMethodExpectingContentUri_useHttpUri_setsTrackCountAndHeadersCorrectly()
throws Exception {
Context context = ApplicationProvider.getApplicationContext();
byte[] fileData = TestUtil.getByteArray(context, /* fileName= */ "media/mp4/sample.mp4");
try (MockWebServer mockWebServer = new MockWebServer()) {
mockWebServer.enqueue(new MockResponse().setBody(new Buffer().write(fileData)));
Map<String, String> headers = new HashMap<>();
headers.put("k", "v");
MediaExtractorCompat mediaExtractorCompat = new MediaExtractorCompat(context);
mediaExtractorCompat.setDataSource(
context, Uri.parse(mockWebServer.url("/test-path").toString()), headers);
assertThat(mediaExtractorCompat.getTrackCount()).isEqualTo(2);
assertThat(mockWebServer.takeRequest().getHeaders().get("k")).isEqualTo("v");
}
}
@Test
public void setDataSourceUsingMethodExpectingContentUri_useContentUri_setsTrackCountCorrectly()
throws IOException {
Context context = ApplicationProvider.getApplicationContext();
Uri contentUri =
AssetContentProvider.buildUri(/* filePath= */ "media/mp4/sample.mp4", /* pipeMode= */ true);
MediaExtractorCompat mediaExtractorCompat = new MediaExtractorCompat(context);
mediaExtractorCompat.setDataSource(context, contentUri, /* headers= */ null);
assertThat(mediaExtractorCompat.getTrackCount()).isEqualTo(2);
}
// Internal methods. // Internal methods.
private void assertReadSample(int trackIndex, long timeUs, byte... sampleData) { private void assertReadSample(int trackIndex, long timeUs, byte... sampleData) {