Skip to content

Commit

Permalink
Add ExoPlayerAssetLoader
Browse files Browse the repository at this point in the history
Just move some code around for now, to start setting up the overall
structure.

PiperOrigin-RevId: 487229329
(cherry picked from commit 95f37b4)
  • Loading branch information
kim-vde authored and microkatz committed Nov 9, 2022
1 parent d262f76 commit c89ceb8
Show file tree
Hide file tree
Showing 2 changed files with 381 additions and 227 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Loading

0 comments on commit c89ceb8

Please sign in to comment.