From 2a0dc414da6df66a036d84e2c0f89411393d87f2 Mon Sep 17 00:00:00 2001 From: kimvde Date: Mon, 28 Nov 2022 11:35:57 +0000 Subject: [PATCH] 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 --- .../transformer/ExoPlayerAssetLoader.java | 12 +- .../exoplayer2/transformer/Transformer.java | 21 +- .../transformer/TransformerInternal.java | 245 ++++++++++-------- 3 files changed, 151 insertions(+), 127 deletions(-) diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java index 8148c3d1fc..7b2c326aab 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/ExoPlayerAssetLoader.java @@ -63,6 +63,7 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; void onError(Exception e); } + private final MediaItem mediaItem; private final ExoPlayer player; public ExoPlayerAssetLoader( @@ -71,8 +72,10 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; boolean removeAudio, boolean removeVideo, MediaSource.Factory mediaSourceFactory, + Looper looper, Listener listener, Clock clock) { + this.mediaItem = mediaItem; DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); trackSelector.setParameters( 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)) .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) - .setLoadControl(loadControl); + .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. @@ -101,11 +105,11 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; } player = playerBuilder.build(); - player.setMediaItem(mediaItem); player.addListener(new PlayerListener(listener)); } public void start() { + player.setMediaItem(mediaItem); player.prepare(); } @@ -113,10 +117,6 @@ import com.google.android.exoplayer2.video.VideoRendererEventListener; player.release(); } - public Looper getPlaybackLooper() { - return player.getPlaybackLooper(); - } - private static final class RenderersFactoryImpl implements RenderersFactory { private final TransformerMediaClock mediaClock; diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 10119be2bc..b235833df0 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -16,12 +16,10 @@ 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 java.lang.annotation.ElementType.TYPE_USE; import android.content.Context; -import android.os.Handler; import android.os.Looper; import android.os.ParcelFileDescriptor; 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.Effect; 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.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -732,6 +731,7 @@ public final class Transformer { encoderFactory, frameProcessorFactory, muxerFactory, + looper, transformerInternalListener, fallbackListener, debugViewProvider, @@ -778,11 +778,10 @@ public final class Transformer { return; } try { - transformerInternal.release(END_TRANSFORMATION_REASON_CANCELLED); - } catch (TransformationException impossible) { - throw new IllegalStateException(impossible); + transformerInternal.cancel(); + } finally { + transformerInternal = null; } - transformerInternal = null; } private void verifyApplicationThread() { @@ -794,18 +793,17 @@ public final class Transformer { private final class TransformerInternalListener implements TransformerInternal.Listener { private final MediaItem mediaItem; - private final Handler handler; + private final HandlerWrapper handler; public TransformerInternalListener(MediaItem mediaItem) { this.mediaItem = mediaItem; - handler = Util.createHandlerForCurrentLooper(); + handler = clock.createHandler(looper, /* callback= */ null); } @Override public void onTransformationCompleted(TransformationResult transformationResult) { // TODO(b/213341814): Add event flags for Transformer events. - Util.postOrRun( - handler, + handler.post( () -> { transformerInternal = null; listeners.queueEvent( @@ -817,8 +815,7 @@ public final class Transformer { @Override public void onTransformationError(TransformationException exception) { - Util.postOrRun( - handler, + handler.post( () -> { transformerInternal = null; listeners.queueEvent( diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java index a2aa89c219..3d4900eed0 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/TransformerInternal.java @@ -16,8 +16,8 @@ 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_NO_TRANSFORMATION; 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.util.Assertions.checkNotNull; @@ -25,7 +25,9 @@ import static java.lang.Math.min; import static java.lang.annotation.ElementType.TYPE_USE; 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 androidx.annotation.IntDef; 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 - * #END_TRANSFORMATION_REASON_COMPLETED}, {@link #END_TRANSFORMATION_REASON_CANCELLED} or {@link - * #END_TRANSFORMATION_REASON_ERROR}. + * Represents a reason for ending a transformation. May be one of {@link #END_REASON_COMPLETED}, + * {@link #END_REASON_CANCELLED} or {@link #END_REASON_ERROR}. */ @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) - @IntDef({ - END_TRANSFORMATION_REASON_COMPLETED, - END_TRANSFORMATION_REASON_CANCELLED, - END_TRANSFORMATION_REASON_ERROR - }) - public @interface EndTransformationReason {} - + @IntDef({END_REASON_COMPLETED, END_REASON_CANCELLED, END_REASON_ERROR}) + private @interface EndReason {} /** 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. */ - public static final int END_TRANSFORMATION_REASON_CANCELLED = 1; + private static final int END_REASON_CANCELLED = 1; /** 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 TransformationRequest transformationRequest; @@ -93,17 +93,18 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Listener listener; private final DebugViewProvider debugViewProvider; 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 MuxerWrapper muxerWrapper; - private final ConditionVariable releasingMuxerConditionVariable; + private final ConditionVariable cancellingConditionVariable; private @Transformer.ProgressState int progressState; private long progressPositionMs; private long durationMs; private boolean released; - private volatile @MonotonicNonNull TransformationResult transformationResult; - private volatile @MonotonicNonNull TransformationException releaseMuxerException; + private @MonotonicNonNull RuntimeException cancelException; public TransformerInternal( Context context, @@ -120,6 +121,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; Codec.EncoderFactory encoderFactory, FrameProcessor.Factory frameProcessorFactory, Muxer.Factory muxerFactory, + Looper applicationLooper, Listener listener, FallbackListener fallbackListener, DebugViewProvider debugViewProvider, @@ -134,14 +136,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.listener = listener; this.debugViewProvider = debugViewProvider; 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); - muxerWrapper = - new MuxerWrapper( - outputPath, - outputParcelFileDescriptor, - muxerFactory, - /* errorConsumer= */ componentListener::onTransformationError); exoPlayerAssetLoader = new ExoPlayerAssetLoader( context, @@ -149,14 +148,26 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; removeAudio, removeVideo, mediaSourceFactory, + internalLooper, componentListener, clock); - releasingMuxerConditionVariable = new ConditionVariable(); + muxerWrapper = + new MuxerWrapper( + outputPath, + outputParcelFileDescriptor, + muxerFactory, + /* errorConsumer= */ componentListener::onTransformationError); + cancellingConditionVariable = new ConditionVariable(); 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() { - exoPlayerAssetLoader.start(); + internalHandler.sendEmptyMessage(MSG_START); } public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) { @@ -166,26 +177,53 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; return progressState; } - /** - * Releases the resources. - * - * @param endTransformationReason The {@linkplain EndTransformationReason reason} for ending the - * transformation. - * @throws TransformationException If the muxer is in the wrong state and {@code - * endTransformationReason} is not {@link #END_TRANSFORMATION_REASON_CANCELLED}. - */ - public void release(@EndTransformationReason int endTransformationReason) - throws TransformationException { - if (released) { - return; + public void cancel() { + internalHandler + .obtainMessage( + MSG_END, END_REASON_CANCELLED, /* unused */ 0, /* transformationException */ null) + .sendToTarget(); + clock.onThreadBlocked(); + cancellingConditionVariable.blockUninterruptible(); + if (cancelException != null) { + throw cancelException; } - progressState = PROGRESS_STATE_NO_TRANSFORMATION; - released = true; - HandlerWrapper playbackHandler = - clock.createHandler(exoPlayerAssetLoader.getPlaybackLooper(), /* callback= */ null); - playbackHandler.post( - () -> { - if (endTransformationReason == END_TRANSFORMATION_REASON_COMPLETED) { + } + + private boolean handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_START: + 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 = new TransformationResult.Builder() .setDurationMs(checkNotNull(muxerWrapper).getDurationMs()) @@ -195,24 +233,37 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; .setFileSizeBytes(muxerWrapper.getCurrentOutputSizeBytes()) .build(); } - try { - muxerWrapper.release( - /* forCancellation= */ endTransformationReason - == END_TRANSFORMATION_REASON_CANCELLED); - } catch (Muxer.MuxerException e) { - releaseMuxerException = - TransformationException.createForMuxer( - e, TransformationException.ERROR_CODE_MUXING_FAILED); - } finally { - releasingMuxerConditionVariable.open(); - } - }); - clock.onThreadBlocked(); - releasingMuxerConditionVariable.blockUninterruptible(); - exoPlayerAssetLoader.release(); - if (releaseMuxerException != null) { - throw releaseMuxerException; + } finally { + muxerWrapper.release(forCancellation); + } + } catch (Muxer.MuxerException e) { + releaseTransformationException = + TransformationException.createForMuxer(e, ERROR_CODE_MUXING_FAILED); + } catch (RuntimeException e) { + releaseTransformationException = TransformationException.createForUnexpected(e); + cancelException = e; + } } + + 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 @@ -236,14 +287,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void onDurationMs(long durationMs) { - // 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 = - durationMs <= 0 || durationMs == C.TIME_UNSET - ? PROGRESS_STATE_UNAVAILABLE - : PROGRESS_STATE_AVAILABLE; - TransformerInternal.this.durationMs = durationMs; + applicationHandler.post( + () -> { + // 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 = + durationMs <= 0 || durationMs == C.TIME_UNSET + ? PROGRESS_STATE_UNAVAILABLE + : PROGRESS_STATE_AVAILABLE; + TransformerInternal.this.durationMs = durationMs; + }); } @Override @@ -278,12 +332,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } else { transformationException = TransformationException.createForUnexpected(e); } - handleTransformationEnded(transformationException); + onTransformationError(transformationException); } @Override public void onEnded() { - handleTransformationEnded(/* transformationException= */ null); + internalHandler + .obtainMessage( + MSG_END, END_REASON_COMPLETED, /* unused */ 0, /* transformationException */ null) + .sendToTarget(); } // SamplePipeline.Listener implementation. @@ -295,15 +352,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (elapsedTimeMs > lastProgressUpdateMs + MIN_DURATION_BETWEEN_PROGRESS_UPDATES_MS && positionMs > lastProgressPositionMs) { 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; - handler.post(() -> progressPositionMs = positionMs); + applicationHandler.post(() -> progressPositionMs = positionMs); } } @Override public void onTransformationError(TransformationException transformationException) { - handleTransformationEnded(transformationException); + internalHandler + .obtainMessage(MSG_END, END_REASON_ERROR, /* unused */ 0, transformationException) + .sendToTarget(); } private SamplePipeline getSamplePipeline( @@ -427,37 +486,5 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } 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)); - } - }); - } } }