Add Transformer internal thread

This thread just starts the player and handles the player callbacks for
now. Sample pipelines are still run on the playback thread.

PiperOrigin-RevId: 491299671
This commit is contained in:
kimvde 2022-11-28 11:35:57 +00:00 committed by Rohit Singh
parent bf77290fbe
commit 2a0dc414da
3 changed files with 151 additions and 127 deletions

View file

@ -63,6 +63,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
void onError(Exception e); void onError(Exception e);
} }
private final MediaItem mediaItem;
private final ExoPlayer player; private final ExoPlayer player;
public ExoPlayerAssetLoader( public ExoPlayerAssetLoader(
@ -71,8 +72,10 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
boolean removeAudio, boolean removeAudio,
boolean removeVideo, boolean removeVideo,
MediaSource.Factory mediaSourceFactory, MediaSource.Factory mediaSourceFactory,
Looper looper,
Listener listener, Listener listener,
Clock clock) { Clock clock) {
this.mediaItem = mediaItem;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters( trackSelector.setParameters(
new DefaultTrackSelector.Parameters.Builder(context) new DefaultTrackSelector.Parameters.Builder(context)
@ -92,7 +95,8 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
new ExoPlayer.Builder(context, new RenderersFactoryImpl(removeAudio, removeVideo, listener)) new ExoPlayer.Builder(context, new RenderersFactoryImpl(removeAudio, removeVideo, listener))
.setMediaSourceFactory(mediaSourceFactory) .setMediaSourceFactory(mediaSourceFactory)
.setTrackSelector(trackSelector) .setTrackSelector(trackSelector)
.setLoadControl(loadControl); .setLoadControl(loadControl)
.setLooper(looper);
if (clock != Clock.DEFAULT) { if (clock != Clock.DEFAULT) {
// Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default // Transformer.Builder#setClock is also @VisibleForTesting, so if we're using a non-default
// clock we must be in a test context. // clock we must be in a test context.
@ -101,11 +105,11 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
} }
player = playerBuilder.build(); player = playerBuilder.build();
player.setMediaItem(mediaItem);
player.addListener(new PlayerListener(listener)); player.addListener(new PlayerListener(listener));
} }
public void start() { public void start() {
player.setMediaItem(mediaItem);
player.prepare(); player.prepare();
} }
@ -113,10 +117,6 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener;
player.release(); player.release();
} }
public Looper getPlaybackLooper() {
return player.getPlaybackLooper();
}
private static final class RenderersFactoryImpl implements RenderersFactory { private static final class RenderersFactoryImpl implements RenderersFactory {
private final TransformerMediaClock mediaClock; private final TransformerMediaClock mediaClock;

View file

@ -16,12 +16,10 @@
package com.google.android.exoplayer2.transformer; package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.transformer.TransformerInternal.END_TRANSFORMATION_REASON_CANCELLED;
import static com.google.android.exoplayer2.util.Assertions.checkState; import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context; import android.content.Context;
import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
@ -43,6 +41,7 @@ import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.DebugViewProvider; import com.google.android.exoplayer2.util.DebugViewProvider;
import com.google.android.exoplayer2.util.Effect; import com.google.android.exoplayer2.util.Effect;
import com.google.android.exoplayer2.util.FrameProcessor; import com.google.android.exoplayer2.util.FrameProcessor;
import com.google.android.exoplayer2.util.HandlerWrapper;
import com.google.android.exoplayer2.util.ListenerSet; import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
@ -732,6 +731,7 @@ public final class Transformer {
encoderFactory, encoderFactory,
frameProcessorFactory, frameProcessorFactory,
muxerFactory, muxerFactory,
looper,
transformerInternalListener, transformerInternalListener,
fallbackListener, fallbackListener,
debugViewProvider, debugViewProvider,
@ -778,11 +778,10 @@ public final class Transformer {
return; return;
} }
try { try {
transformerInternal.release(END_TRANSFORMATION_REASON_CANCELLED); transformerInternal.cancel();
} catch (TransformationException impossible) { } finally {
throw new IllegalStateException(impossible); transformerInternal = null;
} }
transformerInternal = null;
} }
private void verifyApplicationThread() { private void verifyApplicationThread() {
@ -794,18 +793,17 @@ public final class Transformer {
private final class TransformerInternalListener implements TransformerInternal.Listener { private final class TransformerInternalListener implements TransformerInternal.Listener {
private final MediaItem mediaItem; private final MediaItem mediaItem;
private final Handler handler; private final HandlerWrapper handler;
public TransformerInternalListener(MediaItem mediaItem) { public TransformerInternalListener(MediaItem mediaItem) {
this.mediaItem = mediaItem; this.mediaItem = mediaItem;
handler = Util.createHandlerForCurrentLooper(); handler = clock.createHandler(looper, /* callback= */ null);
} }
@Override @Override
public void onTransformationCompleted(TransformationResult transformationResult) { public void onTransformationCompleted(TransformationResult transformationResult) {
// TODO(b/213341814): Add event flags for Transformer events. // TODO(b/213341814): Add event flags for Transformer events.
Util.postOrRun( handler.post(
handler,
() -> { () -> {
transformerInternal = null; transformerInternal = null;
listeners.queueEvent( listeners.queueEvent(
@ -817,8 +815,7 @@ public final class Transformer {
@Override @Override
public void onTransformationError(TransformationException exception) { public void onTransformationError(TransformationException exception) {
Util.postOrRun( handler.post(
handler,
() -> { () -> {
transformerInternal = null; transformerInternal = null;
listeners.queueEvent( listeners.queueEvent(

View file

@ -16,8 +16,8 @@
package com.google.android.exoplayer2.transformer; package com.google.android.exoplayer2.transformer;
import static com.google.android.exoplayer2.transformer.TransformationException.ERROR_CODE_MUXING_FAILED;
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_AVAILABLE; import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION;
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE; import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE;
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
@ -25,7 +25,9 @@ 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;
import android.os.Handler; import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -62,26 +64,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
/** /**
* Represents a reason for ending a transformation. May be one of {@link * Represents a reason for ending a transformation. May be one of {@link #END_REASON_COMPLETED},
* #END_TRANSFORMATION_REASON_COMPLETED}, {@link #END_TRANSFORMATION_REASON_CANCELLED} or {@link * {@link #END_REASON_CANCELLED} or {@link #END_REASON_ERROR}.
* #END_TRANSFORMATION_REASON_ERROR}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({ @IntDef({END_REASON_COMPLETED, END_REASON_CANCELLED, END_REASON_ERROR})
END_TRANSFORMATION_REASON_COMPLETED, private @interface EndReason {}
END_TRANSFORMATION_REASON_CANCELLED,
END_TRANSFORMATION_REASON_ERROR
})
public @interface EndTransformationReason {}
/** The transformation completed successfully. */ /** The transformation completed successfully. */
public static final int END_TRANSFORMATION_REASON_COMPLETED = 0; private static final int END_REASON_COMPLETED = 0;
/** The transformation was cancelled. */ /** The transformation was cancelled. */
public static final int END_TRANSFORMATION_REASON_CANCELLED = 1; private static final int END_REASON_CANCELLED = 1;
/** An error occurred during the transformation. */ /** An error occurred during the transformation. */
public static final int END_TRANSFORMATION_REASON_ERROR = 2; private static final int END_REASON_ERROR = 2;
// Internal messages.
private static final int MSG_START = 0;
private static final int MSG_END = 1;
private final Context context; private final Context context;
private final TransformationRequest transformationRequest; private final TransformationRequest transformationRequest;
@ -93,17 +93,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Listener listener; private final Listener listener;
private final DebugViewProvider debugViewProvider; private final DebugViewProvider debugViewProvider;
private final Clock clock; private final Clock clock;
private final Handler handler; private final HandlerWrapper applicationHandler;
private final HandlerThread internalHandlerThread;
private final HandlerWrapper internalHandler;
private final ExoPlayerAssetLoader exoPlayerAssetLoader; private final ExoPlayerAssetLoader exoPlayerAssetLoader;
private final MuxerWrapper muxerWrapper; private final MuxerWrapper muxerWrapper;
private final ConditionVariable releasingMuxerConditionVariable; private final ConditionVariable cancellingConditionVariable;
private @Transformer.ProgressState int progressState; private @Transformer.ProgressState int progressState;
private long progressPositionMs; private long progressPositionMs;
private long durationMs; private long durationMs;
private boolean released; private boolean released;
private volatile @MonotonicNonNull TransformationResult transformationResult; private @MonotonicNonNull RuntimeException cancelException;
private volatile @MonotonicNonNull TransformationException releaseMuxerException;
public TransformerInternal( public TransformerInternal(
Context context, Context context,
@ -120,6 +121,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
Codec.EncoderFactory encoderFactory, Codec.EncoderFactory encoderFactory,
FrameProcessor.Factory frameProcessorFactory, FrameProcessor.Factory frameProcessorFactory,
Muxer.Factory muxerFactory, Muxer.Factory muxerFactory,
Looper applicationLooper,
Listener listener, Listener listener,
FallbackListener fallbackListener, FallbackListener fallbackListener,
DebugViewProvider debugViewProvider, DebugViewProvider debugViewProvider,
@ -134,14 +136,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.listener = listener; this.listener = listener;
this.debugViewProvider = debugViewProvider; this.debugViewProvider = debugViewProvider;
this.clock = clock; this.clock = clock;
handler = Util.createHandlerForCurrentLooper(); applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null);
internalHandlerThread = new HandlerThread("Transformer:Internal");
internalHandlerThread.start();
Looper internalLooper = internalHandlerThread.getLooper();
ComponentListener componentListener = new ComponentListener(mediaItem, fallbackListener); ComponentListener componentListener = new ComponentListener(mediaItem, fallbackListener);
muxerWrapper =
new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* errorConsumer= */ componentListener::onTransformationError);
exoPlayerAssetLoader = exoPlayerAssetLoader =
new ExoPlayerAssetLoader( new ExoPlayerAssetLoader(
context, context,
@ -149,14 +148,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
removeAudio, removeAudio,
removeVideo, removeVideo,
mediaSourceFactory, mediaSourceFactory,
internalLooper,
componentListener, componentListener,
clock); clock);
releasingMuxerConditionVariable = new ConditionVariable(); muxerWrapper =
new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* errorConsumer= */ componentListener::onTransformationError);
cancellingConditionVariable = new ConditionVariable();
progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY; progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper internalHandler =
clock.createHandler(internalLooper, /* callback= */ this::handleMessage);
this.internalHandler = internalHandler;
} }
public void start() { public void start() {
exoPlayerAssetLoader.start(); internalHandler.sendEmptyMessage(MSG_START);
} }
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) { public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
@ -166,26 +177,53 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
return progressState; return progressState;
} }
/** public void cancel() {
* Releases the resources. internalHandler
* .obtainMessage(
* @param endTransformationReason The {@linkplain EndTransformationReason reason} for ending the MSG_END, END_REASON_CANCELLED, /* unused */ 0, /* transformationException */ null)
* transformation. .sendToTarget();
* @throws TransformationException If the muxer is in the wrong state and {@code clock.onThreadBlocked();
* endTransformationReason} is not {@link #END_TRANSFORMATION_REASON_CANCELLED}. cancellingConditionVariable.blockUninterruptible();
*/ if (cancelException != null) {
public void release(@EndTransformationReason int endTransformationReason) throw cancelException;
throws TransformationException {
if (released) {
return;
} }
progressState = PROGRESS_STATE_NO_TRANSFORMATION; }
released = true;
HandlerWrapper playbackHandler = private boolean handleMessage(Message msg) {
clock.createHandler(exoPlayerAssetLoader.getPlaybackLooper(), /* callback= */ null); try {
playbackHandler.post( switch (msg.what) {
() -> { case MSG_START:
if (endTransformationReason == END_TRANSFORMATION_REASON_COMPLETED) { startInternal();
break;
case MSG_END:
endInternal(
/* endReason= */ msg.arg1,
/* transformationException= */ (TransformationException) msg.obj);
break;
default:
return false;
}
} catch (RuntimeException e) {
endInternal(END_REASON_ERROR, TransformationException.createForUnexpected(e));
}
return true;
}
private void startInternal() {
exoPlayerAssetLoader.start();
}
private void endInternal(
@EndReason int endReason, @Nullable TransformationException transformationException) {
@Nullable TransformationResult transformationResult = null;
boolean forCancellation = endReason == END_REASON_CANCELLED;
@Nullable TransformationException releaseTransformationException = null;
if (!released) {
released = true;
try {
try {
exoPlayerAssetLoader.release();
if (endReason == END_REASON_COMPLETED) {
transformationResult = transformationResult =
new TransformationResult.Builder() new TransformationResult.Builder()
.setDurationMs(checkNotNull(muxerWrapper).getDurationMs()) .setDurationMs(checkNotNull(muxerWrapper).getDurationMs())
@ -195,24 +233,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
.setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes()) .setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes())
.build(); .build();
} }
try { } finally {
muxerWrapper.release( muxerWrapper.release(forCancellation);
/* forCancellation= */ endTransformationReason }
== END_TRANSFORMATION_REASON_CANCELLED); } catch (Muxer.MuxerException e) {
} catch (Muxer.MuxerException e) { releaseTransformationException =
releaseMuxerException = TransformationException.createForMuxer(e, ERROR_CODE_MUXING_FAILED);
TransformationException.createForMuxer( } catch (RuntimeException e) {
e, TransformationException.ERROR_CODE_MUXING_FAILED); releaseTransformationException = TransformationException.createForUnexpected(e);
} finally { cancelException = e;
releasingMuxerConditionVariable.open(); }
}
});
clock.onThreadBlocked();
releasingMuxerConditionVariable.blockUninterruptible();
exoPlayerAssetLoader.release();
if (releaseMuxerException != null) {
throw releaseMuxerException;
} }
if (!forCancellation) {
TransformationException exception = transformationException;
if (exception == null) {
// We only report the exception caused by releasing the resources if there is no other
// exception. It is more intuitive to call the error callback only once and reporting the
// exception caused by releasing the resources can be confusing if it is a consequence of
// the first exception.
exception = releaseTransformationException;
}
if (exception != null) {
listener.onTransformationError(exception);
} else {
listener.onTransformationCompleted(checkNotNull(transformationResult));
}
}
internalHandlerThread.quitSafely();
cancellingConditionVariable.open();
} }
private class ComponentListener private class ComponentListener
@ -236,14 +287,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@Override @Override
public void onDurationMs(long durationMs) { public void onDurationMs(long durationMs) {
// Make progress permanently unavailable if the duration is unknown, so that it doesn't jump applicationHandler.post(
// to a high value at the end of the transformation if the duration is set once the media is () -> {
// entirely loaded. // Make progress permanently unavailable if the duration is unknown, so that it doesn't
progressState = // jump to a high value at the end of the transformation if the duration is set once the
durationMs <= 0 || durationMs == C.TIME_UNSET // media is entirely loaded.
? PROGRESS_STATE_UNAVAILABLE progressState =
: PROGRESS_STATE_AVAILABLE; durationMs <= 0 || durationMs == C.TIME_UNSET
TransformerInternal.this.durationMs = durationMs; ? PROGRESS_STATE_UNAVAILABLE
: PROGRESS_STATE_AVAILABLE;
TransformerInternal.this.durationMs = durationMs;
});
} }
@Override @Override
@ -278,12 +332,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} else { } else {
transformationException = TransformationException.createForUnexpected(e); transformationException = TransformationException.createForUnexpected(e);
} }
handleTransformationEnded(transformationException); onTransformationError(transformationException);
} }
@Override @Override
public void onEnded() { public void onEnded() {
handleTransformationEnded(/* transformationException= */ null); internalHandler
.obtainMessage(
MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null)
.sendToTarget();
} }
// SamplePipeline.Listener implementation. // SamplePipeline.Listener implementation.
@ -295,15 +352,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
if (elapsedTimeMs > lastProgressUpdateMs + MIN_DURATION_BETWEEN_PROGRESS_UPDATES_MS if (elapsedTimeMs > lastProgressUpdateMs + MIN_DURATION_BETWEEN_PROGRESS_UPDATES_MS
&& positionMs > lastProgressPositionMs) { && positionMs > lastProgressPositionMs) {
lastProgressUpdateMs = elapsedTimeMs; lastProgressUpdateMs = elapsedTimeMs;
// Store positionMs in a local variable to make sure the thread reads the latest value. // Store positionMs in a variable to make sure the thread reads the latest value.
lastProgressPositionMs = positionMs; lastProgressPositionMs = positionMs;
handler.post(() -> progressPositionMs = positionMs); applicationHandler.post(() -> progressPositionMs = positionMs);
} }
} }
@Override @Override
public void onTransformationError(TransformationException transformationException) { public void onTransformationError(TransformationException transformationException) {
handleTransformationEnded(transformationException); internalHandler
.obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, transformationException)
.sendToTarget();
} }
private SamplePipeline getSamplePipeline( private SamplePipeline getSamplePipeline(
@ -427,37 +486,5 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
return false; return false;
} }
private void handleTransformationEnded(
@Nullable TransformationException transformationException) {
handler.post(
() -> {
@Nullable TransformationException releaseException = null;
try {
release(
transformationException == null
? END_TRANSFORMATION_REASON_COMPLETED
: END_TRANSFORMATION_REASON_ERROR);
} catch (TransformationException e) {
releaseException = e;
} catch (RuntimeException e) {
releaseException = TransformationException.createForUnexpected(e);
}
TransformationException exception = transformationException;
if (exception == null) {
// We only report the exception caused by releasing the resources if there is no other
// exception. It is more intuitive to call the error callback only once and reporting
// the exception caused by releasing the resources can be confusing if it is a
// consequence of the first exception.
exception = releaseException;
}
if (exception != null) {
listener.onTransformationError(exception);
} else {
listener.onTransformationCompleted(checkNotNull(transformationResult));
}
});
}
} }
} }