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:
rohks 2024-07-04 10:21:34 -07:00 committed by Copybara-Service
parent b7f317e650
commit adf1c7915d
5 changed files with 493 additions and 0 deletions

View File

@ -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:

View File

@ -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();
}
}
}
}

View File

@ -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() {}
}

View File

@ -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() {}
}

View File

@ -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);
}
}
}