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);
}
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;

View file

@ -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(

View file

@ -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));
}
});
}
}
}