Allow multiple Transformer listeners to be registered.

Multiple listeners can be added to Transformer and its builder.
All or specific listeners can also be removed.

PiperOrigin-RevId: 421047650
This commit is contained in:
hschlueter 2022-01-11 17:13:33 +00:00 committed by Ian Baker
parent bf32ae50d7
commit f8d84eec59
7 changed files with 201 additions and 26 deletions

View file

@ -83,6 +83,7 @@
* `TransformationException` is now used to describe errors that occur * `TransformationException` is now used to describe errors that occur
during a transformation. during a transformation.
* Add `TransformationRequest` for specifying the transformation options. * Add `TransformationRequest` for specifying the transformation options.
* Allow multiple listeners to be registered.
* MediaSession extension: * MediaSession extension:
* Remove deprecated call to `onStop(/* reset= */ true)` and provide an * Remove deprecated call to `onStop(/* reset= */ true)` and provide an
opt-out flag for apps that don't want to clear the playlist on stop. opt-out flag for apps that don't want to clear the playlist on stop.

View file

@ -34,7 +34,7 @@ transformation that removes the audio track from the input:
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setRemoveAudio(true) .setRemoveAudio(true)
.setListener(transformerListener) .addListener(transformerListener)
.build(); .build();
// Start the transformation. // Start the transformation.
transformer.startTransformation(inputMediaItem, outputPath); transformer.startTransformation(inputMediaItem, outputPath);
@ -121,7 +121,7 @@ method.
Transformer transformer = Transformer transformer =
new Transformer.Builder(context) new Transformer.Builder(context)
.setFlattenForSlowMotion(true) .setFlattenForSlowMotion(true)
.setListener(transformerListener) .addListener(transformerListener)
.build(); .build();
transformer.startTransformation(inputMediaItem, outputPath); transformer.startTransformation(inputMediaItem, outputPath);
~~~ ~~~

View file

@ -116,6 +116,21 @@ public final class ListenerSet<T extends @NonNull Object> {
*/ */
@CheckResult @CheckResult
public ListenerSet<T> copy(Looper looper, IterationFinishedEvent<T> iterationFinishedEvent) { public ListenerSet<T> copy(Looper looper, IterationFinishedEvent<T> iterationFinishedEvent) {
return copy(looper, clock, iterationFinishedEvent);
}
/**
* Copies the listener set.
*
* @param looper The new {@link Looper} for the copied listener set.
* @param clock The new {@link Clock} for the copied listener set.
* @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
* sent during one {@link Looper} message queue iteration were handled by the listeners.
* @return The copied listener set.
*/
@CheckResult
public ListenerSet<T> copy(
Looper looper, Clock clock, IterationFinishedEvent<T> iterationFinishedEvent) {
return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent); return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent);
} }
@ -150,6 +165,11 @@ public final class ListenerSet<T extends @NonNull Object> {
} }
} }
/** Removes all listeners from the set. */
public void clear() {
listeners.clear();
}
/** Returns the number of added listeners. */ /** Returns the number of added listeners. */
public int size() { public int size() {
return listeners.size(); return listeners.size();

View file

@ -72,7 +72,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
Transformer testTransformer = Transformer testTransformer =
transformer transformer
.buildUpon() .buildUpon()
.setListener( .addListener(
new Transformer.Listener() { new Transformer.Listener() {
@Override @Override
public void onTransformationCompleted(MediaItem inputMediaItem) { public void onTransformationCompleted(MediaItem inputMediaItem) {

View file

@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.ListenerSet;
import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener; import com.google.android.exoplayer2.video.VideoRendererEventListener;
@ -98,7 +99,7 @@ public final class Transformer {
private boolean removeVideo; private boolean removeVideo;
private String containerMimeType; private String containerMimeType;
private TransformationRequest transformationRequest; private TransformationRequest transformationRequest;
private Transformer.Listener listener; private ListenerSet<Transformer.Listener> listeners;
private DebugViewProvider debugViewProvider; private DebugViewProvider debugViewProvider;
private Looper looper; private Looper looper;
private Clock clock; private Clock clock;
@ -108,9 +109,9 @@ public final class Transformer {
@Deprecated @Deprecated
public Builder() { public Builder() {
muxerFactory = new FrameworkMuxer.Factory(); muxerFactory = new FrameworkMuxer.Factory();
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper(); looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT; clock = Clock.DEFAULT;
listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {});
encoderFactory = Codec.EncoderFactory.DEFAULT; encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE; debugViewProvider = DebugViewProvider.NONE;
containerMimeType = MimeTypes.VIDEO_MP4; containerMimeType = MimeTypes.VIDEO_MP4;
@ -125,9 +126,9 @@ public final class Transformer {
public Builder(Context context) { public Builder(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
muxerFactory = new FrameworkMuxer.Factory(); muxerFactory = new FrameworkMuxer.Factory();
listener = new Listener() {};
looper = Util.getCurrentOrMainLooper(); looper = Util.getCurrentOrMainLooper();
clock = Clock.DEFAULT; clock = Clock.DEFAULT;
listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {});
encoderFactory = Codec.EncoderFactory.DEFAULT; encoderFactory = Codec.EncoderFactory.DEFAULT;
debugViewProvider = DebugViewProvider.NONE; debugViewProvider = DebugViewProvider.NONE;
containerMimeType = MimeTypes.VIDEO_MP4; containerMimeType = MimeTypes.VIDEO_MP4;
@ -143,7 +144,7 @@ public final class Transformer {
this.removeVideo = transformer.removeVideo; this.removeVideo = transformer.removeVideo;
this.containerMimeType = transformer.containerMimeType; this.containerMimeType = transformer.containerMimeType;
this.transformationRequest = transformer.transformationRequest; this.transformationRequest = transformer.transformationRequest;
this.listener = transformer.listener; this.listeners = transformer.listeners;
this.looper = transformer.looper; this.looper = transformer.looper;
this.encoderFactory = transformer.encoderFactory; this.encoderFactory = transformer.encoderFactory;
this.debugViewProvider = transformer.debugViewProvider; this.debugViewProvider = transformer.debugViewProvider;
@ -265,15 +266,51 @@ public final class Transformer {
} }
/** /**
* Sets the {@link Transformer.Listener} to listen to the transformation events. * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
* #removeAllListeners()} instead.
*/
@Deprecated
public Builder setListener(Transformer.Listener listener) {
this.listeners.clear();
this.listeners.add(listener);
return this;
}
/**
* Adds a {@link Transformer.Listener} to listen to the transformation events.
* *
* <p>This is equivalent to {@link Transformer#setListener(Listener)}. * <p>This is equivalent to {@link Transformer#addListener(Listener)}.
* *
* @param listener A {@link Transformer.Listener}. * @param listener A {@link Transformer.Listener}.
* @return This builder. * @return This builder.
*/ */
public Builder setListener(Transformer.Listener listener) { public Builder addListener(Transformer.Listener listener) {
this.listener = listener; this.listeners.add(listener);
return this;
}
/**
* Removes a {@link Transformer.Listener}.
*
* <p>This is equivalent to {@link Transformer#removeListener(Listener)}.
*
* @param listener A {@link Transformer.Listener}.
* @return This builder.
*/
public Builder removeListener(Transformer.Listener listener) {
this.listeners.remove(listener);
return this;
}
/**
* Removes all {@link Transformer.Listener listeners}.
*
* <p>This is equivalent to {@link Transformer#removeAllListeners()}.
*
* @return This builder.
*/
public Builder removeAllListeners() {
this.listeners.clear();
return this; return this;
} }
@ -288,6 +325,7 @@ public final class Transformer {
*/ */
public Builder setLooper(Looper looper) { public Builder setLooper(Looper looper) {
this.looper = looper; this.looper = looper;
this.listeners = listeners.copy(looper, (listener, flags) -> {});
return this; return this;
} }
@ -328,6 +366,7 @@ public final class Transformer {
@VisibleForTesting @VisibleForTesting
/* package */ Builder setClock(Clock clock) { /* package */ Builder setClock(Clock clock) {
this.clock = clock; this.clock = clock;
this.listeners = listeners.copy(looper, clock, (listener, flags) -> {});
return this; return this;
} }
@ -381,7 +420,7 @@ public final class Transformer {
removeVideo, removeVideo,
containerMimeType, containerMimeType,
transformationRequest, transformationRequest,
listener, listeners,
looper, looper,
clock, clock,
encoderFactory, encoderFactory,
@ -480,8 +519,8 @@ public final class Transformer {
private final Codec.EncoderFactory encoderFactory; private final Codec.EncoderFactory encoderFactory;
private final Codec.DecoderFactory decoderFactory; private final Codec.DecoderFactory decoderFactory;
private final Transformer.DebugViewProvider debugViewProvider; private final Transformer.DebugViewProvider debugViewProvider;
private final ListenerSet<Transformer.Listener> listeners;
private Transformer.Listener listener;
@Nullable private MuxerWrapper muxerWrapper; @Nullable private MuxerWrapper muxerWrapper;
@Nullable private ExoPlayer player; @Nullable private ExoPlayer player;
@ProgressState private int progressState; @ProgressState private int progressState;
@ -494,7 +533,7 @@ public final class Transformer {
boolean removeVideo, boolean removeVideo,
String containerMimeType, String containerMimeType,
TransformationRequest transformationRequest, TransformationRequest transformationRequest,
Transformer.Listener listener, ListenerSet<Transformer.Listener> listeners,
Looper looper, Looper looper,
Clock clock, Clock clock,
Codec.EncoderFactory encoderFactory, Codec.EncoderFactory encoderFactory,
@ -508,7 +547,7 @@ public final class Transformer {
this.removeVideo = removeVideo; this.removeVideo = removeVideo;
this.containerMimeType = containerMimeType; this.containerMimeType = containerMimeType;
this.transformationRequest = transformationRequest; this.transformationRequest = transformationRequest;
this.listener = listener; this.listeners = listeners;
this.looper = looper; this.looper = looper;
this.clock = clock; this.clock = clock;
this.encoderFactory = encoderFactory; this.encoderFactory = encoderFactory;
@ -523,20 +562,52 @@ public final class Transformer {
} }
/** /**
* Sets the {@link Transformer.Listener} to listen to the transformation events. * @deprecated Use {@link #addListener(Listener)}, {@link #removeListener(Listener)} or {@link
* #removeAllListeners()} instead.
*/
@Deprecated
public void setListener(Transformer.Listener listener) {
verifyApplicationThread();
this.listeners.clear();
this.listeners.add(listener);
}
/**
* Adds a {@link Transformer.Listener} to listen to the transformation events.
* *
* @param listener A {@link Transformer.Listener}. * @param listener A {@link Transformer.Listener}.
* @throws IllegalStateException If this method is called from the wrong thread. * @throws IllegalStateException If this method is called from the wrong thread.
*/ */
public void setListener(Transformer.Listener listener) { public void addListener(Transformer.Listener listener) {
verifyApplicationThread(); verifyApplicationThread();
this.listener = listener; this.listeners.add(listener);
}
/**
* Removes a {@link Transformer.Listener}.
*
* @param listener A {@link Transformer.Listener}.
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void removeListener(Transformer.Listener listener) {
verifyApplicationThread();
this.listeners.remove(listener);
}
/**
* Removes all {@link Transformer.Listener listeners}.
*
* @throws IllegalStateException If this method is called from the wrong thread.
*/
public void removeAllListeners() {
verifyApplicationThread();
this.listeners.clear();
} }
/** /**
* Starts an asynchronous operation to transform the given {@link MediaItem}. * Starts an asynchronous operation to transform the given {@link MediaItem}.
* *
* <p>The transformation state is notified through the {@link Builder#setListener(Listener) * <p>The transformation state is notified through the {@link Builder#addListener(Listener)
* listener}. * listener}.
* *
* <p>Concurrent transformations on the same Transformer object are not allowed. * <p>Concurrent transformations on the same Transformer object are not allowed.
@ -559,7 +630,7 @@ public final class Transformer {
/** /**
* Starts an asynchronous operation to transform the given {@link MediaItem}. * Starts an asynchronous operation to transform the given {@link MediaItem}.
* *
* <p>The transformation state is notified through the {@link Builder#setListener(Listener) * <p>The transformation state is notified through the {@link Builder#addListener(Listener)
* listener}. * listener}.
* *
* <p>Concurrent transformations on the same Transformer object are not allowed. * <p>Concurrent transformations on the same Transformer object are not allowed.
@ -840,16 +911,26 @@ public final class Transformer {
} }
if (exception == null && resourceReleaseException == null) { if (exception == null && resourceReleaseException == null) {
listener.onTransformationCompleted(mediaItem); // TODO(b/213341814): Add event flags for Transformer events.
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationCompleted(mediaItem));
listeners.flushEvents();
return; return;
} }
if (exception != null) { if (exception != null) {
listener.onTransformationError(mediaItem, exception); listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, exception));
} }
if (resourceReleaseException != null) { if (resourceReleaseException != null) {
listener.onTransformationError(mediaItem, resourceReleaseException); TransformationException finalResourceReleaseException = resourceReleaseException;
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener -> listener.onTransformationError(mediaItem, finalResourceReleaseException));
} }
listeners.flushEvents();
} }
} }
} }

View file

@ -22,6 +22,9 @@ import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STA
import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY; import static com.google.android.exoplayer2.transformer.Transformer.PROGRESS_STATE_WAITING_FOR_AVAILABILITY;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import android.content.Context; import android.content.Context;
import android.media.MediaCrypto; import android.media.MediaCrypto;
@ -248,6 +251,77 @@ public final class TransformerTest {
context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo")); context, testMuxer, getDumpFileName(FILE_AUDIO_VIDEO + ".novideo"));
} }
@Test
public void startTransformation_withMultipleListeners_callsEachOnCompletion() throws Exception {
Transformer.Listener mockListener1 = mock(Transformer.Listener.class);
Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.addListener(mockListener1)
.addListener(mockListener2)
.addListener(mockListener3)
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO);
transformer.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer);
verify(mockListener1, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener2, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener3, times(1)).onTransformationCompleted(mediaItem);
}
@Test
public void startTransformation_withMultipleListeners_callsEachOnError() throws Exception {
Transformer.Listener mockListener1 = mock(Transformer.Listener.class);
Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.addListener(mockListener1)
.addListener(mockListener2)
.addListener(mockListener3)
.build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_UNSUPPORTED_BY_MUXER);
transformer.startTransformation(mediaItem, outputPath);
TransformationException exception = TransformerTestRunner.runUntilError(transformer);
verify(mockListener1, times(1)).onTransformationError(mediaItem, exception);
verify(mockListener2, times(1)).onTransformationError(mediaItem, exception);
verify(mockListener3, times(1)).onTransformationError(mediaItem, exception);
}
@Test
public void startTransformation_afterBuildUponWithListenerRemoved_onlyCallsRemainingListeners()
throws Exception {
Transformer.Listener mockListener1 = mock(Transformer.Listener.class);
Transformer.Listener mockListener2 = mock(Transformer.Listener.class);
Transformer.Listener mockListener3 = mock(Transformer.Listener.class);
Transformer transformer1 =
new Transformer.Builder(context)
.setClock(clock)
.setMuxerFactory(new TestMuxerFactory())
.addListener(mockListener1)
.addListener(mockListener2)
.addListener(mockListener3)
.build();
Transformer transformer2 = transformer1.buildUpon().removeListener(mockListener2).build();
MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_AUDIO_VIDEO);
transformer2.startTransformation(mediaItem, outputPath);
TransformerTestRunner.runUntilCompleted(transformer2);
verify(mockListener1, times(1)).onTransformationCompleted(mediaItem);
verify(mockListener2, times(0)).onTransformationCompleted(mediaItem);
verify(mockListener3, times(1)).onTransformationCompleted(mediaItem);
}
@Test @Test
public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception {
Transformer transformer = Transformer transformer =

View file

@ -69,7 +69,7 @@ public final class TransformerTestRunner {
private static TransformationException runUntilListenerCalled(Transformer transformer) private static TransformationException runUntilListenerCalled(Transformer transformer)
throws TimeoutException { throws TimeoutException {
TransformationResult transformationResult = new TransformationResult(); TransformationResult transformationResult = new TransformationResult();
Transformer.Listener listener = transformer.addListener(
new Transformer.Listener() { new Transformer.Listener() {
@Override @Override
public void onTransformationCompleted(MediaItem inputMediaItem) { public void onTransformationCompleted(MediaItem inputMediaItem) {
@ -81,8 +81,7 @@ public final class TransformerTestRunner {
MediaItem inputMediaItem, TransformationException exception) { MediaItem inputMediaItem, TransformationException exception) {
transformationResult.exception = exception; transformationResult.exception = exception;
} }
}; });
transformer.setListener(listener);
runLooperUntil( runLooperUntil(
transformer.getApplicationLooper(), transformer.getApplicationLooper(),
() -> transformationResult.isCompleted || transformationResult.exception != null); () -> transformationResult.isCompleted || transformationResult.exception != null);