Add ExoPlayerAssetLoader

Just move some code around for now, to start setting up the overall
structure.

PiperOrigin-RevId: 487229329
(cherry picked from commit 95f37b4df8475e5eaf09da0a5d4dee04a81646a0)
This commit is contained in:
kimvde 2022-11-09 14:45:12 +00:00 committed by microkatz
parent d262f76047
commit c89ceb878d
2 changed files with 386 additions and 232 deletions

View File

@ -0,0 +1,325 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_AVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_NO_TRANSFORMATION;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE;
import static androidx.media3.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
import static java.lang.Math.min;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.FrameProcessor;
import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import com.google.common.collect.ImmutableList;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class ExoPlayerAssetLoader {
public interface Listener {
void onEnded();
void onError(Exception e);
}
private final Context context;
private final TransformationRequest transformationRequest;
private final ImmutableList<Effect> videoEffects;
private final boolean removeAudio;
private final boolean removeVideo;
private final MediaSource.Factory mediaSourceFactory;
private final Codec.DecoderFactory decoderFactory;
private final Codec.EncoderFactory encoderFactory;
private final FrameProcessor.Factory frameProcessorFactory;
private final Looper looper;
private final DebugViewProvider debugViewProvider;
private final Clock clock;
private @MonotonicNonNull MuxerWrapper muxerWrapper;
@Nullable private ExoPlayer player;
private @Transformer.ProgressState int progressState;
public ExoPlayerAssetLoader(
Context context,
TransformationRequest transformationRequest,
ImmutableList<Effect> videoEffects,
boolean removeAudio,
boolean removeVideo,
MediaSource.Factory mediaSourceFactory,
Codec.DecoderFactory decoderFactory,
Codec.EncoderFactory encoderFactory,
FrameProcessor.Factory frameProcessorFactory,
Looper looper,
DebugViewProvider debugViewProvider,
Clock clock) {
this.context = context;
this.transformationRequest = transformationRequest;
this.videoEffects = videoEffects;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.mediaSourceFactory = mediaSourceFactory;
this.decoderFactory = decoderFactory;
this.encoderFactory = encoderFactory;
this.frameProcessorFactory = frameProcessorFactory;
this.looper = looper;
this.debugViewProvider = debugViewProvider;
this.clock = clock;
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
}
public void start(
MediaItem mediaItem,
MuxerWrapper muxerWrapper,
Listener listener,
FallbackListener fallbackListener,
Transformer.AsyncErrorListener asyncErrorListener) {
this.muxerWrapper = muxerWrapper;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(
new DefaultTrackSelector.Parameters.Builder(context)
.setForceHighestSupportedBitrate(true)
.build());
// Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
// muxer (rebuffers are less problematic for the transformation use case).
DefaultLoadControl loadControl =
new DefaultLoadControl.Builder()
.setBufferDurationsMs(
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
.build();
ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(
context,
new RenderersFactoryImpl(
context,
muxerWrapper,
removeAudio,
removeVideo,
transformationRequest,
mediaItem.clippingConfiguration.startsAtKeyFrame,
videoEffects,
frameProcessorFactory,
encoderFactory,
decoderFactory,
fallbackListener,
asyncErrorListener,
debugViewProvider))
.setMediaSourceFactory(mediaSourceFactory)
.setTrackSelector(trackSelector)
.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.
@SuppressWarnings("VisibleForTests")
ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock);
}
player = playerBuilder.build();
player.setMediaItem(mediaItem);
player.addListener(new PlayerListener(listener));
player.prepare();
progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
}
public @Transformer.ProgressState int getProgress(ProgressHolder progressHolder) {
if (progressState == PROGRESS_STATE_AVAILABLE) {
Player player = checkNotNull(this.player);
long durationMs = player.getDuration();
long positionMs = player.getCurrentPosition();
progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
}
return progressState;
}
public void release() {
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
if (player != null) {
player.release();
player = null;
}
}
private static final class RenderersFactoryImpl implements RenderersFactory {
private final Context context;
private final MuxerWrapper muxerWrapper;
private final TransformerMediaClock mediaClock;
private final boolean removeAudio;
private final boolean removeVideo;
private final TransformationRequest transformationRequest;
private final boolean clippingStartsAtKeyFrame;
private final ImmutableList<Effect> videoEffects;
private final FrameProcessor.Factory frameProcessorFactory;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final FallbackListener fallbackListener;
private final Transformer.AsyncErrorListener asyncErrorListener;
private final DebugViewProvider debugViewProvider;
public RenderersFactoryImpl(
Context context,
MuxerWrapper muxerWrapper,
boolean removeAudio,
boolean removeVideo,
TransformationRequest transformationRequest,
boolean clippingStartsAtKeyFrame,
ImmutableList<Effect> videoEffects,
FrameProcessor.Factory frameProcessorFactory,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
FallbackListener fallbackListener,
Transformer.AsyncErrorListener asyncErrorListener,
DebugViewProvider debugViewProvider) {
this.context = context;
this.muxerWrapper = muxerWrapper;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.transformationRequest = transformationRequest;
this.clippingStartsAtKeyFrame = clippingStartsAtKeyFrame;
this.videoEffects = videoEffects;
this.frameProcessorFactory = frameProcessorFactory;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.fallbackListener = fallbackListener;
this.asyncErrorListener = asyncErrorListener;
this.debugViewProvider = debugViewProvider;
mediaClock = new TransformerMediaClock();
}
@Override
public Renderer[] createRenderers(
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
int rendererCount = removeAudio || removeVideo ? 1 : 2;
Renderer[] renderers = new Renderer[rendererCount];
int index = 0;
if (!removeAudio) {
renderers[index] =
new TransformerAudioRenderer(
muxerWrapper,
mediaClock,
transformationRequest,
encoderFactory,
decoderFactory,
asyncErrorListener,
fallbackListener);
index++;
}
if (!removeVideo) {
renderers[index] =
new TransformerVideoRenderer(
context,
muxerWrapper,
mediaClock,
transformationRequest,
clippingStartsAtKeyFrame,
videoEffects,
frameProcessorFactory,
encoderFactory,
decoderFactory,
asyncErrorListener,
fallbackListener,
debugViewProvider);
index++;
}
return renderers;
}
}
private final class PlayerListener implements Player.Listener {
private final Listener listener;
public PlayerListener(Listener listener) {
this.listener = listener;
}
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_ENDED) {
listener.onEnded();
}
}
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
return;
}
Timeline.Window window = new Timeline.Window();
timeline.getWindow(/* windowIndex= */ 0, window);
if (!window.isPlaceholder) {
long durationUs = window.durationUs;
// 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 =
durationUs <= 0 || durationUs == C.TIME_UNSET
? PROGRESS_STATE_UNAVAILABLE
: PROGRESS_STATE_AVAILABLE;
checkNotNull(player).play();
}
}
@Override
public void onTracksChanged(Tracks tracks) {
if (checkNotNull(muxerWrapper).getTrackCount() == 0) {
listener.onError(new IllegalStateException("The output does not contain any tracks."));
}
}
@Override
public void onPlayerError(PlaybackException error) {
listener.onError(error);
}
}
}

View File

@ -18,11 +18,6 @@ package androidx.media3.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MAX_BUFFER_MS;
import static androidx.media3.exoplayer.DefaultLoadControl.DEFAULT_MIN_BUFFER_MS;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context;
@ -41,9 +36,6 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi;
@ -51,17 +43,8 @@ import androidx.media3.common.util.Util;
import androidx.media3.effect.GlEffect;
import androidx.media3.effect.GlEffectsFrameProcessor;
import androidx.media3.effect.GlMatrixTransformation;
import androidx.media3.exoplayer.DefaultLoadControl;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import androidx.media3.extractor.DefaultExtractorsFactory;
import androidx.media3.extractor.mp4.Mp4Extractor;
import com.google.common.collect.ImmutableList;
@ -571,12 +554,12 @@ public final class Transformer {
private final Looper looper;
private final DebugViewProvider debugViewProvider;
private final Clock clock;
private final ExoPlayerAssetLoader exoPlayerAssetLoader;
@Nullable private MuxerWrapper muxerWrapper;
@Nullable private ExoPlayer player;
@Nullable private String outputPath;
@Nullable private ParcelFileDescriptor outputParcelFileDescriptor;
private @ProgressState int progressState;
private boolean transformationInProgress;
private boolean isCancelling;
private Transformer(
@ -609,7 +592,20 @@ public final class Transformer {
this.looper = looper;
this.debugViewProvider = debugViewProvider;
this.clock = clock;
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
exoPlayerAssetLoader =
new ExoPlayerAssetLoader(
context,
transformationRequest,
videoEffects,
removeAudio,
removeVideo,
mediaSourceFactory,
decoderFactory,
encoderFactory,
frameProcessorFactory,
looper,
debugViewProvider,
clock);
}
/** Returns a {@link Transformer.Builder} initialized with the values of this instance. */
@ -721,66 +717,26 @@ public final class Transformer {
private void startTransformationInternal(MediaItem mediaItem) {
verifyApplicationThread();
if (player != null) {
if (transformationInProgress) {
throw new IllegalStateException("There is already a transformation in progress.");
}
TransformerPlayerListener playerListener = new TransformerPlayerListener(mediaItem, looper);
transformationInProgress = true;
ComponentListener componentListener = new ComponentListener(mediaItem, looper);
MuxerWrapper muxerWrapper =
new MuxerWrapper(
outputPath,
outputParcelFileDescriptor,
muxerFactory,
/* asyncErrorListener= */ playerListener);
/* asyncErrorListener= */ componentListener);
this.muxerWrapper = muxerWrapper;
DefaultTrackSelector trackSelector = new DefaultTrackSelector(context);
trackSelector.setParameters(
new DefaultTrackSelector.ParametersBuilder(context)
.setForceHighestSupportedBitrate(true)
.build());
// Arbitrarily decrease buffers for playback so that samples start being sent earlier to the
// muxer (rebuffers are less problematic for the transformation use case).
DefaultLoadControl loadControl =
new DefaultLoadControl.Builder()
.setBufferDurationsMs(
DEFAULT_MIN_BUFFER_MS,
DEFAULT_MAX_BUFFER_MS,
DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10)
.build();
ExoPlayer.Builder playerBuilder =
new ExoPlayer.Builder(
context,
new TransformerRenderersFactory(
context,
muxerWrapper,
removeAudio,
removeVideo,
transformationRequest,
mediaItem.clippingConfiguration.startsAtKeyFrame,
videoEffects,
frameProcessorFactory,
encoderFactory,
decoderFactory,
new FallbackListener(mediaItem, listeners, transformationRequest),
/* asyncErrorListener= */ playerListener,
debugViewProvider))
.setMediaSourceFactory(mediaSourceFactory)
.setTrackSelector(trackSelector)
.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.
@SuppressWarnings("VisibleForTests")
ExoPlayer.Builder unusedForAnnotation = playerBuilder.setClock(clock);
}
player = playerBuilder.build();
player.setMediaItem(mediaItem);
player.addListener(playerListener);
player.prepare();
progressState = PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
FallbackListener fallbackListener =
new FallbackListener(mediaItem, listeners, transformationRequest);
exoPlayerAssetLoader.start(
mediaItem,
muxerWrapper,
/* listener= */ componentListener,
fallbackListener,
/* asyncErrorListener= */ componentListener);
}
/**
@ -806,13 +762,7 @@ public final class Transformer {
*/
public @ProgressState int getProgress(ProgressHolder progressHolder) {
verifyApplicationThread();
if (progressState == PROGRESS_STATE_AVAILABLE) {
Player player = checkNotNull(this.player);
long durationMs = player.getDuration();
long positionMs = player.getCurrentPosition();
progressHolder.progress = min((int) (positionMs * 100 / durationMs), 99);
}
return progressState;
return exoPlayerAssetLoader.getProgress(progressHolder);
}
/**
@ -821,6 +771,7 @@ public final class Transformer {
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void cancel() {
verifyApplicationThread();
isCancelling = true;
try {
releaseResources(/* forCancellation= */ true);
@ -840,12 +791,8 @@ public final class Transformer {
* is false.
*/
private void releaseResources(boolean forCancellation) throws TransformationException {
verifyApplicationThread();
progressState = PROGRESS_STATE_NO_TRANSFORMATION;
if (player != null) {
player.release();
player = null;
}
transformationInProgress = false;
exoPlayerAssetLoader.release();
if (muxerWrapper != null) {
try {
muxerWrapper.release(forCancellation);
@ -883,149 +830,50 @@ public final class Transformer {
return fileSize;
}
private static final class TransformerRenderersFactory implements RenderersFactory {
private final Context context;
private final MuxerWrapper muxerWrapper;
private final TransformerMediaClock mediaClock;
private final boolean removeAudio;
private final boolean removeVideo;
private final TransformationRequest transformationRequest;
private final boolean clippingStartsAtKeyFrame;
private final ImmutableList<Effect> videoEffects;
private final FrameProcessor.Factory frameProcessorFactory;
private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory;
private final FallbackListener fallbackListener;
private final AsyncErrorListener asyncErrorListener;
private final DebugViewProvider debugViewProvider;
public TransformerRenderersFactory(
Context context,
MuxerWrapper muxerWrapper,
boolean removeAudio,
boolean removeVideo,
TransformationRequest transformationRequest,
boolean clippingStartsAtKeyFrame,
ImmutableList<Effect> videoEffects,
FrameProcessor.Factory frameProcessorFactory,
Codec.EncoderFactory encoderFactory,
Codec.DecoderFactory decoderFactory,
FallbackListener fallbackListener,
AsyncErrorListener asyncErrorListener,
DebugViewProvider debugViewProvider) {
this.context = context;
this.muxerWrapper = muxerWrapper;
this.removeAudio = removeAudio;
this.removeVideo = removeVideo;
this.transformationRequest = transformationRequest;
this.clippingStartsAtKeyFrame = clippingStartsAtKeyFrame;
this.videoEffects = videoEffects;
this.frameProcessorFactory = frameProcessorFactory;
this.encoderFactory = encoderFactory;
this.decoderFactory = decoderFactory;
this.fallbackListener = fallbackListener;
this.asyncErrorListener = asyncErrorListener;
this.debugViewProvider = debugViewProvider;
mediaClock = new TransformerMediaClock();
}
@Override
public Renderer[] createRenderers(
Handler eventHandler,
VideoRendererEventListener videoRendererEventListener,
AudioRendererEventListener audioRendererEventListener,
TextOutput textRendererOutput,
MetadataOutput metadataRendererOutput) {
int rendererCount = removeAudio || removeVideo ? 1 : 2;
Renderer[] renderers = new Renderer[rendererCount];
int index = 0;
if (!removeAudio) {
renderers[index] =
new TransformerAudioRenderer(
muxerWrapper,
mediaClock,
transformationRequest,
encoderFactory,
decoderFactory,
asyncErrorListener,
fallbackListener);
index++;
}
if (!removeVideo) {
renderers[index] =
new TransformerVideoRenderer(
context,
muxerWrapper,
mediaClock,
transformationRequest,
clippingStartsAtKeyFrame,
videoEffects,
frameProcessorFactory,
encoderFactory,
decoderFactory,
asyncErrorListener,
fallbackListener,
debugViewProvider);
index++;
}
return renderers;
}
/** Listener for exceptions that occur during a transformation. */
/* package */ interface AsyncErrorListener {
/**
* Called when a {@link TransformationException} occurs.
*
* <p>Can be called from any thread.
*/
void onTransformationException(TransformationException exception);
}
private final class TransformerPlayerListener implements Player.Listener, AsyncErrorListener {
private final class ComponentListener
implements ExoPlayerAssetLoader.Listener, AsyncErrorListener {
private final MediaItem mediaItem;
private final Handler handler;
public TransformerPlayerListener(MediaItem mediaItem, Looper looper) {
public ComponentListener(MediaItem mediaItem, Looper looper) {
this.mediaItem = mediaItem;
handler = new Handler(looper);
}
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_ENDED) {
handleTransformationEnded(/* exception= */ null);
}
}
@Override
public void onTimelineChanged(Timeline timeline, int reason) {
if (progressState != PROGRESS_STATE_WAITING_FOR_AVAILABILITY) {
return;
}
Timeline.Window window = new Timeline.Window();
timeline.getWindow(/* windowIndex= */ 0, window);
if (!window.isPlaceholder) {
long durationUs = window.durationUs;
// 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 =
durationUs <= 0 || durationUs == C.TIME_UNSET
? PROGRESS_STATE_UNAVAILABLE
: PROGRESS_STATE_AVAILABLE;
checkNotNull(player).play();
}
}
@Override
public void onTracksChanged(Tracks tracks) {
if (checkNotNull(muxerWrapper).getTrackCount() == 0) {
handleTransformationEnded(
TransformationException.createForUnexpected(
new IllegalStateException("The output does not contain any tracks.")));
}
}
@Override
public void onPlayerError(PlaybackException error) {
public void onError(Exception e) {
TransformationException transformationException =
TransformationException.createForPlaybackException(error);
e instanceof PlaybackException
? TransformationException.createForPlaybackException((PlaybackException) e)
: TransformationException.createForUnexpected(e);
handleTransformationException(transformationException);
}
@Override
public void onEnded() {
handleTransformationEnded(/* exception= */ null);
}
@Override
public void onTransformationException(TransformationException exception) {
if (Looper.myLooper() == looper) {
handleTransformationException(exception);
} else {
handler.post(() -> handleTransformationException(exception));
}
}
private void handleTransformationException(TransformationException transformationException) {
if (isCancelling) {
// Resources are already being released.
@ -1078,24 +926,5 @@ public final class Transformer {
}
listeners.flushEvents();
}
@Override
public void onTransformationException(TransformationException exception) {
if (Looper.myLooper() == looper) {
handleTransformationException(exception);
} else {
handler.post(() -> handleTransformationException(exception));
}
}
}
/** Listener for exceptions that occur during a transformation. */
/* package */ interface AsyncErrorListener {
/**
* Called when a {@link TransformationException} occurs.
*
* <p>Can be called from any thread.
*/
void onTransformationException(TransformationException exception);
}
}