Add ExoPlayerAssetLoader
Just move some code around for now, to start setting up the overall structure. PiperOrigin-RevId: 487229329 (cherry picked from commit 95f37b4df8475e5eaf09da0a5d4dee04a81646a0)
This commit is contained in:
parent
d262f76047
commit
c89ceb878d
@ -0,0 +1,325 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2022 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.transformer;
|
||||||
|
|
||||||
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
|
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
||||||
|
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
|
||||||
|
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
|
||||||
|
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
||||||
|
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
|
||||||
|
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION;
|
||||||
|
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE;
|
||||||
|
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
|
||||||
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.media3.common.C;
|
||||||
|
import androidx.media3.common.DebugViewProvider;
|
||||||
|
import androidx.media3.common.Effect;
|
||||||
|
import androidx.media3.common.FrameProcessor;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.PlaybackException;
|
||||||
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.Timeline;
|
||||||
|
import androidx.media3.common.Tracks;
|
||||||
|
import androidx.media3.common.util.Clock;
|
||||||
|
import androidx.media3.exoplayer.DefaultLoadControl;
|
||||||
|
import androidx.media3.exoplayer.ExoPlayer;
|
||||||
|
import androidx.media3.exoplayer.Renderer;
|
||||||
|
import androidx.media3.exoplayer.RenderersFactory;
|
||||||
|
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
||||||
|
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
||||||
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
|
import androidx.media3.exoplayer.text.TextOutput;
|
||||||
|
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
||||||
|
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
|
||||||
|
/* package */ final class ExoPlayerAssetLoader {
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
|
||||||
|
void onEnded();
|
||||||
|
|
||||||
|
void onError(Exception e);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final TransformationRequest transformationRequest;
|
||||||
|
private final ImmutableList<Effect> videoEffects;
|
||||||
|
private final boolean removeAudio;
|
||||||
|
private final boolean removeVideo;
|
||||||
|
private final MediaSource.Factory mediaSourceFactory;
|
||||||
|
private final Codec.DecoderFactory decoderFactory;
|
||||||
|
private final Codec.EncoderFactory encoderFactory;
|
||||||
|
private final FrameProcessor.Factory frameProcessorFactory;
|
||||||
|
private final Looper looper;
|
||||||
|
private final DebugViewProvider debugViewProvider;
|
||||||
|
private final Clock clock;
|
||||||
|
|
||||||
|
private @MonotonicNonNull MuxerWrapper muxerWrapper;
|
||||||
|
@Nullable private ExoPlayer player;
|
||||||
|
private @Transformer.ProgressState int progressState;
|
||||||
|
|
||||||
|
public ExoPlayerAssetLoader(
|
||||||
|
Context context,
|
||||||
|
TransformationRequest transformationRequest,
|
||||||
|
ImmutableList<Effect> videoEffects,
|
||||||
|
boolean removeAudio,
|
||||||
|
boolean removeVideo,
|
||||||
|
MediaSource.Factory mediaSourceFactory,
|
||||||
|
Codec.DecoderFactory decoderFactory,
|
||||||
|
Codec.EncoderFactory encoderFactory,
|
||||||
|
FrameProcessor.Factory frameProcessorFactory,
|
||||||
|
Looper looper,
|
||||||
|
DebugViewProvider debugViewProvider,
|
||||||
|
Clock clock) {
|
||||||
|
this.context = context;
|
||||||
|
this.transformationRequest = transformationRequest;
|
||||||
|
this.videoEffects = videoEffects;
|
||||||
|
this.removeAudio = removeAudio;
|
||||||
|
this.removeVideo = removeVideo;
|
||||||
|
this.mediaSourceFactory = mediaSourceFactory;
|
||||||
|
this.decoderFactory = decoderFactory;
|
||||||
|
this.encoderFactory = encoderFactory;
|
||||||
|
this.frameProcessorFactory = frameProcessorFactory;
|
||||||
|
this.looper = looper;
|
||||||
|
this.debugViewProvider = debugViewProvider;
|
||||||
|
this.clock = clock;
|
||||||
|
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start(
|
||||||
|
MediaItem mediaItem,
|
||||||
|
MuxerWrapper muxerWrapper,
|
||||||
|
Listener listener,
|
||||||
|
FallbackListener fallbackListener,
|
||||||
|
Transformer.AsyncErrorListener asyncErrorListener) {
|
||||||
|
this.muxerWrapper = muxerWrapper;
|
||||||
|
|
||||||
|
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
|
||||||
|
trackSelector.setParameters(
|
||||||
|
new DefaultTrackSelector.Parameters.Builder(context)
|
||||||
|
.setForceHighestSupportedBitrate(true)
|
||||||
|
.build());
|
||||||
|
// Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
|
||||||
|
// muxer (rebuffers are less problematic for the transformation use case).
|
||||||
|
DefaultLoadControl loadControl =
|
||||||
|
new DefaultLoadControl.Builder()
|
||||||
|
.setBufferDurationsMs(
|
||||||
|
DEFAULT_MIN_BUFFER_MS,
|
||||||
|
DEFAULT_MAX_BUFFER_MS,
|
||||||
|
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
|
||||||
|
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
|
||||||
|
.build();
|
||||||
|
ExoPlayer.Builder playerBuilder =
|
||||||
|
new ExoPlayer.Builder(
|
||||||
|
context,
|
||||||
|
new RenderersFactoryImpl(
|
||||||
|
context,
|
||||||
|
muxerWrapper,
|
||||||
|
removeAudio,
|
||||||
|
removeVideo,
|
||||||
|
transformationRequest,
|
||||||
|
mediaItem.clippingConfiguration.startsAtKeyFrame,
|
||||||
|
videoEffects,
|
||||||
|
frameProcessorFactory,
|
||||||
|
encoderFactory,
|
||||||
|
decoderFactory,
|
||||||
|
fallbackListener,
|
||||||
|
asyncErrorListener,
|
||||||
|
debugViewProvider))
|
||||||
|
.setMediaSourceFactory(mediaSourceFactory)
|
||||||
|
.setTrackSelector(trackSelector)
|
||||||
|
.setLoadControl(loadControl)
|
||||||
|
.setLooper(looper);
|
||||||
|
if (clock != Clock.DEFAULT) {
|
||||||
|
// Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default
|
||||||
|
// clock we must be in a test context.
|
||||||
|
@SuppressWarnings("VisibleForTests")
|
||||||
|
ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
player = playerBuilder.build();
|
||||||
|
player.setMediaItem(mediaItem);
|
||||||
|
player.addListener(new PlayerListener(listener));
|
||||||
|
player.prepare();
|
||||||
|
|
||||||
|
progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
|
||||||
|
if (progressState == PROGRESS_STATE_AVAILABLE) {
|
||||||
|
Player player = checkNotNull(this.player);
|
||||||
|
long durationMs = player.getDuration();
|
||||||
|
long positionMs = player.getCurrentPosition();
|
||||||
|
progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
|
||||||
|
}
|
||||||
|
return progressState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void release() {
|
||||||
|
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
|
||||||
|
if (player != null) {
|
||||||
|
player.release();
|
||||||
|
player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class RenderersFactoryImpl implements RenderersFactory {
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final MuxerWrapper muxerWrapper;
|
||||||
|
private final TransformerMediaClock mediaClock;
|
||||||
|
private final boolean removeAudio;
|
||||||
|
private final boolean removeVideo;
|
||||||
|
private final TransformationRequest transformationRequest;
|
||||||
|
private final boolean clippingStartsAtKeyFrame;
|
||||||
|
private final ImmutableList<Effect> videoEffects;
|
||||||
|
private final FrameProcessor.Factory frameProcessorFactory;
|
||||||
|
private final Codec.EncoderFactory encoderFactory;
|
||||||
|
private final Codec.DecoderFactory decoderFactory;
|
||||||
|
private final FallbackListener fallbackListener;
|
||||||
|
private final Transformer.AsyncErrorListener asyncErrorListener;
|
||||||
|
private final DebugViewProvider debugViewProvider;
|
||||||
|
|
||||||
|
public RenderersFactoryImpl(
|
||||||
|
Context context,
|
||||||
|
MuxerWrapper muxerWrapper,
|
||||||
|
boolean removeAudio,
|
||||||
|
boolean removeVideo,
|
||||||
|
TransformationRequest transformationRequest,
|
||||||
|
boolean clippingStartsAtKeyFrame,
|
||||||
|
ImmutableList<Effect> videoEffects,
|
||||||
|
FrameProcessor.Factory frameProcessorFactory,
|
||||||
|
Codec.EncoderFactory encoderFactory,
|
||||||
|
Codec.DecoderFactory decoderFactory,
|
||||||
|
FallbackListener fallbackListener,
|
||||||
|
Transformer.AsyncErrorListener asyncErrorListener,
|
||||||
|
DebugViewProvider debugViewProvider) {
|
||||||
|
this.context = context;
|
||||||
|
this.muxerWrapper = muxerWrapper;
|
||||||
|
this.removeAudio = removeAudio;
|
||||||
|
this.removeVideo = removeVideo;
|
||||||
|
this.transformationRequest = transformationRequest;
|
||||||
|
this.clippingStartsAtKeyFrame = clippingStartsAtKeyFrame;
|
||||||
|
this.videoEffects = videoEffects;
|
||||||
|
this.frameProcessorFactory = frameProcessorFactory;
|
||||||
|
this.encoderFactory = encoderFactory;
|
||||||
|
this.decoderFactory = decoderFactory;
|
||||||
|
this.fallbackListener = fallbackListener;
|
||||||
|
this.asyncErrorListener = asyncErrorListener;
|
||||||
|
this.debugViewProvider = debugViewProvider;
|
||||||
|
mediaClock = new TransformerMediaClock();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Renderer[] createRenderers(
|
||||||
|
Handler eventHandler,
|
||||||
|
VideoRendererEventListener videoRendererEventListener,
|
||||||
|
AudioRendererEventListener audioRendererEventListener,
|
||||||
|
TextOutput textRendererOutput,
|
||||||
|
MetadataOutput metadataRendererOutput) {
|
||||||
|
int rendererCount = removeAudio || removeVideo ? 1 : 2;
|
||||||
|
Renderer[] renderers = new Renderer[rendererCount];
|
||||||
|
int index = 0;
|
||||||
|
if (!removeAudio) {
|
||||||
|
renderers[index] =
|
||||||
|
new TransformerAudioRenderer(
|
||||||
|
muxerWrapper,
|
||||||
|
mediaClock,
|
||||||
|
transformationRequest,
|
||||||
|
encoderFactory,
|
||||||
|
decoderFactory,
|
||||||
|
asyncErrorListener,
|
||||||
|
fallbackListener);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (!removeVideo) {
|
||||||
|
renderers[index] =
|
||||||
|
new TransformerVideoRenderer(
|
||||||
|
context,
|
||||||
|
muxerWrapper,
|
||||||
|
mediaClock,
|
||||||
|
transformationRequest,
|
||||||
|
clippingStartsAtKeyFrame,
|
||||||
|
videoEffects,
|
||||||
|
frameProcessorFactory,
|
||||||
|
encoderFactory,
|
||||||
|
decoderFactory,
|
||||||
|
asyncErrorListener,
|
||||||
|
fallbackListener,
|
||||||
|
debugViewProvider);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return renderers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class PlayerListener implements Player.Listener {
|
||||||
|
|
||||||
|
private final Listener listener;
|
||||||
|
|
||||||
|
public PlayerListener(Listener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlaybackStateChanged(int state) {
|
||||||
|
if (state == Player.STATE_ENDED) {
|
||||||
|
listener.onEnded();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTimelineChanged(Timeline timeline, int reason) {
|
||||||
|
if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Timeline.Window window = new Timeline.Window();
|
||||||
|
timeline.getWindow(/* windowIndex= */ 0, window);
|
||||||
|
if (!window.isPlaceholder) {
|
||||||
|
long durationUs = window.durationUs;
|
||||||
|
// Make progress permanently unavailable if the duration is unknown, so that it doesn't jump
|
||||||
|
// to a high value at the end of the transformation if the duration is set once the media is
|
||||||
|
// entirely loaded.
|
||||||
|
progressState =
|
||||||
|
durationUs <= 0 || durationUs == C.TIME_UNSET
|
||||||
|
? PROGRESS_STATE_UNAVAILABLE
|
||||||
|
: PROGRESS_STATE_AVAILABLE;
|
||||||
|
checkNotNull(player).play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTracksChanged(Tracks tracks) {
|
||||||
|
if (checkNotNull(muxerWrapper).getTrackCount() == 0) {
|
||||||
|
listener.onError(new IllegalStateException("The output does not contain any tracks."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerError(PlaybackException error) {
|
||||||
|
listener.onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -18,11 +18,6 @@ package androidx.media3.transformer;
|
|||||||
|
|
||||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||||
import static androidx.media3.common.util.Assertions.checkState;
|
import static androidx.media3.common.util.Assertions.checkState;
|
||||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
|
|
||||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
|
|
||||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
|
|
||||||
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
|
|
||||||
import static java.lang.Math.min;
|
|
||||||
import static java.lang.annotation.ElementType.TYPE_USE;
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -41,9 +36,6 @@ import androidx.media3.common.MediaItem;
|
|||||||
import androidx.media3.common.MediaLibraryInfo;
|
import androidx.media3.common.MediaLibraryInfo;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.PlaybackException;
|
import androidx.media3.common.PlaybackException;
|
||||||
import androidx.media3.common.Player;
|
|
||||||
import androidx.media3.common.Timeline;
|
|
||||||
import androidx.media3.common.Tracks;
|
|
||||||
import androidx.media3.common.util.Clock;
|
import androidx.media3.common.util.Clock;
|
||||||
import androidx.media3.common.util.ListenerSet;
|
import androidx.media3.common.util.ListenerSet;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
@ -51,17 +43,8 @@ import androidx.media3.common.util.Util;
|
|||||||
import androidx.media3.effect.GlEffect;
|
import androidx.media3.effect.GlEffect;
|
||||||
import androidx.media3.effect.GlEffectsFrameProcessor;
|
import androidx.media3.effect.GlEffectsFrameProcessor;
|
||||||
import androidx.media3.effect.GlMatrixTransformation;
|
import androidx.media3.effect.GlMatrixTransformation;
|
||||||
import androidx.media3.exoplayer.DefaultLoadControl;
|
|
||||||
import androidx.media3.exoplayer.ExoPlayer;
|
|
||||||
import androidx.media3.exoplayer.Renderer;
|
|
||||||
import androidx.media3.exoplayer.RenderersFactory;
|
|
||||||
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
|
|
||||||
import androidx.media3.exoplayer.metadata.MetadataOutput;
|
|
||||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
|
||||||
import androidx.media3.exoplayer.source.MediaSource;
|
import androidx.media3.exoplayer.source.MediaSource;
|
||||||
import androidx.media3.exoplayer.text.TextOutput;
|
|
||||||
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
|
|
||||||
import androidx.media3.exoplayer.video.VideoRendererEventListener;
|
|
||||||
import androidx.media3.extractor.DefaultExtractorsFactory;
|
import androidx.media3.extractor.DefaultExtractorsFactory;
|
||||||
import androidx.media3.extractor.mp4.Mp4Extractor;
|
import androidx.media3.extractor.mp4.Mp4Extractor;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
@ -571,12 +554,12 @@ public final class Transformer {
|
|||||||
private final Looper looper;
|
private final Looper looper;
|
||||||
private final DebugViewProvider debugViewProvider;
|
private final DebugViewProvider debugViewProvider;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
private final ExoPlayerAssetLoader exoPlayerAssetLoader;
|
||||||
|
|
||||||
@Nullable private MuxerWrapper muxerWrapper;
|
@Nullable private MuxerWrapper muxerWrapper;
|
||||||
@Nullable private ExoPlayer player;
|
|
||||||
@Nullable private String outputPath;
|
@Nullable private String outputPath;
|
||||||
@Nullable private ParcelFileDescriptor outputParcelFileDescriptor;
|
@Nullable private ParcelFileDescriptor outputParcelFileDescriptor;
|
||||||
private @ProgressState int progressState;
|
private boolean transformationInProgress;
|
||||||
private boolean isCancelling;
|
private boolean isCancelling;
|
||||||
|
|
||||||
private Transformer(
|
private Transformer(
|
||||||
@ -609,7 +592,20 @@ public final class Transformer {
|
|||||||
this.looper = looper;
|
this.looper = looper;
|
||||||
this.debugViewProvider = debugViewProvider;
|
this.debugViewProvider = debugViewProvider;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
|
exoPlayerAssetLoader =
|
||||||
|
new ExoPlayerAssetLoader(
|
||||||
|
context,
|
||||||
|
transformationRequest,
|
||||||
|
videoEffects,
|
||||||
|
removeAudio,
|
||||||
|
removeVideo,
|
||||||
|
mediaSourceFactory,
|
||||||
|
decoderFactory,
|
||||||
|
encoderFactory,
|
||||||
|
frameProcessorFactory,
|
||||||
|
looper,
|
||||||
|
debugViewProvider,
|
||||||
|
clock);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
|
/** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
|
||||||
@ -721,66 +717,26 @@ public final class Transformer {
|
|||||||
|
|
||||||
private void startTransformationInternal(MediaItem mediaItem) {
|
private void startTransformationInternal(MediaItem mediaItem) {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
if (player != null) {
|
if (transformationInProgress) {
|
||||||
throw new IllegalStateException("There is already a transformation in progress.");
|
throw new IllegalStateException("There is already a transformation in progress.");
|
||||||
}
|
}
|
||||||
TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
|
transformationInProgress = true;
|
||||||
|
ComponentListener componentListener = new ComponentListener(mediaItem, looper);
|
||||||
MuxerWrapper muxerWrapper =
|
MuxerWrapper muxerWrapper =
|
||||||
new MuxerWrapper(
|
new MuxerWrapper(
|
||||||
outputPath,
|
outputPath,
|
||||||
outputParcelFileDescriptor,
|
outputParcelFileDescriptor,
|
||||||
muxerFactory,
|
muxerFactory,
|
||||||
/* asyncErrorListener= */ playerListener);
|
/* asyncErrorListener= */ componentListener);
|
||||||
this.muxerWrapper = muxerWrapper;
|
this.muxerWrapper = muxerWrapper;
|
||||||
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
|
FallbackListener fallbackListener =
|
||||||
trackSelector.setParameters(
|
new FallbackListener(mediaItem, listeners, transformationRequest);
|
||||||
new DefaultTrackSelector.ParametersBuilder(context)
|
exoPlayerAssetLoader.start(
|
||||||
.setForceHighestSupportedBitrate(true)
|
mediaItem,
|
||||||
.build());
|
|
||||||
// Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
|
|
||||||
// muxer (rebuffers are less problematic for the transformation use case).
|
|
||||||
DefaultLoadControl loadControl =
|
|
||||||
new DefaultLoadControl.Builder()
|
|
||||||
.setBufferDurationsMs(
|
|
||||||
DEFAULT_MIN_BUFFER_MS,
|
|
||||||
DEFAULT_MAX_BUFFER_MS,
|
|
||||||
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
|
|
||||||
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
|
|
||||||
.build();
|
|
||||||
ExoPlayer.Builder playerBuilder =
|
|
||||||
new ExoPlayer.Builder(
|
|
||||||
context,
|
|
||||||
new TransformerRenderersFactory(
|
|
||||||
context,
|
|
||||||
muxerWrapper,
|
muxerWrapper,
|
||||||
removeAudio,
|
/* listener= */ componentListener,
|
||||||
removeVideo,
|
fallbackListener,
|
||||||
transformationRequest,
|
/* asyncErrorListener= */ componentListener);
|
||||||
mediaItem.clippingConfiguration.startsAtKeyFrame,
|
|
||||||
videoEffects,
|
|
||||||
frameProcessorFactory,
|
|
||||||
encoderFactory,
|
|
||||||
decoderFactory,
|
|
||||||
new FallbackListener(mediaItem, listeners, transformationRequest),
|
|
||||||
/* asyncErrorListener= */ playerListener,
|
|
||||||
debugViewProvider))
|
|
||||||
.setMediaSourceFactory(mediaSourceFactory)
|
|
||||||
.setTrackSelector(trackSelector)
|
|
||||||
.setLoadControl(loadControl)
|
|
||||||
.setLooper(looper);
|
|
||||||
if (clock != Clock.DEFAULT) {
|
|
||||||
// Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default
|
|
||||||
// clock we must be in a test context.
|
|
||||||
@SuppressWarnings("VisibleForTests")
|
|
||||||
ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock);
|
|
||||||
}
|
|
||||||
|
|
||||||
player = playerBuilder.build();
|
|
||||||
player.setMediaItem(mediaItem);
|
|
||||||
player.addListener(playerListener);
|
|
||||||
player.prepare();
|
|
||||||
|
|
||||||
progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -806,13 +762,7 @@ public final class Transformer {
|
|||||||
*/
|
*/
|
||||||
public @ProgressState int getProgress(ProgressHolder progressHolder) {
|
public @ProgressState int getProgress(ProgressHolder progressHolder) {
|
||||||
verifyApplicationThread();
|
verifyApplicationThread();
|
||||||
if (progressState == PROGRESS_STATE_AVAILABLE) {
|
return exoPlayerAssetLoader.getProgress(progressHolder);
|
||||||
Player player = checkNotNull(this.player);
|
|
||||||
long durationMs = player.getDuration();
|
|
||||||
long positionMs = player.getCurrentPosition();
|
|
||||||
progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
|
|
||||||
}
|
|
||||||
return progressState;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -821,6 +771,7 @@ public final class Transformer {
|
|||||||
* @throws IllegalStateException If this method is called from the wrong thread.
|
* @throws IllegalStateException If this method is called from the wrong thread.
|
||||||
*/
|
*/
|
||||||
public void cancel() {
|
public void cancel() {
|
||||||
|
verifyApplicationThread();
|
||||||
isCancelling = true;
|
isCancelling = true;
|
||||||
try {
|
try {
|
||||||
releaseResources(/* forCancellation= */ true);
|
releaseResources(/* forCancellation= */ true);
|
||||||
@ -840,12 +791,8 @@ public final class Transformer {
|
|||||||
* is false.
|
* is false.
|
||||||
*/
|
*/
|
||||||
private void releaseResources(boolean forCancellation) throws TransformationException {
|
private void releaseResources(boolean forCancellation) throws TransformationException {
|
||||||
verifyApplicationThread();
|
transformationInProgress = false;
|
||||||
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
|
exoPlayerAssetLoader.release();
|
||||||
if (player != null) {
|
|
||||||
player.release();
|
|
||||||
player = null;
|
|
||||||
}
|
|
||||||
if (muxerWrapper != null) {
|
if (muxerWrapper != null) {
|
||||||
try {
|
try {
|
||||||
muxerWrapper.release(forCancellation);
|
muxerWrapper.release(forCancellation);
|
||||||
@ -883,147 +830,48 @@ public final class Transformer {
|
|||||||
return fileSize;
|
return fileSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class TransformerRenderersFactory implements RenderersFactory {
|
/** Listener for exceptions that occur during a transformation. */
|
||||||
|
/* package */ interface AsyncErrorListener {
|
||||||
private final Context context;
|
/**
|
||||||
private final MuxerWrapper muxerWrapper;
|
* Called when a {@link TransformationException} occurs.
|
||||||
private final TransformerMediaClock mediaClock;
|
*
|
||||||
private final boolean removeAudio;
|
* <p>Can be called from any thread.
|
||||||
private final boolean removeVideo;
|
*/
|
||||||
private final TransformationRequest transformationRequest;
|
void onTransformationException(TransformationException exception);
|
||||||
private final boolean clippingStartsAtKeyFrame;
|
|
||||||
private final ImmutableList<Effect> videoEffects;
|
|
||||||
private final FrameProcessor.Factory frameProcessorFactory;
|
|
||||||
private final Codec.EncoderFactory encoderFactory;
|
|
||||||
private final Codec.DecoderFactory decoderFactory;
|
|
||||||
private final FallbackListener fallbackListener;
|
|
||||||
private final AsyncErrorListener asyncErrorListener;
|
|
||||||
private final DebugViewProvider debugViewProvider;
|
|
||||||
|
|
||||||
public TransformerRenderersFactory(
|
|
||||||
Context context,
|
|
||||||
MuxerWrapper muxerWrapper,
|
|
||||||
boolean removeAudio,
|
|
||||||
boolean removeVideo,
|
|
||||||
TransformationRequest transformationRequest,
|
|
||||||
boolean clippingStartsAtKeyFrame,
|
|
||||||
ImmutableList<Effect> videoEffects,
|
|
||||||
FrameProcessor.Factory frameProcessorFactory,
|
|
||||||
Codec.EncoderFactory encoderFactory,
|
|
||||||
Codec.DecoderFactory decoderFactory,
|
|
||||||
FallbackListener fallbackListener,
|
|
||||||
AsyncErrorListener asyncErrorListener,
|
|
||||||
DebugViewProvider debugViewProvider) {
|
|
||||||
this.context = context;
|
|
||||||
this.muxerWrapper = muxerWrapper;
|
|
||||||
this.removeAudio = removeAudio;
|
|
||||||
this.removeVideo = removeVideo;
|
|
||||||
this.transformationRequest = transformationRequest;
|
|
||||||
this.clippingStartsAtKeyFrame = clippingStartsAtKeyFrame;
|
|
||||||
this.videoEffects = videoEffects;
|
|
||||||
this.frameProcessorFactory = frameProcessorFactory;
|
|
||||||
this.encoderFactory = encoderFactory;
|
|
||||||
this.decoderFactory = decoderFactory;
|
|
||||||
this.fallbackListener = fallbackListener;
|
|
||||||
this.asyncErrorListener = asyncErrorListener;
|
|
||||||
this.debugViewProvider = debugViewProvider;
|
|
||||||
mediaClock = new TransformerMediaClock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private final class ComponentListener
|
||||||
public Renderer[] createRenderers(
|
implements ExoPlayerAssetLoader.Listener, AsyncErrorListener {
|
||||||
Handler eventHandler,
|
|
||||||
VideoRendererEventListener videoRendererEventListener,
|
|
||||||
AudioRendererEventListener audioRendererEventListener,
|
|
||||||
TextOutput textRendererOutput,
|
|
||||||
MetadataOutput metadataRendererOutput) {
|
|
||||||
int rendererCount = removeAudio || removeVideo ? 1 : 2;
|
|
||||||
Renderer[] renderers = new Renderer[rendererCount];
|
|
||||||
int index = 0;
|
|
||||||
if (!removeAudio) {
|
|
||||||
renderers[index] =
|
|
||||||
new TransformerAudioRenderer(
|
|
||||||
muxerWrapper,
|
|
||||||
mediaClock,
|
|
||||||
transformationRequest,
|
|
||||||
encoderFactory,
|
|
||||||
decoderFactory,
|
|
||||||
asyncErrorListener,
|
|
||||||
fallbackListener);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
if (!removeVideo) {
|
|
||||||
renderers[index] =
|
|
||||||
new TransformerVideoRenderer(
|
|
||||||
context,
|
|
||||||
muxerWrapper,
|
|
||||||
mediaClock,
|
|
||||||
transformationRequest,
|
|
||||||
clippingStartsAtKeyFrame,
|
|
||||||
videoEffects,
|
|
||||||
frameProcessorFactory,
|
|
||||||
encoderFactory,
|
|
||||||
decoderFactory,
|
|
||||||
asyncErrorListener,
|
|
||||||
fallbackListener,
|
|
||||||
debugViewProvider);
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
return renderers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final class TransformerPlayerListener implements Player.Listener, AsyncErrorListener {
|
|
||||||
|
|
||||||
private final MediaItem mediaItem;
|
private final MediaItem mediaItem;
|
||||||
private final Handler handler;
|
private final Handler handler;
|
||||||
|
|
||||||
public TransformerPlayerListener(MediaItem mediaItem, Looper looper) {
|
public ComponentListener(MediaItem mediaItem, Looper looper) {
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
handler = new Handler(looper);
|
handler = new Handler(looper);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPlaybackStateChanged(int state) {
|
public void onError(Exception e) {
|
||||||
if (state == Player.STATE_ENDED) {
|
TransformationException transformationException =
|
||||||
|
e instanceof PlaybackException
|
||||||
|
? TransformationException.createForPlaybackException((PlaybackException) e)
|
||||||
|
: TransformationException.createForUnexpected(e);
|
||||||
|
handleTransformationException(transformationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEnded() {
|
||||||
handleTransformationEnded(/* exception= */ null);
|
handleTransformationEnded(/* exception= */ null);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTimelineChanged(Timeline timeline, int reason) {
|
public void onTransformationException(TransformationException exception) {
|
||||||
if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
|
if (Looper.myLooper() == looper) {
|
||||||
return;
|
handleTransformationException(exception);
|
||||||
|
} else {
|
||||||
|
handler.post(() -> handleTransformationException(exception));
|
||||||
}
|
}
|
||||||
Timeline.Window window = new Timeline.Window();
|
|
||||||
timeline.getWindow(/* windowIndex= */ 0, window);
|
|
||||||
if (!window.isPlaceholder) {
|
|
||||||
long durationUs = window.durationUs;
|
|
||||||
// Make progress permanently unavailable if the duration is unknown, so that it doesn't jump
|
|
||||||
// to a high value at the end of the transformation if the duration is set once the media is
|
|
||||||
// entirely loaded.
|
|
||||||
progressState =
|
|
||||||
durationUs <= 0 || durationUs == C.TIME_UNSET
|
|
||||||
? PROGRESS_STATE_UNAVAILABLE
|
|
||||||
: PROGRESS_STATE_AVAILABLE;
|
|
||||||
checkNotNull(player).play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTracksChanged(Tracks tracks) {
|
|
||||||
if (checkNotNull(muxerWrapper).getTrackCount() == 0) {
|
|
||||||
handleTransformationEnded(
|
|
||||||
TransformationException.createForUnexpected(
|
|
||||||
new IllegalStateException("The output does not contain any tracks.")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlayerError(PlaybackException error) {
|
|
||||||
TransformationException transformationException =
|
|
||||||
TransformationException.createForPlaybackException(error);
|
|
||||||
handleTransformationException(transformationException);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleTransformationException(TransformationException transformationException) {
|
private void handleTransformationException(TransformationException transformationException) {
|
||||||
@ -1078,24 +926,5 @@ public final class Transformer {
|
|||||||
}
|
}
|
||||||
listeners.flushEvents();
|
listeners.flushEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTransformationException(TransformationException exception) {
|
|
||||||
if (Looper.myLooper() == looper) {
|
|
||||||
handleTransformationException(exception);
|
|
||||||
} else {
|
|
||||||
handler.post(() -> handleTransformationException(exception));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Listener for exceptions that occur during a transformation. */
|
|
||||||
/* package */ interface AsyncErrorListener {
|
|
||||||
/**
|
|
||||||
* Called when a {@link TransformationException} occurs.
|
|
||||||
*
|
|
||||||
* <p>Can be called from any thread.
|
|
||||||
*/
|
|
||||||
void onTransformationException(TransformationException exception);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user