mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
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:
parent
ed288fca46
commit
de0e08397e
@ -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) {
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user