mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
bf77290fbe
commit
2a0dc414da
3 changed files with 151 additions and 127 deletions
|
|
@ -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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue