mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Add FileDescriptorDataSource
This is a new `DataSource` that can be used to read from a `FileDescriptor`. Limitations: - The provided file descriptor must be seekable via lseek. - There's no way to duplicate a file descriptor with an independent position (it would be necessary instead for the app to provide a new FD). Therefore this implementation will only work if there's one open data source for a given file descriptor at a time. PiperOrigin-RevId: 649443584
This commit is contained in:
parent
b7f317e650
commit
adf1c7915d
@ -16,6 +16,10 @@
|
||||
* Text:
|
||||
* Metadata:
|
||||
* Image:
|
||||
* DataSource:
|
||||
* Add `FileDescriptorDataSource`, a new `DataSource` that can be used to
|
||||
read from a `FileDescriptor`
|
||||
([#3757](https://github.com/google/ExoPlayer/issues/3757)).
|
||||
* DRM:
|
||||
* Effect:
|
||||
* Muxers:
|
||||
|
@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 org.junit.Assert.assertThrows;
|
||||
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.net.Uri;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/** Unit tests for {@link FileDescriptorDataSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public final class FileDescriptorDataSourceTest {
|
||||
|
||||
private static final byte[] DATA = TestUtil.buildTestData(20);
|
||||
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private static final String ASSET_PATH = "media/mp3/1024_incrementing_bytes.mp3";
|
||||
|
||||
@Test
|
||||
public void testReadViaFileDescriptor() throws Exception {
|
||||
File file = tempFolder.newFile();
|
||||
Files.write(Paths.get(file.getAbsolutePath()), DATA);
|
||||
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
DataSource dataSource =
|
||||
new FileDescriptorDataSource(inputStream.getFD(), /* offset= */ 0, DATA.length);
|
||||
|
||||
TestUtil.assertDataSourceContent(
|
||||
dataSource, new DataSpec(Uri.EMPTY), DATA, /* expectKnownLength= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadViaFileDescriptorWithOffset() throws Exception {
|
||||
File file = tempFolder.newFile();
|
||||
Files.write(Paths.get(file.getAbsolutePath()), DATA);
|
||||
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
DataSource dataSource =
|
||||
new FileDescriptorDataSource(inputStream.getFD(), /* offset= */ 0, DATA.length);
|
||||
DataSpec dataSpec = new DataSpec(Uri.EMPTY, /* position= */ 10, C.LENGTH_UNSET);
|
||||
byte[] expectedData = Arrays.copyOfRange(DATA, /* position= */ 10, DATA.length);
|
||||
|
||||
TestUtil.assertDataSourceContent(
|
||||
dataSource, dataSpec, expectedData, /* expectKnownLength= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadViaAssetFileDescriptor() throws Exception {
|
||||
try (AssetFileDescriptor afd =
|
||||
ApplicationProvider.getApplicationContext().getAssets().openFd(ASSET_PATH)) {
|
||||
DataSource dataSource =
|
||||
new FileDescriptorDataSource(
|
||||
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
byte[] expectedData =
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), ASSET_PATH);
|
||||
|
||||
TestUtil.assertDataSourceContent(
|
||||
dataSource, new DataSpec(Uri.EMPTY), expectedData, /* expectKnownLength= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadViaAssetFileDescriptorWithOffset() throws Exception {
|
||||
try (AssetFileDescriptor afd =
|
||||
ApplicationProvider.getApplicationContext().getAssets().openFd(ASSET_PATH)) {
|
||||
DataSource dataSource =
|
||||
new FileDescriptorDataSource(
|
||||
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
DataSpec dataSpec = new DataSpec(Uri.EMPTY, /* position= */ 100, C.LENGTH_UNSET);
|
||||
byte[] data = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), ASSET_PATH);
|
||||
byte[] expectedData = Arrays.copyOfRange(data, /* position= */ 100, data.length);
|
||||
|
||||
TestUtil.assertDataSourceContent(
|
||||
dataSource, dataSpec, expectedData, /* expectKnownLength= */ true);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConcurrentUseOfSameFileDescriptorFails() throws Exception {
|
||||
try (AssetFileDescriptor afd =
|
||||
ApplicationProvider.getApplicationContext().getAssets().openFd(ASSET_PATH)) {
|
||||
DataSpec dataSpec = new DataSpec(Uri.EMPTY, /* position= */ 100, C.LENGTH_UNSET);
|
||||
DataSource dataSource1 =
|
||||
new FileDescriptorDataSource(
|
||||
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
dataSource1.open(dataSpec);
|
||||
DataSource dataSource2 =
|
||||
new FileDescriptorDataSource(
|
||||
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
|
||||
// Opening a data source with the same file descriptor is expected to fail.
|
||||
assertThrows(DataSourceException.class, () -> dataSource2.open(dataSpec));
|
||||
|
||||
if (dataSource1 != null) {
|
||||
dataSource1.close();
|
||||
}
|
||||
if (dataSource2 != null) {
|
||||
dataSource2.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 android.content.res.AssetFileDescriptor;
|
||||
import android.net.Uri;
|
||||
import androidx.media3.test.utils.DataSourceContractTest;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
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 FileDescriptorDataSource} using {@link
|
||||
* AssetFileDescriptor}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FileDescriptorDataSourceUsingAssetFileDescriptorContractTest
|
||||
extends DataSourceContractTest {
|
||||
|
||||
private static final String ASSET_PATH = "media/mp3/1024_incrementing_bytes.mp3";
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() throws Exception {
|
||||
AssetFileDescriptor afd =
|
||||
ApplicationProvider.getApplicationContext().getAssets().openFd(ASSET_PATH);
|
||||
return new FileDescriptorDataSource(
|
||||
afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableList<TestResource> getTestResources() throws Exception {
|
||||
return ImmutableList.of(
|
||||
new TestResource.Builder()
|
||||
.setName("simple")
|
||||
.setUri(Uri.EMPTY)
|
||||
.setExpectedBytes(
|
||||
TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), ASSET_PATH))
|
||||
.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() {}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* 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 android.net.Uri;
|
||||
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 java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import org.junit.After;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
/**
|
||||
* {@link DataSource} contract tests for {@link FileDescriptorDataSource} using {@link
|
||||
* FileDescriptor}.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class FileDescriptorDataSourceUsingFileDescriptorContractTest
|
||||
extends DataSourceContractTest {
|
||||
|
||||
private static final byte[] DATA = TestUtil.buildTestData(20);
|
||||
@Rule public final TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
private FileInputStream inputStream;
|
||||
|
||||
@After
|
||||
public void cleanUp() throws IOException {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DataSource createDataSource() throws Exception {
|
||||
File file = tempFolder.newFile();
|
||||
Files.write(Paths.get(file.getAbsolutePath()), DATA);
|
||||
inputStream = new FileInputStream(file);
|
||||
return new FileDescriptorDataSource(inputStream.getFD(), /* offset= */ 0, DATA.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImmutableList<TestResource> getTestResources() throws Exception {
|
||||
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() {}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* 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 androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Util.castNonNull;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
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 com.google.common.collect.Sets;
|
||||
import java.io.EOFException;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A {@link DataSource} for reading from a {@link FileDescriptor}.
|
||||
*
|
||||
* <p>Due to limitations of file descriptors, it's only possible to have one {@link DataSource}
|
||||
* created for a given file descriptor open at a time. The provided file descriptor must be
|
||||
* seekable.
|
||||
*
|
||||
* <p>Unlike typical {@link DataSource} instances, each instance of this class can only read from a
|
||||
* single {@link FileDescriptor}. Additionally, the {@link DataSpec#uri} passed to {@link
|
||||
* #open(DataSpec)} is not actually used for reading data. Instead, the underlying {@link
|
||||
* FileDescriptor} is used for all read operations.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
@UnstableApi
|
||||
public class FileDescriptorDataSource extends BaseDataSource {
|
||||
|
||||
// Track file descriptors currently in use to fail fast if an attempt is made to re-use one.
|
||||
private static final Set<FileDescriptor> inUseFileDescriptors = Sets.newConcurrentHashSet();
|
||||
|
||||
private final FileDescriptor fileDescriptor;
|
||||
private final long offset;
|
||||
private final long length;
|
||||
|
||||
@Nullable private Uri uri;
|
||||
@Nullable private InputStream inputStream;
|
||||
private long bytesRemaining;
|
||||
private boolean opened;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param fileDescriptor The file descriptor from which to read.
|
||||
* @param offset The start offset of data to read.
|
||||
* @param length The length of data to read, or {@link C#LENGTH_UNSET} if not known.
|
||||
*/
|
||||
public FileDescriptorDataSource(FileDescriptor fileDescriptor, long offset, long length) {
|
||||
super(/* isNetwork= */ false);
|
||||
this.fileDescriptor = checkNotNull(fileDescriptor);
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long open(DataSpec dataSpec) throws DataSourceException {
|
||||
if (!inUseFileDescriptors.add(fileDescriptor)) {
|
||||
throw new DataSourceException(
|
||||
new IllegalStateException("Attempted to re-use an already in-use file descriptor"),
|
||||
PlaybackException.ERROR_CODE_INVALID_STATE);
|
||||
}
|
||||
|
||||
if (dataSpec.position > length) {
|
||||
throw new DataSourceException(PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE);
|
||||
}
|
||||
|
||||
uri = dataSpec.uri;
|
||||
transferInitializing(dataSpec);
|
||||
seekFileDescriptor(fileDescriptor, offset + dataSpec.position);
|
||||
inputStream = new FileInputStream(fileDescriptor);
|
||||
|
||||
if (dataSpec.length != C.LENGTH_UNSET) {
|
||||
bytesRemaining =
|
||||
length != C.LENGTH_UNSET
|
||||
? min(dataSpec.length, length - dataSpec.position)
|
||||
: dataSpec.length;
|
||||
} else {
|
||||
bytesRemaining = length != C.LENGTH_UNSET ? length - dataSpec.position : C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
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 = castNonNull(inputStream).read(buffer, offset, bytesToRead);
|
||||
} catch (IOException e) {
|
||||
throw new DataSourceException(e, PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
}
|
||||
if (bytesRead == -1) {
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
throw new DataSourceException(
|
||||
new EOFException(
|
||||
String.format(
|
||||
"Attempted to read %d bytes starting at position %d, but reached end of the"
|
||||
+ " file before reading sufficient data.",
|
||||
this.length, this.offset)),
|
||||
PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
}
|
||||
return C.RESULT_END_OF_INPUT;
|
||||
}
|
||||
if (bytesRemaining != C.LENGTH_UNSET) {
|
||||
bytesRemaining -= bytesRead;
|
||||
}
|
||||
bytesTransferred(bytesRead);
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws DataSourceException {
|
||||
uri = null;
|
||||
inUseFileDescriptors.remove(fileDescriptor);
|
||||
try {
|
||||
if (inputStream != null) {
|
||||
inputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DataSourceException(e, PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
} finally {
|
||||
inputStream = null;
|
||||
if (opened) {
|
||||
opened = false;
|
||||
transferEnded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void seekFileDescriptor(FileDescriptor fileDescriptor, long position)
|
||||
throws DataSourceException {
|
||||
try {
|
||||
Os.lseek(fileDescriptor, position, /* whence= */ OsConstants.SEEK_SET);
|
||||
} catch (ErrnoException e) {
|
||||
throw new DataSourceException(e, PlaybackException.ERROR_CODE_IO_UNSPECIFIED);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user