Create ExternallyLoadedMediaPeriod and ExternallyLoadedMediaSource

PiperOrigin-RevId: 571292394
This commit is contained in:
tofunmi 2023-10-06 03:49:53 -07:00 committed by Copybara-Service
parent 89d01981bc
commit addfd3e986
5 changed files with 480 additions and 3 deletions

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer.source;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.common.util.Util.msToUs;
import android.content.Context;
import android.net.Uri;
@ -60,6 +61,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -435,6 +437,12 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
if (scheme != null && scheme.equals(C.SSAI_SCHEME)) {
return checkNotNull(serverSideAdInsertionMediaSourceFactory).createMediaSource(mediaItem);
}
if (Objects.equals(
mediaItem.localConfiguration.mimeType, MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)) {
return new ExternallyLoadedMediaSource.Factory(
msToUs(mediaItem.localConfiguration.imageDurationMs))
.createMediaSource(mediaItem);
}
@C.ContentType
int type =
Util.inferContentTypeForUriAndMimeType(
@ -531,8 +539,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
}
return new ClippingMediaSource(
mediaSource,
Util.msToUs(mediaItem.clippingConfiguration.startPositionMs),
Util.msToUs(mediaItem.clippingConfiguration.endPositionMs),
msToUs(mediaItem.clippingConfiguration.startPositionMs),
msToUs(mediaItem.clippingConfiguration.endPositionMs),
/* enableInitialDiscontinuity= */ !mediaItem.clippingConfiguration.startsAtKeyFrame,
/* allowDynamicClippingUpdates= */ mediaItem.clippingConfiguration.relativeToLiveWindow,
mediaItem.clippingConfiguration.relativeToDefaultPosition);

View File

@ -0,0 +1,193 @@
/*
* 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.C;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
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 com.google.common.base.Charsets;
/**
* A {@link MediaPeriod} that puts a {@link Charsets#UTF_8}-encoded {@link Uri} into the sample
* queue as a single sample.
*/
/* package */ final class ExternallyLoadedMediaPeriod implements MediaPeriod {
private final Format format;
private final TrackGroupArray tracks;
private final byte[] sampleData;
// TODO: b/303375301 - Removing this variable (replacing it with static returns in the methods
// that
// use it) causes playback to hang.
private boolean loadingFinished;
public ExternallyLoadedMediaPeriod(Uri uri, String mimeType) {
this.format = new Format.Builder().setSampleMimeType(mimeType).build();
tracks = new TrackGroupArray(new TrackGroup(format));
sampleData = uri.toString().getBytes(Charsets.UTF_8);
}
@Override
public void prepare(Callback callback, long positionUs) {
callback.onPrepared(this);
}
@Override
public void maybeThrowPrepareError() {
// Do nothing.
}
@Override
public TrackGroupArray getTrackGroups() {
return tracks;
}
@Override
public long selectTracks(
@NullableType ExoTrackSelection[] selections,
boolean[] mayRetainStreamFlags,
@NullableType SampleStream[] streams,
boolean[] streamResetFlags,
long positionUs) {
for (int i = 0; i < selections.length; i++) {
if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) {
streams[i] = null;
}
if (streams[i] == null && selections[i] != null) {
SampleStreamImpl stream = new SampleStreamImpl();
streams[i] = stream;
streamResetFlags[i] = true;
}
}
return positionUs;
}
@Override
public void discardBuffer(long positionUs, boolean toKeyframe) {
// Do nothing.
}
@Override
public long readDiscontinuity() {
return C.TIME_UNSET;
}
@Override
public long seekToUs(long positionUs) {
return positionUs;
}
@Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs;
}
@Override
public long getBufferedPositionUs() {
return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
}
@Override
public long getNextLoadPositionUs() {
return loadingFinished ? C.TIME_END_OF_SOURCE : 0;
}
@Override
public boolean continueLoading(LoadingInfo loadingInfo) {
if (loadingFinished) {
return false;
}
loadingFinished = true;
return true;
}
@Override
public boolean isLoading() {
return !loadingFinished;
}
@Override
public void reevaluateBuffer(long positionUs) {
// Do nothing.
}
private final class SampleStreamImpl implements SampleStream {
private static final int STREAM_STATE_SEND_FORMAT = 0;
private static final int STREAM_STATE_SEND_SAMPLE = 1;
private static final int STREAM_STATE_END_OF_STREAM = 2;
private int streamState;
public SampleStreamImpl() {
streamState = STREAM_STATE_SEND_FORMAT;
}
@Override
public boolean isReady() {
return loadingFinished;
}
@Override
public void maybeThrowError() {
// Do nothing.
}
@Override
public @ReadDataResult int readData(
FormatHolder formatHolder, DecoderInputBuffer buffer, @ReadFlags int readFlags) {
if (streamState == STREAM_STATE_END_OF_STREAM) {
buffer.addFlag(C.BUFFER_FLAG_END_OF_STREAM);
return C.RESULT_BUFFER_READ;
}
if ((readFlags & FLAG_REQUIRE_FORMAT) != 0 || streamState == STREAM_STATE_SEND_FORMAT) {
formatHolder.format = tracks.get(0).getFormat(0);
streamState = STREAM_STATE_SEND_SAMPLE;
return C.RESULT_FORMAT_READ;
}
int sampleSize = sampleData.length;
buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME);
buffer.timeUs = 0;
if ((readFlags & FLAG_OMIT_SAMPLE_DATA) == 0) {
buffer.ensureSpaceForWrite(sampleSize);
buffer.data.put(sampleData, /* offset= */ 0, sampleSize);
}
if ((readFlags & FLAG_PEEK) == 0) {
streamState = STREAM_STATE_END_OF_STREAM;
}
return C.RESULT_BUFFER_READ;
}
@Override
public int skipData(long positionUs) {
// We should never skip our sample because the sample before any positive time is our only
// sample in the stream.
return 0;
}
}
}

View File

@ -0,0 +1,130 @@
/*
* 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.common.util.Assertions.checkNotNull;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.drm.DrmSessionManagerProvider;
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy;
import com.google.common.base.Charsets;
/**
* A {@link MediaSource} for media loaded outside of the usual ExoPlayer loading mechanism.
*
* <p>Puts the {@link MediaItem.LocalConfiguration#uri} (encoded with {@link Charsets#UTF_8}) in a
* single sample belonging to a single {@link MediaPeriod}.
*
* <p>Typically used for image content that is managed by an external image management framework
* (for example, Glide).
*/
@UnstableApi
public final class ExternallyLoadedMediaSource extends BaseMediaSource {
/** Factory for {@link ExternallyLoadedMediaSource}. */
public static final class Factory implements MediaSource.Factory {
private final long timelineDurationUs;
/**
* Creates an instance.
*
* @param timelineDurationUs The duration of the {@link SinglePeriodTimeline} created.
*/
Factory(long timelineDurationUs) {
this.timelineDurationUs = timelineDurationUs;
}
/** Does nothing. {@link ExternallyLoadedMediaSource} does not support DRM. */
@Override
public MediaSource.Factory setDrmSessionManagerProvider(
DrmSessionManagerProvider drmSessionManagerProvider) {
return this;
}
/**
* Does nothing. {@link ExternallyLoadedMediaSource} does not support error handling policies.
*/
@Override
public MediaSource.Factory setLoadErrorHandlingPolicy(
LoadErrorHandlingPolicy loadErrorHandlingPolicy) {
return this;
}
@Override
public @C.ContentType int[] getSupportedTypes() {
return new int[] {C.CONTENT_TYPE_OTHER};
}
@Override
public ExternallyLoadedMediaSource createMediaSource(MediaItem mediaItem) {
return new ExternallyLoadedMediaSource(mediaItem, timelineDurationUs);
}
}
private final MediaItem mediaItem;
private final Timeline timeline;
private ExternallyLoadedMediaSource(MediaItem mediaItem, long timelineDurationUs) {
this.mediaItem = mediaItem;
this.timeline =
new SinglePeriodTimeline(
timelineDurationUs,
/* isSeekable= */ true,
/* isDynamic= */ false,
/* useLiveConfiguration= */ false,
/* manifest= */ null,
mediaItem);
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
refreshSourceInfo(timeline);
}
@Override
protected void releaseSourceInternal() {
// Do nothing.
}
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
@Override
public void maybeThrowSourceInfoRefreshError() {
// Do nothing.
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
checkNotNull(mediaItem.localConfiguration);
checkNotNull(
mediaItem.localConfiguration.mimeType, "Externally loaded mediaItems require a MIME type.");
return new ExternallyLoadedMediaPeriod(
mediaItem.localConfiguration.uri, mediaItem.localConfiguration.mimeType);
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright (C) 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.e2etest;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.annotation.GraphicsMode.Mode.NATIVE;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.util.Clock;
import androidx.media3.datasource.AssetDataSource;
import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.RendererCapabilities;
import androidx.media3.exoplayer.image.BitmapFactoryImageDecoder;
import androidx.media3.exoplayer.image.ImageDecoder;
import androidx.media3.exoplayer.image.ImageDecoderException;
import androidx.media3.test.utils.CapturingRenderersFactory;
import androidx.media3.test.utils.DumpFileAsserts;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.robolectric.PlaybackOutput;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Charsets;
import java.io.IOException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.GraphicsMode;
/** End-to-end tests using image content loaded from an injected image management framework. */
@RunWith(AndroidJUnit4.class)
@GraphicsMode(value = NATIVE)
public final class ExternallyLoadedImagePlaybackTest {
private static final String INPUT_FILE = "png/non-motion-photo-shortened.png";
@Test
public void test() throws Exception {
Context applicationContext = ApplicationProvider.getApplicationContext();
CapturingRenderersFactory renderersFactory =
new CapturingRenderersFactory(applicationContext, /* addImageRenderer= */ true)
.setImageDecoderFactory(new CustomImageDecoderFactory());
Clock clock = new FakeClock(/* isAutoAdvancing= */ true);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory).setClock(clock).build();
PlaybackOutput playbackOutput = PlaybackOutput.register(player, renderersFactory);
long durationMs = 5 * C.MILLIS_PER_SECOND;
player.setMediaItem(
new MediaItem.Builder()
.setUri("asset:///media/" + INPUT_FILE)
.setImageDurationMs(durationMs)
.setMimeType(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
.build());
player.prepare();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_READY);
long playerStartedMs = clock.elapsedRealtime();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
long playbackDurationMs = clock.elapsedRealtime() - playerStartedMs;
player.release();
assertThat(playbackDurationMs).isAtLeast(durationMs);
DumpFileAsserts.assertOutput(
applicationContext, playbackOutput, "playbackdumps/" + INPUT_FILE + ".dump");
}
private static final class CustomImageDecoderFactory implements ImageDecoder.Factory {
@Override
public @RendererCapabilities.Capabilities int supportsFormat(Format format) {
return format.sampleMimeType.equals(MimeTypes.APPLICATION_EXTERNALLY_LOADED_IMAGE)
? RendererCapabilities.create(C.FORMAT_HANDLED)
: RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
@Override
public ImageDecoder createImageDecoder() {
return new BitmapFactoryImageDecoder.Factory(ExternallyLoadedImagePlaybackTest::decode)
.createImageDecoder();
}
}
private static Bitmap decode(byte[] data, int length) throws ImageDecoderException {
String uriString = new String(data, Charsets.UTF_8);
AssetDataSource assetDataSource =
new AssetDataSource(ApplicationProvider.getApplicationContext());
DataSpec dataSpec = new DataSpec(Uri.parse(uriString));
@Nullable Bitmap bitmap;
try {
assetDataSource.open(dataSpec);
byte[] imageData = DataSourceUtil.readToEnd(assetDataSource);
bitmap = BitmapFactory.decodeByteArray(imageData, /* offset= */ 0, imageData.length);
} catch (IOException e) {
throw new ImageDecoderException(e);
}
if (bitmap == null) {
throw new ImageDecoderException(
"Could not decode image data with BitmapFactory. uriString decoded from data = "
+ uriString);
}
return bitmap;
}
}

View File

@ -73,6 +73,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory;
private final CapturingAudioSink audioSink;
private final CapturingImageOutput imageOutput;
private ImageDecoder.Factory imageDecoderFactory;
/**
* Creates an instance.
@ -96,6 +97,23 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
this.audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder(context).build());
this.imageOutput = new CapturingImageOutput();
this.addImageRenderer = addImageRenderer;
this.imageDecoderFactory = ImageDecoder.Factory.DEFAULT;
}
/**
* Sets the {@link ImageDecoder.Factory} used by the {@link ImageRenderer}.
*
* <p>Must {@code addImageRenderer} when creating the {@link
* CapturingRenderersFactory#CapturingRenderersFactory(Context, boolean)}.
*
* @param imageDecoderFactory The {@link ImageDecoder.Factory}.
* @return This factory, for convenience.
*/
public CapturingRenderersFactory setImageDecoderFactory(
ImageDecoder.Factory imageDecoderFactory) {
checkState(addImageRenderer);
this.imageDecoderFactory = imageDecoderFactory;
return this;
}
@Override
@ -149,7 +167,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
temp.add(new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper()));
if (addImageRenderer) {
temp.add(new ImageRenderer(ImageDecoder.Factory.DEFAULT, imageOutput));
temp.add(new ImageRenderer(imageDecoderFactory, imageOutput));
}
return temp.toArray(new Renderer[] {});
}