Move TimeOffsetMediaPeriod to its own class
This makes it reusable for other MediaSource/Periods in the same package. Issue: google/ExoPlayer#11226 PiperOrigin-RevId: 563687935
This commit is contained in:
parent
2ab5841ac2
commit
f2b1d0cfa3
@ -18,8 +18,10 @@ package androidx.media3.exoplayer;
|
|||||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||||
|
|
||||||
/** Information about the player state when loading is started or continued. */
|
/** Information about the player state when loading is started or continued. */
|
||||||
@ -133,4 +135,23 @@ public final class LoadingInfo {
|
|||||||
&& realtimeMs != C.TIME_UNSET
|
&& realtimeMs != C.TIME_UNSET
|
||||||
&& lastRebufferRealtimeMs >= realtimeMs;
|
&& lastRebufferRealtimeMs >= realtimeMs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(@Nullable Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof LoadingInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LoadingInfo that = (LoadingInfo) o;
|
||||||
|
return playbackPositionUs == that.playbackPositionUs
|
||||||
|
&& playbackSpeed == that.playbackSpeed
|
||||||
|
&& lastRebufferRealtimeMs == that.lastRebufferRealtimeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(playbackPositionUs, playbackSpeed, lastRebufferRealtimeMs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,9 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.StreamKey;
|
|
||||||
import androidx.media3.common.TrackGroup;
|
import androidx.media3.common.TrackGroup;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.NullableType;
|
import androidx.media3.common.util.NullableType;
|
||||||
import androidx.media3.decoder.DecoderInputBuffer;
|
|
||||||
import androidx.media3.exoplayer.FormatHolder;
|
|
||||||
import androidx.media3.exoplayer.LoadingInfo;
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
import androidx.media3.exoplayer.SeekParameters;
|
import androidx.media3.exoplayer.SeekParameters;
|
||||||
import androidx.media3.exoplayer.source.chunk.Chunk;
|
import androidx.media3.exoplayer.source.chunk.Chunk;
|
||||||
@ -38,7 +35,6 @@ import java.util.Collections;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|
||||||
|
|
||||||
/** Merges multiple {@link MediaPeriod}s. */
|
/** Merges multiple {@link MediaPeriod}s. */
|
||||||
/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
/* package */ final class MergingMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||||
@ -80,7 +76,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
*/
|
*/
|
||||||
public MediaPeriod getChildPeriod(int index) {
|
public MediaPeriod getChildPeriod(int index) {
|
||||||
return periods[index] instanceof TimeOffsetMediaPeriod
|
return periods[index] instanceof TimeOffsetMediaPeriod
|
||||||
? ((TimeOffsetMediaPeriod) periods[index]).mediaPeriod
|
? ((TimeOffsetMediaPeriod) periods[index]).getWrappedMediaPeriod()
|
||||||
: periods[index];
|
: periods[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -302,180 +298,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
|||||||
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
|
Assertions.checkNotNull(callback).onContinueLoadingRequested(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TimeOffsetMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
|
||||||
|
|
||||||
private final MediaPeriod mediaPeriod;
|
|
||||||
private final long timeOffsetUs;
|
|
||||||
|
|
||||||
private @MonotonicNonNull Callback callback;
|
|
||||||
|
|
||||||
public TimeOffsetMediaPeriod(MediaPeriod mediaPeriod, long timeOffsetUs) {
|
|
||||||
this.mediaPeriod = mediaPeriod;
|
|
||||||
this.timeOffsetUs = timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void prepare(Callback callback, long positionUs) {
|
|
||||||
this.callback = callback;
|
|
||||||
mediaPeriod.prepare(/* callback= */ this, positionUs - timeOffsetUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void maybeThrowPrepareError() throws IOException {
|
|
||||||
mediaPeriod.maybeThrowPrepareError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TrackGroupArray getTrackGroups() {
|
|
||||||
return mediaPeriod.getTrackGroups();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<StreamKey> getStreamKeys(List<ExoTrackSelection> trackSelections) {
|
|
||||||
return mediaPeriod.getStreamKeys(trackSelections);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long selectTracks(
|
|
||||||
@NullableType ExoTrackSelection[] selections,
|
|
||||||
boolean[] mayRetainStreamFlags,
|
|
||||||
@NullableType SampleStream[] streams,
|
|
||||||
boolean[] streamResetFlags,
|
|
||||||
long positionUs) {
|
|
||||||
@NullableType SampleStream[] childStreams = new SampleStream[streams.length];
|
|
||||||
for (int i = 0; i < streams.length; i++) {
|
|
||||||
TimeOffsetSampleStream sampleStream = (TimeOffsetSampleStream) streams[i];
|
|
||||||
childStreams[i] = sampleStream != null ? sampleStream.getChildStream() : null;
|
|
||||||
}
|
|
||||||
long startPositionUs =
|
|
||||||
mediaPeriod.selectTracks(
|
|
||||||
selections,
|
|
||||||
mayRetainStreamFlags,
|
|
||||||
childStreams,
|
|
||||||
streamResetFlags,
|
|
||||||
positionUs - timeOffsetUs);
|
|
||||||
for (int i = 0; i < streams.length; i++) {
|
|
||||||
@Nullable SampleStream childStream = childStreams[i];
|
|
||||||
if (childStream == null) {
|
|
||||||
streams[i] = null;
|
|
||||||
} else if (streams[i] == null
|
|
||||||
|| ((TimeOffsetSampleStream) streams[i]).getChildStream() != childStream) {
|
|
||||||
streams[i] = new TimeOffsetSampleStream(childStream, timeOffsetUs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return startPositionUs + timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void discardBuffer(long positionUs, boolean toKeyframe) {
|
|
||||||
mediaPeriod.discardBuffer(positionUs - timeOffsetUs, toKeyframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long readDiscontinuity() {
|
|
||||||
long discontinuityPositionUs = mediaPeriod.readDiscontinuity();
|
|
||||||
return discontinuityPositionUs == C.TIME_UNSET
|
|
||||||
? C.TIME_UNSET
|
|
||||||
: discontinuityPositionUs + timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long seekToUs(long positionUs) {
|
|
||||||
return mediaPeriod.seekToUs(positionUs - timeOffsetUs) + timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
|
||||||
return mediaPeriod.getAdjustedSeekPositionUs(positionUs - timeOffsetUs, seekParameters)
|
|
||||||
+ timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getBufferedPositionUs() {
|
|
||||||
long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
|
|
||||||
return bufferedPositionUs == C.TIME_END_OF_SOURCE
|
|
||||||
? C.TIME_END_OF_SOURCE
|
|
||||||
: bufferedPositionUs + timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getNextLoadPositionUs() {
|
|
||||||
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
|
|
||||||
return nextLoadPositionUs == C.TIME_END_OF_SOURCE
|
|
||||||
? C.TIME_END_OF_SOURCE
|
|
||||||
: nextLoadPositionUs + timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean continueLoading(LoadingInfo loadingInfo) {
|
|
||||||
return mediaPeriod.continueLoading(
|
|
||||||
loadingInfo
|
|
||||||
.buildUpon()
|
|
||||||
.setPlaybackPositionUs(loadingInfo.playbackPositionUs - timeOffsetUs)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLoading() {
|
|
||||||
return mediaPeriod.isLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reevaluateBuffer(long positionUs) {
|
|
||||||
mediaPeriod.reevaluateBuffer(positionUs - timeOffsetUs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPrepared(MediaPeriod mediaPeriod) {
|
|
||||||
Assertions.checkNotNull(callback).onPrepared(/* mediaPeriod= */ this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onContinueLoadingRequested(MediaPeriod source) {
|
|
||||||
Assertions.checkNotNull(callback).onContinueLoadingRequested(/* source= */ this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class TimeOffsetSampleStream implements SampleStream {
|
|
||||||
|
|
||||||
private final SampleStream sampleStream;
|
|
||||||
private final long timeOffsetUs;
|
|
||||||
|
|
||||||
public TimeOffsetSampleStream(SampleStream sampleStream, long timeOffsetUs) {
|
|
||||||
this.sampleStream = sampleStream;
|
|
||||||
this.timeOffsetUs = timeOffsetUs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SampleStream getChildStream() {
|
|
||||||
return sampleStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isReady() {
|
|
||||||
return sampleStream.isReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void maybeThrowError() throws IOException {
|
|
||||||
sampleStream.maybeThrowError();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int readData(
|
|
||||||
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
|
|
||||||
int readResult = sampleStream.readData(formatHolder, buffer, readFlags);
|
|
||||||
if (readResult == C.RESULT_BUFFER_READ) {
|
|
||||||
buffer.timeUs = buffer.timeUs + timeOffsetUs;
|
|
||||||
}
|
|
||||||
return readResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int skipData(long positionUs) {
|
|
||||||
return sampleStream.skipData(positionUs - timeOffsetUs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class ForwardingTrackSelection implements ExoTrackSelection {
|
private static final class ForwardingTrackSelection implements ExoTrackSelection {
|
||||||
|
|
||||||
private final ExoTrackSelection trackSelection;
|
private final ExoTrackSelection trackSelection;
|
||||||
|
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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.exoplayer.source;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.StreamKey;
|
||||||
|
import androidx.media3.common.util.Assertions;
|
||||||
|
import androidx.media3.common.util.NullableType;
|
||||||
|
import androidx.media3.decoder.DecoderInputBuffer;
|
||||||
|
import androidx.media3.exoplayer.FormatHolder;
|
||||||
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
|
import androidx.media3.exoplayer.SeekParameters;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
/** A {@link MediaPeriod} that applies a fixed time offset to all timestamps */
|
||||||
|
/* package */ final class TimeOffsetMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
|
||||||
|
|
||||||
|
private final MediaPeriod mediaPeriod;
|
||||||
|
private final long timeOffsetUs;
|
||||||
|
|
||||||
|
private @MonotonicNonNull Callback callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the time offset period.
|
||||||
|
*
|
||||||
|
* @param mediaPeriod The wrapped {@link MediaPeriod}.
|
||||||
|
* @param timeOffsetUs The offset to apply to all timestamps coming from the wrapped period, in
|
||||||
|
* microseconds.
|
||||||
|
*/
|
||||||
|
public TimeOffsetMediaPeriod(MediaPeriod mediaPeriod, long timeOffsetUs) {
|
||||||
|
this.mediaPeriod = mediaPeriod;
|
||||||
|
this.timeOffsetUs = timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the wrapped {@link MediaPeriod}. */
|
||||||
|
public MediaPeriod getWrappedMediaPeriod() {
|
||||||
|
return mediaPeriod;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare(Callback callback, long positionUs) {
|
||||||
|
this.callback = callback;
|
||||||
|
mediaPeriod.prepare(/* callback= */ this, positionUs - timeOffsetUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowPrepareError() throws IOException {
|
||||||
|
mediaPeriod.maybeThrowPrepareError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TrackGroupArray getTrackGroups() {
|
||||||
|
return mediaPeriod.getTrackGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<StreamKey> getStreamKeys(List<ExoTrackSelection> trackSelections) {
|
||||||
|
return mediaPeriod.getStreamKeys(trackSelections);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long selectTracks(
|
||||||
|
@NullableType ExoTrackSelection[] selections,
|
||||||
|
boolean[] mayRetainStreamFlags,
|
||||||
|
@NullableType SampleStream[] streams,
|
||||||
|
boolean[] streamResetFlags,
|
||||||
|
long positionUs) {
|
||||||
|
@NullableType SampleStream[] childStreams = new SampleStream[streams.length];
|
||||||
|
for (int i = 0; i < streams.length; i++) {
|
||||||
|
TimeOffsetSampleStream sampleStream = (TimeOffsetSampleStream) streams[i];
|
||||||
|
childStreams[i] = sampleStream != null ? sampleStream.getChildStream() : null;
|
||||||
|
}
|
||||||
|
long startPositionUs =
|
||||||
|
mediaPeriod.selectTracks(
|
||||||
|
selections,
|
||||||
|
mayRetainStreamFlags,
|
||||||
|
childStreams,
|
||||||
|
streamResetFlags,
|
||||||
|
positionUs - timeOffsetUs);
|
||||||
|
for (int i = 0; i < streams.length; i++) {
|
||||||
|
@Nullable SampleStream childStream = childStreams[i];
|
||||||
|
if (childStream == null) {
|
||||||
|
streams[i] = null;
|
||||||
|
} else if (streams[i] == null
|
||||||
|
|| ((TimeOffsetSampleStream) streams[i]).getChildStream() != childStream) {
|
||||||
|
streams[i] = new TimeOffsetSampleStream(childStream, timeOffsetUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return startPositionUs + timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void discardBuffer(long positionUs, boolean toKeyframe) {
|
||||||
|
mediaPeriod.discardBuffer(positionUs - timeOffsetUs, toKeyframe);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long readDiscontinuity() {
|
||||||
|
long discontinuityPositionUs = mediaPeriod.readDiscontinuity();
|
||||||
|
return discontinuityPositionUs == C.TIME_UNSET
|
||||||
|
? C.TIME_UNSET
|
||||||
|
: discontinuityPositionUs + timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long seekToUs(long positionUs) {
|
||||||
|
return mediaPeriod.seekToUs(positionUs - timeOffsetUs) + timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
|
||||||
|
return mediaPeriod.getAdjustedSeekPositionUs(positionUs - timeOffsetUs, seekParameters)
|
||||||
|
+ timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getBufferedPositionUs() {
|
||||||
|
long bufferedPositionUs = mediaPeriod.getBufferedPositionUs();
|
||||||
|
return bufferedPositionUs == C.TIME_END_OF_SOURCE
|
||||||
|
? C.TIME_END_OF_SOURCE
|
||||||
|
: bufferedPositionUs + timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getNextLoadPositionUs() {
|
||||||
|
long nextLoadPositionUs = mediaPeriod.getNextLoadPositionUs();
|
||||||
|
return nextLoadPositionUs == C.TIME_END_OF_SOURCE
|
||||||
|
? C.TIME_END_OF_SOURCE
|
||||||
|
: nextLoadPositionUs + timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean continueLoading(LoadingInfo loadingInfo) {
|
||||||
|
return mediaPeriod.continueLoading(
|
||||||
|
loadingInfo
|
||||||
|
.buildUpon()
|
||||||
|
.setPlaybackPositionUs(loadingInfo.playbackPositionUs - timeOffsetUs)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isLoading() {
|
||||||
|
return mediaPeriod.isLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reevaluateBuffer(long positionUs) {
|
||||||
|
mediaPeriod.reevaluateBuffer(positionUs - timeOffsetUs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
Assertions.checkNotNull(callback).onPrepared(/* mediaPeriod= */ this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {
|
||||||
|
Assertions.checkNotNull(callback).onContinueLoadingRequested(/* source= */ this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class TimeOffsetSampleStream implements SampleStream {
|
||||||
|
|
||||||
|
private final SampleStream sampleStream;
|
||||||
|
private final long timeOffsetUs;
|
||||||
|
|
||||||
|
public TimeOffsetSampleStream(SampleStream sampleStream, long timeOffsetUs) {
|
||||||
|
this.sampleStream = sampleStream;
|
||||||
|
this.timeOffsetUs = timeOffsetUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SampleStream getChildStream() {
|
||||||
|
return sampleStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return sampleStream.isReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void maybeThrowError() throws IOException {
|
||||||
|
sampleStream.maybeThrowError();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int readData(
|
||||||
|
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
|
||||||
|
int readResult = sampleStream.readData(formatHolder, buffer, readFlags);
|
||||||
|
if (readResult == C.RESULT_BUFFER_READ) {
|
||||||
|
buffer.timeUs = buffer.timeUs + timeOffsetUs;
|
||||||
|
}
|
||||||
|
return readResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int skipData(long positionUs) {
|
||||||
|
return sampleStream.skipData(positionUs - timeOffsetUs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,264 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 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.exoplayer.source;
|
||||||
|
|
||||||
|
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
|
||||||
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM;
|
||||||
|
import static androidx.media3.test.utils.FakeSampleStream.FakeSampleStreamItem.oneByteSample;
|
||||||
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.spy;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.Format;
|
||||||
|
import androidx.media3.common.TrackGroup;
|
||||||
|
import androidx.media3.decoder.DecoderInputBuffer;
|
||||||
|
import androidx.media3.exoplayer.FormatHolder;
|
||||||
|
import androidx.media3.exoplayer.LoadingInfo;
|
||||||
|
import androidx.media3.exoplayer.SeekParameters;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||||
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher;
|
||||||
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.trackselection.FixedTrackSelection;
|
||||||
|
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
|
import androidx.media3.test.utils.FakeMediaPeriod;
|
||||||
|
import androidx.media3.test.utils.FakeSampleStream;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/** Unit test for {@link TimeOffsetMediaPeriod}. */
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public final class TimeOffsetMediaPeriodTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void selectTracks_createsSampleStreamCorrectingOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0);
|
||||||
|
FormatHolder formatHolder = new FormatHolder();
|
||||||
|
DecoderInputBuffer inputBuffer =
|
||||||
|
new DecoderInputBuffer(DecoderInputBuffer.BUFFER_REPLACEMENT_MODE_NORMAL);
|
||||||
|
ImmutableList.Builder<Integer> readResults = ImmutableList.builder();
|
||||||
|
|
||||||
|
SampleStream sampleStream = selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
readResults.add(sampleStream.readData(formatHolder, inputBuffer, FLAG_REQUIRE_FORMAT));
|
||||||
|
readResults.add(sampleStream.readData(formatHolder, inputBuffer, /* readFlags= */ 0));
|
||||||
|
long readBufferTimeUs = inputBuffer.timeUs;
|
||||||
|
readResults.add(sampleStream.readData(formatHolder, inputBuffer, /* readFlags= */ 0));
|
||||||
|
boolean readEndOfStreamBuffer = inputBuffer.isEndOfStream();
|
||||||
|
|
||||||
|
assertThat(readResults.build())
|
||||||
|
.containsExactly(C.RESULT_FORMAT_READ, C.RESULT_BUFFER_READ, C.RESULT_BUFFER_READ);
|
||||||
|
assertThat(readBufferTimeUs).isEqualTo(5000);
|
||||||
|
assertThat(readEndOfStreamBuffer).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getBufferedPositionUs_returnsPositionWithOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 12000, C.BUFFER_FLAG_KEY_FRAME)));
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
assertThat(timeOffsetMediaPeriod.getBufferedPositionUs()).isEqualTo(9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getNextLoadPositionUs_returnsPositionWithOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME),
|
||||||
|
oneByteSample(/* timeUs= */ 12000, C.BUFFER_FLAG_KEY_FRAME)));
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(fakeMediaPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 0);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
assertThat(timeOffsetMediaPeriod.getNextLoadPositionUs()).isEqualTo(9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
|
||||||
|
verify(spyPeriod).prepare(any(), eq(4000L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void discardBuffer_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
timeOffsetMediaPeriod.discardBuffer(/* positionUs= */ 1000, /* toKeyframe= */ true);
|
||||||
|
|
||||||
|
verify(spyPeriod).discardBuffer(4000, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void seekTo_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
long seekResultTimeUs = timeOffsetMediaPeriod.seekToUs(/* positionUs= */ 1000);
|
||||||
|
|
||||||
|
verify(spyPeriod).seekToUs(4000);
|
||||||
|
assertThat(seekResultTimeUs).isEqualTo(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getAdjustedSeekPosition_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
fakeMediaPeriod.setSeekToUsOffset(2000);
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
long adjustedSeekPositionUs =
|
||||||
|
timeOffsetMediaPeriod.getAdjustedSeekPositionUs(
|
||||||
|
/* positionUs= */ 1000, SeekParameters.DEFAULT);
|
||||||
|
|
||||||
|
verify(spyPeriod).getAdjustedSeekPositionUs(4000, SeekParameters.DEFAULT);
|
||||||
|
assertThat(adjustedSeekPositionUs).isEqualTo(3000); // = 4000 + 2000 - 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void continueLoading_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
timeOffsetMediaPeriod.continueLoading(
|
||||||
|
new LoadingInfo.Builder().setPlaybackPositionUs(1000).build());
|
||||||
|
|
||||||
|
verify(spyPeriod)
|
||||||
|
.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(4000).build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reevaluateBuffer_isForwardedWithTimeOffset() throws Exception {
|
||||||
|
FakeMediaPeriod fakeMediaPeriod =
|
||||||
|
createFakeMediaPeriod(
|
||||||
|
ImmutableList.of(
|
||||||
|
oneByteSample(/* timeUs= */ 8000, C.BUFFER_FLAG_KEY_FRAME), END_OF_STREAM_ITEM));
|
||||||
|
MediaPeriod spyPeriod = spy(fakeMediaPeriod);
|
||||||
|
TimeOffsetMediaPeriod timeOffsetMediaPeriod =
|
||||||
|
new TimeOffsetMediaPeriod(spyPeriod, /* timeOffsetUs= */ -3000);
|
||||||
|
prepareMediaPeriodSync(timeOffsetMediaPeriod, /* positionUs= */ 1000);
|
||||||
|
selectTracksOnMediaPeriodAndTriggerLoading(timeOffsetMediaPeriod);
|
||||||
|
|
||||||
|
timeOffsetMediaPeriod.reevaluateBuffer(/* positionUs= */ 1000);
|
||||||
|
|
||||||
|
verify(spyPeriod).reevaluateBuffer(4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FakeMediaPeriod createFakeMediaPeriod(
|
||||||
|
ImmutableList<FakeSampleStream.FakeSampleStreamItem> sampleStreamItems) {
|
||||||
|
EventDispatcher eventDispatcher =
|
||||||
|
new EventDispatcher()
|
||||||
|
.withParameters(/* windowIndex= */ 0, new MediaPeriodId(/* periodUid= */ new Object()));
|
||||||
|
return new FakeMediaPeriod(
|
||||||
|
new TrackGroupArray(new TrackGroup(new Format.Builder().build())),
|
||||||
|
new DefaultAllocator(/* trimOnReset= */ false, /* individualAllocationSize= */ 1024),
|
||||||
|
(unusedFormat, unusedMediaPeriodId) -> sampleStreamItems,
|
||||||
|
eventDispatcher,
|
||||||
|
DrmSessionManager.DRM_UNSUPPORTED,
|
||||||
|
new DrmSessionEventListener.EventDispatcher(),
|
||||||
|
/* deferOnPrepared= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void prepareMediaPeriodSync(MediaPeriod mediaPeriod, long positionUs)
|
||||||
|
throws Exception {
|
||||||
|
CountDownLatch prepareCountDown = new CountDownLatch(1);
|
||||||
|
mediaPeriod.prepare(
|
||||||
|
new MediaPeriod.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPrepared(MediaPeriod mediaPeriod) {
|
||||||
|
prepareCountDown.countDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContinueLoadingRequested(MediaPeriod source) {
|
||||||
|
mediaPeriod.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
positionUs);
|
||||||
|
prepareCountDown.await();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SampleStream selectTracksOnMediaPeriodAndTriggerLoading(MediaPeriod mediaPeriod) {
|
||||||
|
ExoTrackSelection selection =
|
||||||
|
new FixedTrackSelection(mediaPeriod.getTrackGroups().get(0), /* track= */ 0);
|
||||||
|
SampleStream[] streams = new SampleStream[1];
|
||||||
|
mediaPeriod.selectTracks(
|
||||||
|
/* selections= */ new ExoTrackSelection[] {selection},
|
||||||
|
/* mayRetainStreamFlags= */ new boolean[] {false},
|
||||||
|
streams,
|
||||||
|
/* streamResetFlags= */ new boolean[] {false},
|
||||||
|
/* positionUs= */ 0);
|
||||||
|
mediaPeriod.continueLoading(new LoadingInfo.Builder().setPlaybackPositionUs(0).build());
|
||||||
|
return streams[0];
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user