Create ExternallyLoadedMediaPeriod and ExternallyLoadedMediaSource
PiperOrigin-RevId: 571292394
This commit is contained in:
parent
89d01981bc
commit
addfd3e986
@ -18,6 +18,7 @@ package androidx.media3.exoplayer.source;
|
|||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||||
import static androidx.media3.common.util.Util.castNonNull;
|
import static androidx.media3.common.util.Util.castNonNull;
|
||||||
|
import static androidx.media3.common.util.Util.msToUs;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
@ -60,6 +61,7 @@ import java.util.HashMap;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
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)) {
|
if (scheme != null && scheme.equals(C.SSAI_SCHEME)) {
|
||||||
return checkNotNull(serverSideAdInsertionMediaSourceFactory).createMediaSource(mediaItem);
|
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
|
@C.ContentType
|
||||||
int type =
|
int type =
|
||||||
Util.inferContentTypeForUriAndMimeType(
|
Util.inferContentTypeForUriAndMimeType(
|
||||||
@ -531,8 +539,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory {
|
|||||||
}
|
}
|
||||||
return new ClippingMediaSource(
|
return new ClippingMediaSource(
|
||||||
mediaSource,
|
mediaSource,
|
||||||
Util.msToUs(mediaItem.clippingConfiguration.startPositionMs),
|
msToUs(mediaItem.clippingConfiguration.startPositionMs),
|
||||||
Util.msToUs(mediaItem.clippingConfiguration.endPositionMs),
|
msToUs(mediaItem.clippingConfiguration.endPositionMs),
|
||||||
/* enableInitialDiscontinuity= */ !mediaItem.clippingConfiguration.startsAtKeyFrame,
|
/* enableInitialDiscontinuity= */ !mediaItem.clippingConfiguration.startsAtKeyFrame,
|
||||||
/* allowDynamicClippingUpdates= */ mediaItem.clippingConfiguration.relativeToLiveWindow,
|
/* allowDynamicClippingUpdates= */ mediaItem.clippingConfiguration.relativeToLiveWindow,
|
||||||
mediaItem.clippingConfiguration.relativeToDefaultPosition);
|
mediaItem.clippingConfiguration.relativeToDefaultPosition);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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) {}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -73,6 +73,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
|
|||||||
private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory;
|
private final CapturingMediaCodecAdapter.Factory mediaCodecAdapterFactory;
|
||||||
private final CapturingAudioSink audioSink;
|
private final CapturingAudioSink audioSink;
|
||||||
private final CapturingImageOutput imageOutput;
|
private final CapturingImageOutput imageOutput;
|
||||||
|
private ImageDecoder.Factory imageDecoderFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an instance.
|
* Creates an instance.
|
||||||
@ -96,6 +97,23 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
|
|||||||
this.audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder(context).build());
|
this.audioSink = new CapturingAudioSink(new DefaultAudioSink.Builder(context).build());
|
||||||
this.imageOutput = new CapturingImageOutput();
|
this.imageOutput = new CapturingImageOutput();
|
||||||
this.addImageRenderer = addImageRenderer;
|
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
|
@Override
|
||||||
@ -149,7 +167,7 @@ public class CapturingRenderersFactory implements RenderersFactory, Dumper.Dumpa
|
|||||||
temp.add(new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper()));
|
temp.add(new MetadataRenderer(metadataRendererOutput, eventHandler.getLooper()));
|
||||||
|
|
||||||
if (addImageRenderer) {
|
if (addImageRenderer) {
|
||||||
temp.add(new ImageRenderer(ImageDecoder.Factory.DEFAULT, imageOutput));
|
temp.add(new ImageRenderer(imageDecoderFactory, imageOutput));
|
||||||
}
|
}
|
||||||
return temp.toArray(new Renderer[] {});
|
return temp.toArray(new Renderer[] {});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user