Commit 2a0dc414 by kimvde Committed by Rohit Singh

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
parent bf77290f
......@@ -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;
......
......@@ -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(
......
......@@ -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;
}
}
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));
}
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
released = true;
HandlerWrapper playbackHandler =
clock.createHandler(exoPlayerAssetLoader.getPlaybackLooper(), /* callback= */ null);
playbackHandler.post(
() -> {
if (endTransformationReason == END_TRANSFORMATION_REASON_COMPLETED) {
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));
}
});
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment