mirror of
https://github.com/samsonjs/media.git
synced 2026-04-27 15:07:40 +00:00
Support customized loading in ExternallyLoadedMediaSource
PiperOrigin-RevId: 572299971
This commit is contained in:
parent
66fa591959
commit
8f2161c43d
6 changed files with 235 additions and 33 deletions
|
|
@ -116,6 +116,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||||
|
|
||||||
private DataSource.Factory dataSourceFactory;
|
private DataSource.Factory dataSourceFactory;
|
||||||
@Nullable private MediaSource.Factory serverSideAdInsertionMediaSourceFactory;
|
@Nullable private MediaSource.Factory serverSideAdInsertionMediaSourceFactory;
|
||||||
|
@Nullable private ExternalLoader externalImageLoader;
|
||||||
@Nullable private AdsLoader.Provider adsLoaderProvider;
|
@Nullable private AdsLoader.Provider adsLoaderProvider;
|
||||||
@Nullable private AdViewProvider adViewProvider;
|
@Nullable private AdViewProvider adViewProvider;
|
||||||
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
@Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
|
|
@ -315,6 +316,26 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link ExternalLoader} to be called when loading starts in {@link
|
||||||
|
* ExternallyLoadedMediaSource} when loading images in an external Image Management Framework (for
|
||||||
|
* example, Glide).
|
||||||
|
*
|
||||||
|
* <p>This loader is only used when the {@link MediaItem.LocalConfiguration#mimeType} is set to
|
||||||
|
* {@link MimeTypes#APPLICATION_EXTERNALLY_LOADED_IMAGE}.
|
||||||
|
*
|
||||||
|
* @param externalImageLoader The {@link ExternalLoader} to load the media or {@code null} to
|
||||||
|
* remove a previously set {@link ExternalLoader}.
|
||||||
|
* @return This factory, for convenience.
|
||||||
|
*/
|
||||||
|
@CanIgnoreReturnValue
|
||||||
|
@UnstableApi
|
||||||
|
public DefaultMediaSourceFactory setExternalImageLoader(
|
||||||
|
@Nullable ExternalLoader externalImageLoader) {
|
||||||
|
this.externalImageLoader = externalImageLoader;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the target live offset for live streams, in milliseconds.
|
* Sets the target live offset for live streams, in milliseconds.
|
||||||
*
|
*
|
||||||
|
|
@ -440,7 +461,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
||||||
if (Objects.equals(
|
if (Objects.equals(
|
||||||
mediaItem.localConfiguration.mimeType, MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)) {
|
mediaItem.localConfiguration.mimeType, MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)) {
|
||||||
return new ExternallyLoadedMediaSource.Factory(
|
return new ExternallyLoadedMediaSource.Factory(
|
||||||
msToUs(mediaItem.localConfiguration.imageDurationMs))
|
msToUs(mediaItem.localConfiguration.imageDurationMs),
|
||||||
|
checkNotNull(externalImageLoader))
|
||||||
.createMediaSource(mediaItem);
|
.createMediaSource(mediaItem);
|
||||||
}
|
}
|
||||||
@C.ContentType
|
@C.ContentType
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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 android.net.Uri;
|
||||||
|
import androidx.media3.common.util.UnstableApi;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
|
/** An object for loading media outside of ExoPlayer's loading mechanism. */
|
||||||
|
@UnstableApi
|
||||||
|
public interface ExternalLoader {
|
||||||
|
|
||||||
|
/** A data class providing information associated with the load event. */
|
||||||
|
final class LoadRequest {
|
||||||
|
|
||||||
|
/** The {@link Uri} stored in the load request object. */
|
||||||
|
public final Uri uri;
|
||||||
|
|
||||||
|
/** Creates an instance. */
|
||||||
|
public LoadRequest(Uri uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the external media.
|
||||||
|
*
|
||||||
|
* @param loadRequest The load request.
|
||||||
|
* @return The {@link ListenableFuture} tracking the completion of the loading work.
|
||||||
|
*/
|
||||||
|
ListenableFuture<@Nullable ?> load(LoadRequest loadRequest);
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
package androidx.media3.exoplayer.source;
|
package androidx.media3.exoplayer.source;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
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.TrackGroup;
|
import androidx.media3.common.TrackGroup;
|
||||||
|
|
@ -24,8 +25,17 @@ import androidx.media3.decoder.DecoderInputBuffer;
|
||||||
import androidx.media3.exoplayer.FormatHolder;
|
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.ExternalLoader.LoadRequest;
|
||||||
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link MediaPeriod} that puts a {@link Charsets#UTF_8}-encoded {@link Uri} into the sample
|
* A {@link MediaPeriod} that puts a {@link Charsets#UTF_8}-encoded {@link Uri} into the sample
|
||||||
|
|
@ -33,24 +43,42 @@ import com.google.common.base.Charsets;
|
||||||
*/
|
*/
|
||||||
/* package */ final class ExternallyLoadedMediaPeriod implements MediaPeriod {
|
/* package */ final class ExternallyLoadedMediaPeriod implements MediaPeriod {
|
||||||
|
|
||||||
private final Format format;
|
private final Uri uri;
|
||||||
|
private final ExternalLoader externalLoader;
|
||||||
private final TrackGroupArray tracks;
|
private final TrackGroupArray tracks;
|
||||||
private final byte[] sampleData;
|
private final byte[] sampleData;
|
||||||
|
private final AtomicBoolean loadingFinished;
|
||||||
|
private final AtomicReference<Throwable> loadingThrowable;
|
||||||
|
private @MonotonicNonNull ListenableFuture<?> loadingFuture;
|
||||||
|
|
||||||
// TODO: b/303375301 - Removing this variable (replacing it with static returns in the methods
|
public ExternallyLoadedMediaPeriod(Uri uri, String mimeType, ExternalLoader externalLoader) {
|
||||||
// that
|
this.uri = uri;
|
||||||
// use it) causes playback to hang.
|
Format format = new Format.Builder().setSampleMimeType(mimeType).build();
|
||||||
private boolean loadingFinished;
|
this.externalLoader = externalLoader;
|
||||||
|
|
||||||
public ExternallyLoadedMediaPeriod(Uri uri, String mimeType) {
|
|
||||||
this.format = new Format.Builder().setSampleMimeType(mimeType).build();
|
|
||||||
tracks = new TrackGroupArray(new TrackGroup(format));
|
tracks = new TrackGroupArray(new TrackGroup(format));
|
||||||
sampleData = uri.toString().getBytes(Charsets.UTF_8);
|
sampleData = uri.toString().getBytes(Charsets.UTF_8);
|
||||||
|
loadingFinished = new AtomicBoolean();
|
||||||
|
loadingThrowable = new AtomicReference<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepare(Callback callback, long positionUs) {
|
public void prepare(Callback callback, long positionUs) {
|
||||||
callback.onPrepared(this);
|
callback.onPrepared(this);
|
||||||
|
loadingFuture = externalLoader.load(new LoadRequest(uri));
|
||||||
|
Futures.addCallback(
|
||||||
|
loadingFuture,
|
||||||
|
new FutureCallback<@NullableType Object>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(@Nullable Object result) {
|
||||||
|
loadingFinished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Throwable t) {
|
||||||
|
loadingThrowable.set(t);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -105,26 +133,22 @@ import com.google.common.base.Charsets;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getBufferedPositionUs() {
|
public long getBufferedPositionUs() {
|
||||||
return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
|
return loadingFinished.get() ? C.TIME_END_OF_SOURCE : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long getNextLoadPositionUs() {
|
public long getNextLoadPositionUs() {
|
||||||
return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
|
return loadingFinished.get() ? C.TIME_END_OF_SOURCE : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean continueLoading(LoadingInfo loadingInfo) {
|
public boolean continueLoading(LoadingInfo loadingInfo) {
|
||||||
if (loadingFinished) {
|
return !loadingFinished.get();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
loadingFinished = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isLoading() {
|
public boolean isLoading() {
|
||||||
return !loadingFinished;
|
return !loadingFinished.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -132,8 +156,13 @@ import com.google.common.base.Charsets;
|
||||||
// Do nothing.
|
// Do nothing.
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class SampleStreamImpl implements SampleStream {
|
public void releasePeriod() {
|
||||||
|
if (loadingFuture != null) {
|
||||||
|
loadingFuture.cancel(/* mayInterruptIfRunning= */ false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class SampleStreamImpl implements SampleStream {
|
||||||
private static final int STREAM_STATE_SEND_FORMAT = 0;
|
private static final int STREAM_STATE_SEND_FORMAT = 0;
|
||||||
private static final int STREAM_STATE_SEND_SAMPLE = 1;
|
private static final int STREAM_STATE_SEND_SAMPLE = 1;
|
||||||
private static final int STREAM_STATE_END_OF_STREAM = 2;
|
private static final int STREAM_STATE_END_OF_STREAM = 2;
|
||||||
|
|
@ -146,13 +175,16 @@ import com.google.common.base.Charsets;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isReady() {
|
public boolean isReady() {
|
||||||
return loadingFinished;
|
return loadingFinished.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void maybeThrowError() {
|
public void maybeThrowError() throws IOException {
|
||||||
// Do nothing.
|
@Nullable
|
||||||
|
Throwable loadingThrowable = ExternallyLoadedMediaPeriod.this.loadingThrowable.get();
|
||||||
|
if (loadingThrowable != null) {
|
||||||
|
throw new IOException(loadingThrowable);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -170,6 +202,10 @@ import com.google.common.base.Charsets;
|
||||||
return C.RESULT_FORMAT_READ;
|
return C.RESULT_FORMAT_READ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!loadingFinished.get()) {
|
||||||
|
return C.RESULT_NOTHING_READ;
|
||||||
|
}
|
||||||
|
|
||||||
int sampleSize = sampleData.length;
|
int sampleSize = sampleData.length;
|
||||||
buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
|
buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
|
||||||
buffer.timeUs = 0;
|
buffer.timeUs = 0;
|
||||||
|
|
|
||||||
|
|
@ -43,18 +43,24 @@ import java.util.Objects;
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
||||||
|
|
||||||
|
private final ExternalLoader externalLoader;
|
||||||
|
|
||||||
/** Factory for {@link ExternallyLoadedMediaSource}. */
|
/** Factory for {@link ExternallyLoadedMediaSource}. */
|
||||||
public static final class Factory implements MediaSource.Factory {
|
public static final class Factory implements MediaSource.Factory {
|
||||||
|
|
||||||
private final long timelineDurationUs;
|
private final long timelineDurationUs;
|
||||||
|
private final ExternalLoader externalLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
*
|
*
|
||||||
* @param timelineDurationUs The duration of the {@link SinglePeriodTimeline} created.
|
* @param timelineDurationUs The duration of the {@link SinglePeriodTimeline} created, in
|
||||||
|
* microseconds.
|
||||||
|
* @param externalLoader The {@link ExternalLoader} to load the media in preparation for
|
||||||
|
* playback.
|
||||||
*/
|
*/
|
||||||
Factory(long timelineDurationUs) {
|
public Factory(long timelineDurationUs, ExternalLoader externalLoader) {
|
||||||
this.timelineDurationUs = timelineDurationUs;
|
this.timelineDurationUs = timelineDurationUs;
|
||||||
|
this.externalLoader = externalLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Does nothing. {@link ExternallyLoadedMediaSource} does not support DRM. */
|
/** Does nothing. {@link ExternallyLoadedMediaSource} does not support DRM. */
|
||||||
|
|
@ -80,7 +86,7 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExternallyLoadedMediaSource createMediaSource(MediaItem mediaItem) {
|
public ExternallyLoadedMediaSource createMediaSource(MediaItem mediaItem) {
|
||||||
return new ExternallyLoadedMediaSource(mediaItem, timelineDurationUs);
|
return new ExternallyLoadedMediaSource(mediaItem, timelineDurationUs, externalLoader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,9 +95,11 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
||||||
@GuardedBy("this")
|
@GuardedBy("this")
|
||||||
private MediaItem mediaItem;
|
private MediaItem mediaItem;
|
||||||
|
|
||||||
private ExternallyLoadedMediaSource(MediaItem mediaItem, long timelineDurationUs) {
|
private ExternallyLoadedMediaSource(
|
||||||
|
MediaItem mediaItem, long timelineDurationUs, ExternalLoader externalLoader) {
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
this.timelineDurationUs = timelineDurationUs;
|
this.timelineDurationUs = timelineDurationUs;
|
||||||
|
this.externalLoader = externalLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -118,7 +126,7 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized boolean canUpdateMediaItem(MediaItem mediaItem) {
|
public boolean canUpdateMediaItem(MediaItem mediaItem) {
|
||||||
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
|
@Nullable MediaItem.LocalConfiguration newConfiguration = mediaItem.localConfiguration;
|
||||||
MediaItem.LocalConfiguration oldConfiguration = checkNotNull(getMediaItem().localConfiguration);
|
MediaItem.LocalConfiguration oldConfiguration = checkNotNull(getMediaItem().localConfiguration);
|
||||||
return newConfiguration != null
|
return newConfiguration != null
|
||||||
|
|
@ -145,11 +153,11 @@ public final class ExternallyLoadedMediaSource extends BaseMediaSource {
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
mediaItem.localConfiguration.mimeType, "Externally loaded mediaItems require a MIME type.");
|
mediaItem.localConfiguration.mimeType, "Externally loaded mediaItems require a MIME type.");
|
||||||
return new ExternallyLoadedMediaPeriod(
|
return new ExternallyLoadedMediaPeriod(
|
||||||
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
|
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType, externalLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releasePeriod(MediaPeriod mediaPeriod) {
|
public void releasePeriod(MediaPeriod mediaPeriod) {
|
||||||
// Do nothing.
|
((ExternallyLoadedMediaPeriod) mediaPeriod).releasePeriod();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,14 +29,18 @@ import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
|
import androidx.media3.common.util.ConditionVariable;
|
||||||
import androidx.media3.datasource.AssetDataSource;
|
import androidx.media3.datasource.AssetDataSource;
|
||||||
import androidx.media3.datasource.DataSourceUtil;
|
import androidx.media3.datasource.DataSourceUtil;
|
||||||
import androidx.media3.datasource.DataSpec;
|
import androidx.media3.datasource.DataSpec;
|
||||||
|
import androidx.media3.exoplayer.ExoPlaybackException;
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
import androidx.media3.exoplayer.RendererCapabilities;
|
import androidx.media3.exoplayer.RendererCapabilities;
|
||||||
import androidx.media3.exoplayer.image.BitmapFactoryImageDecoder;
|
import androidx.media3.exoplayer.image.BitmapFactoryImageDecoder;
|
||||||
import androidx.media3.exoplayer.image.ImageDecoder;
|
import androidx.media3.exoplayer.image.ImageDecoder;
|
||||||
import androidx.media3.exoplayer.image.ImageDecoderException;
|
import androidx.media3.exoplayer.image.ImageDecoderException;
|
||||||
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.test.utils.CapturingRenderersFactory;
|
import androidx.media3.test.utils.CapturingRenderersFactory;
|
||||||
import androidx.media3.test.utils.DumpFileAsserts;
|
import androidx.media3.test.utils.DumpFileAsserts;
|
||||||
import androidx.media3.test.utils.FakeClock;
|
import androidx.media3.test.utils.FakeClock;
|
||||||
|
|
@ -45,7 +49,11 @@ import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.base.Charsets;
|
import com.google.common.base.Charsets;
|
||||||
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.annotation.GraphicsMode;
|
import org.robolectric.annotation.GraphicsMode;
|
||||||
|
|
@ -58,14 +66,26 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||||
private static final String INPUT_FILE = "png/non-motion-photo-shortened.png";
|
private static final String INPUT_FILE = "png/non-motion-photo-shortened.png";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test() throws Exception {
|
public void imagePlayback_validExternalLoader_callsLoadOnceAndPlaysSuccessfully()
|
||||||
|
throws Exception {
|
||||||
Context applicationContext = ApplicationProvider.getApplicationContext();
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
CapturingRenderersFactory renderersFactory =
|
CapturingRenderersFactory renderersFactory =
|
||||||
new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true)
|
new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true)
|
||||||
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||||
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
|
||||||
|
AtomicInteger externalLoaderCallCount = new AtomicInteger();
|
||||||
|
ListeningExecutorService listeningExecutorService =
|
||||||
|
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||||
|
MediaSource.Factory mediaSourceFactory =
|
||||||
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
|
.setExternalImageLoader(
|
||||||
|
unused ->
|
||||||
|
listeningExecutorService.submit(externalLoaderCallCount::getAndIncrement));
|
||||||
ExoPlayer player =
|
ExoPlayer player =
|
||||||
new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build();
|
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||||
|
.setClock(clock)
|
||||||
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.build();
|
||||||
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
|
||||||
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
long durationMs = 5 * C.MILLIS_PER_SECOND;
|
||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
|
|
@ -83,11 +103,80 @@ public final class ExternallyLoadedImagePlaybackTest {
|
||||||
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
|
||||||
player.release();
|
player.release();
|
||||||
|
|
||||||
|
assertThat(externalLoaderCallCount.get()).isEqualTo(1);
|
||||||
assertThat(playbackDurationMs).isAtLeast(durationMs);
|
assertThat(playbackDurationMs).isAtLeast(durationMs);
|
||||||
DumpFileAsserts.assertOutput(
|
DumpFileAsserts.assertOutput(
|
||||||
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
|
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void imagePlayback_externalLoaderFutureFails_propagatesFailure() throws Exception {
|
||||||
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
|
CapturingRenderersFactory renderersFactory =
|
||||||
|
new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true)
|
||||||
|
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||||
|
ListeningExecutorService listeningExecutorService =
|
||||||
|
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||||
|
MediaSource.Factory mediaSourceFactory =
|
||||||
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
|
.setExternalImageLoader(
|
||||||
|
unused ->
|
||||||
|
listeningExecutorService.submit(
|
||||||
|
() -> {
|
||||||
|
throw new RuntimeException("My Exception");
|
||||||
|
}));
|
||||||
|
ExoPlayer player =
|
||||||
|
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("asset:///media/" + INPUT_FILE)
|
||||||
|
.setImageDurationMs(5 * C.MILLIS_PER_SECOND)
|
||||||
|
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||||
|
.build());
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
ExoPlaybackException error = TestPlayerRunHelper.runUntilError(player);
|
||||||
|
assertThat(error).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void imagePlayback_loadingCompletedWhenFutureCompletes() throws Exception {
|
||||||
|
Context applicationContext = ApplicationProvider.getApplicationContext();
|
||||||
|
CapturingRenderersFactory renderersFactory =
|
||||||
|
new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true)
|
||||||
|
.setImageDecoderFactory(new CustomImageDecoderFactory());
|
||||||
|
ListeningExecutorService listeningExecutorService =
|
||||||
|
MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor());
|
||||||
|
ConditionVariable loadingComplete = new ConditionVariable();
|
||||||
|
MediaSource.Factory mediaSourceFactory =
|
||||||
|
new DefaultMediaSourceFactory(applicationContext)
|
||||||
|
.setExternalImageLoader(
|
||||||
|
unused -> listeningExecutorService.submit(loadingComplete::blockUninterruptible));
|
||||||
|
ExoPlayer player =
|
||||||
|
new ExoPlayer.Builder(applicationContext, renderersFactory)
|
||||||
|
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
|
||||||
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.build();
|
||||||
|
player.setMediaItem(
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri("asset:///media/" + INPUT_FILE)
|
||||||
|
.setImageDurationMs(5 * C.MILLIS_PER_SECOND)
|
||||||
|
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
|
||||||
|
.build());
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
TestPlayerRunHelper.runUntilPendingCommandsAreFullyHandled(player);
|
||||||
|
|
||||||
|
assertThat(player.isLoading()).isTrue();
|
||||||
|
|
||||||
|
loadingComplete.open();
|
||||||
|
// Assert the player stops loading.
|
||||||
|
TestPlayerRunHelper.runUntilIsLoading(player, /* expectedIsLoading= */ false);
|
||||||
|
}
|
||||||
|
|
||||||
private static final class CustomImageDecoderFactory implements ImageDecoder.Factory {
|
private static final class CustomImageDecoderFactory implements ImageDecoder.Factory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import androidx.media3.exoplayer.analytics.PlayerId;
|
||||||
import androidx.media3.test.utils.TestUtil;
|
import androidx.media3.test.utils.TestUtil;
|
||||||
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
import androidx.media3.test.utils.robolectric.RobolectricUtil;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.util.concurrent.SettableFuture;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
@ -128,7 +129,7 @@ public class ExternallyLoadedMediaSourceTest {
|
||||||
|
|
||||||
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
private static MediaSource buildMediaSource(MediaItem mediaItem) {
|
||||||
return new ExternallyLoadedMediaSource.Factory(
|
return new ExternallyLoadedMediaSource.Factory(
|
||||||
msToUs(mediaItem.localConfiguration.imageDurationMs))
|
msToUs(mediaItem.localConfiguration.imageDurationMs), unused -> SettableFuture.create())
|
||||||
.createMediaSource(mediaItem);
|
.createMediaSource(mediaItem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue