Add caching status APIs to MediaExtractorCompat

Implemented `getCachedDuration()` to provide an estimate of cached data in memory and `hasCacheReachedEndOfStream()` to indicate if the cache has reached the end of the stream.

Note: The Javadoc for the newly added methods closely follows that of the platform `MediaExtractor`. While the current implementation always uses a cache and therefore never returns `-1` from `getCachedDuration`, this leaves room for future changes should caching behavior or conditions evolve.
PiperOrigin-RevId: 695694460
This commit is contained in:
rohks 2024-11-12 05:31:18 -08:00 committed by Copybara-Service
parent ed288fca46
commit de0e08397e
2 changed files with 259 additions and 0 deletions

View File

@ -793,6 +793,215 @@ public class MediaExtractorCompatTest {
assertThat(mediaExtractorCompat.getDrmInitData()).isEqualTo(firstDrmInitData);
}
@Test
public void
getCachedDurationAndHasCacheReachedEndOfStream_withSingleTrackAndNoneSelected_returnsExpectedValues()
throws IOException {
TrackOutput[] outputs = new TrackOutput[1];
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputs[0] = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_VIDEO);
outputs[0].format(PLACEHOLDER_FORMAT_VIDEO);
extractorOutput.endTracks();
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSampleData(outputs[0], /* sampleData...= */ (byte) 1, (byte) 2, (byte) 3);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 0, /* size= */ 3, /* offset= */ 0);
return Extractor.RESULT_CONTINUE;
});
mediaExtractorCompat.setDataSource(PLACEHOLDER_URI, /* offset= */ 0);
// Sample is queued but discarded since no track is selected.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(0);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isTrue();
}
@Test
public void
getCachedDurationAndHasCacheReachedEndOfStream_withSingleTrackAndSelected_returnsExpectedValues()
throws IOException {
TrackOutput[] outputs = new TrackOutput[1];
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputs[0] = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_VIDEO);
outputs[0].format(PLACEHOLDER_FORMAT_VIDEO);
extractorOutput.endTracks();
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSampleData(outputs[0], /* sampleData...= */ (byte) 1, (byte) 2, (byte) 3);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 0, /* size= */ 1, /* offset= */ 2);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 100_000, /* size= */ 1, /* offset= */ 1);
outputSample(outputs[0], /* timeUs= */ 200_000, /* size= */ 1, /* offset= */ 0);
return Extractor.RESULT_CONTINUE;
});
mediaExtractorCompat.setDataSource(PLACEHOLDER_URI, /* offset= */ 0);
mediaExtractorCompat.selectTrack(0);
// First sample queued but not read; returns default duration for last sample.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(10_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Remaining two samples queued, first sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(210_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Second sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(110_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Final sample read; no remaining samples, so cached duration is zero and has reached end of
// stream.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(0);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isTrue();
}
@Test
public void
getCachedDurationAndHasCacheReachedEndOfStream_withMultipleTracksAndOneSelected_returnsExpectedValues()
throws IOException {
TrackOutput[] outputs = new TrackOutput[2];
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputs[0] = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_VIDEO);
outputs[0].format(PLACEHOLDER_FORMAT_VIDEO);
outputs[1] = extractorOutput.track(/* id= */ 1, C.TRACK_TYPE_AUDIO);
outputs[1].format(PLACEHOLDER_FORMAT_AUDIO);
extractorOutput.endTracks();
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSampleData(outputs[0], /* sampleData...= */ (byte) 1, (byte) 2);
outputSampleData(outputs[1], /* sampleData...= */ (byte) 4, (byte) 5, (byte) 6);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 0, /* size= */ 1, /* offset= */ 2);
outputSample(outputs[1], /* timeUs= */ 0, /* size= */ 1, /* offset= */ 2);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 100_000, /* size= */ 1, /* offset= */ 1);
outputSample(outputs[1], /* timeUs= */ 200_000, /* size= */ 1, /* offset= */ 1);
outputSample(outputs[1], /* timeUs= */ 300_000, /* size= */ 1, /* offset= */ 0);
return Extractor.RESULT_CONTINUE;
});
mediaExtractorCompat.setDataSource(PLACEHOLDER_URI, /* offset= */ 0);
mediaExtractorCompat.selectTrack(0);
// First two samples queued but not read; returns default duration for last sample.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(10_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// All samples queued, first sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(310_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Second sample read; remaining samples are from an unselected track and are discarded.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(0);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isTrue();
}
@Test
public void
getCachedDurationAndHasCacheReachedEndOfStream_withMultipleTracksAndAllSelected_returnsExpectedValues()
throws IOException {
TrackOutput[] outputs = new TrackOutput[2];
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputs[0] = extractorOutput.track(/* id= */ 0, C.TRACK_TYPE_VIDEO);
outputs[0].format(PLACEHOLDER_FORMAT_VIDEO);
outputs[1] = extractorOutput.track(/* id= */ 1, C.TRACK_TYPE_AUDIO);
outputs[1].format(PLACEHOLDER_FORMAT_AUDIO);
extractorOutput.endTracks();
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSampleData(outputs[0], /* sampleData...= */ (byte) 1, (byte) 2);
outputSampleData(outputs[1], /* sampleData...= */ (byte) 4, (byte) 5, (byte) 6);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 0, /* size= */ 1, /* offset= */ 2);
outputSample(outputs[1], /* timeUs= */ 0, /* size= */ 1, /* offset= */ 2);
return Extractor.RESULT_CONTINUE;
});
fakeExtractor.addReadAction(
(input, seekPosition) -> {
outputSample(outputs[0], /* timeUs= */ 100_000, /* size= */ 1, /* offset= */ 1);
outputSample(outputs[1], /* timeUs= */ 200_000, /* size= */ 1, /* offset= */ 1);
outputSample(outputs[1], /* timeUs= */ 300_000, /* size= */ 1, /* offset= */ 0);
return Extractor.RESULT_CONTINUE;
});
mediaExtractorCompat.setDataSource(PLACEHOLDER_URI, /* offset= */ 0);
mediaExtractorCompat.selectTrack(0);
mediaExtractorCompat.selectTrack(1);
// First two samples queued but not read; returns default duration for last sample.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(10_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
mediaExtractorCompat.advance();
// All samples queued, first and second sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(310_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Third sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(210_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Fourth sample read.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(110_000);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isFalse();
mediaExtractorCompat.advance();
// Final sample read; no remaining samples, so cached duration is zero and has reached end of
// stream.
assertThat(mediaExtractorCompat.getCachedDuration()).isEqualTo(0);
assertThat(mediaExtractorCompat.hasCacheReachedEndOfStream()).isTrue();
}
// Internal methods.
private void assertReadSample(int trackIndex, long timeUs, int size, byte... sampleData) {

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_PEEK;
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import static java.lang.Math.max;
import android.content.ContentResolver;
import android.content.Context;
@ -115,6 +116,14 @@ public final class MediaExtractorCompat {
/** See {@link MediaExtractor#SEEK_TO_CLOSEST_SYNC}. */
public static final int SEEK_TO_CLOSEST_SYNC = MediaExtractor.SEEK_TO_CLOSEST_SYNC;
/**
* A default duration added to the largest queued sample timestamp to provide a more realistic
* estimate of the cached duration. Since the duration of the last sample is unknown, this value
* prevents the duration of the last sample from being assumed as zero, which would otherwise make
* the estimated duration appear shorter than it actually is.
*/
private static final long DEFAULT_LAST_SAMPLE_DURATION_US = 10_000;
private static final String TAG = "MediaExtractorCompat";
private final ExtractorsFactory extractorsFactory;
@ -607,6 +616,47 @@ public final class MediaExtractorCompat {
return null;
}
/**
* Returns an estimate of how much data is presently cached in memory, expressed in microseconds,
* or -1 if this information is unavailable or not applicable (i.e., no cache exists).
*/
public long getCachedDuration() {
if (!advanceToSampleOrEndOfInput()) {
return 0;
}
long largestReadTimestampUs = Long.MIN_VALUE;
long largestQueuedTimestampUs = Long.MIN_VALUE;
for (int i = 0; i < tracks.size(); i++) {
MediaExtractorSampleQueue mediaExtractorSampleQueue = tracks.get(i).sampleQueue;
largestReadTimestampUs =
max(largestReadTimestampUs, mediaExtractorSampleQueue.getLargestReadTimestampUs());
largestQueuedTimestampUs =
max(largestQueuedTimestampUs, mediaExtractorSampleQueue.getLargestQueuedTimestampUs());
}
checkState(largestQueuedTimestampUs != Long.MIN_VALUE);
if (largestReadTimestampUs == largestQueuedTimestampUs) {
return 0;
}
if (largestReadTimestampUs == Long.MIN_VALUE) {
largestReadTimestampUs = 0;
}
return largestQueuedTimestampUs - largestReadTimestampUs + DEFAULT_LAST_SAMPLE_DURATION_US;
}
/**
* Returns {@code true} if data is being cached and the cache has reached the end of the data
* stream. This indicates that no additional data is currently available for caching, although a
* future seek may restart data fetching. This method only returns a meaningful result if {@link
* #getCachedDuration} indicates the presence of a cache (i.e., does not return -1).
*/
public boolean hasCacheReachedEndOfStream() {
return getCachedDuration() == 0;
}
@VisibleForTesting(otherwise = NONE)
public Allocator getAllocator() {
return allocator;