From f8d84eec5962f7eb9c7c10899ec44a796579eedd Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 11 Jan 2022 17:13:33 +0000 Subject: [PATCH] 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 --- RELEASENOTES.md | 1 + docs/transforming-media.md | 4 +- .../android/exoplayer2/util/ListenerSet.java | 20 +++ .../transformer/mh/AndroidTestUtil.java | 2 +- .../exoplayer2/transformer/Transformer.java | 121 +++++++++++++++--- .../transformer/TransformerTest.java | 74 +++++++++++ .../transformer/TransformerTestRunner.java | 5 +- 7 files changed, 201 insertions(+), 26 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a55d2fc1d3..e5c67f3a36 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -83,6 +83,7 @@ * `TransformationException` is now used to describe errors that occur during a transformation. * Add `TransformationRequest` for specifying the transformation options. + * Allow multiple listeners to be registered. * MediaSession extension: * 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. diff --git a/docs/transforming-media.md b/docs/transforming-media.md index a3c112f77a..81f54a8656 100644 --- a/docs/transforming-media.md +++ b/docs/transforming-media.md @@ -34,7 +34,7 @@ transformation that removes the audio track from the input: Transformer transformer = new Transformer.Builder(context) .setRemoveAudio(true) - .setListener(transformerListener) + .addListener(transformerListener) .build(); // Start the transformation. transformer.startTransformation(inputMediaItem, outputPath); @@ -121,7 +121,7 @@ method. Transformer transformer = new Transformer.Builder(context) .setFlattenForSlowMotion(true) - .setListener(transformerListener) + .addListener(transformerListener) .build(); transformer.startTransformation(inputMediaItem, outputPath); ~~~ diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java index 8aa4025bca..e347f534fb 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/ListenerSet.java @@ -116,6 +116,21 @@ public final class ListenerSet { */ @CheckResult public ListenerSet copy(Looper looper, IterationFinishedEvent 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 copy( + Looper looper, Clock clock, IterationFinishedEvent iterationFinishedEvent) { return new ListenerSet<>(listeners, looper, clock, iterationFinishedEvent); } @@ -150,6 +165,11 @@ public final class ListenerSet { } } + /** Removes all listeners from the set. */ + public void clear() { + listeners.clear(); + } + /** Returns the number of added listeners. */ public int size() { return listeners.size(); diff --git a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java index 6f54d93949..503aaaa223 100644 --- a/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java +++ b/library/transformer/src/androidTest/java/com/google/android/exoplayer2/transformer/mh/AndroidTestUtil.java @@ -72,7 +72,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; Transformer testTransformer = transformer .buildUpon() - .setListener( + .addListener( new Transformer.Listener() { @Override public void onTransformationCompleted(MediaItem inputMediaItem) { diff --git a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java index 6005b0f972..52f0e96f61 100644 --- a/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java +++ b/library/transformer/src/main/java/com/google/android/exoplayer2/transformer/Transformer.java @@ -54,6 +54,7 @@ import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; 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.Util; import com.google.android.exoplayer2.video.VideoRendererEventListener; @@ -98,7 +99,7 @@ public final class Transformer { private boolean removeVideo; private String containerMimeType; private TransformationRequest transformationRequest; - private Transformer.Listener listener; + private ListenerSet listeners; private DebugViewProvider debugViewProvider; private Looper looper; private Clock clock; @@ -108,9 +109,9 @@ public final class Transformer { @Deprecated public Builder() { muxerFactory = new FrameworkMuxer.Factory(); - listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {}); encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; @@ -125,9 +126,9 @@ public final class Transformer { public Builder(Context context) { this.context = context.getApplicationContext(); muxerFactory = new FrameworkMuxer.Factory(); - listener = new Listener() {}; looper = Util.getCurrentOrMainLooper(); clock = Clock.DEFAULT; + listeners = new ListenerSet<>(looper, clock, (listener, flags) -> {}); encoderFactory = Codec.EncoderFactory.DEFAULT; debugViewProvider = DebugViewProvider.NONE; containerMimeType = MimeTypes.VIDEO_MP4; @@ -143,7 +144,7 @@ public final class Transformer { this.removeVideo = transformer.removeVideo; this.containerMimeType = transformer.containerMimeType; this.transformationRequest = transformer.transformationRequest; - this.listener = transformer.listener; + this.listeners = transformer.listeners; this.looper = transformer.looper; this.encoderFactory = transformer.encoderFactory; 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. * - *

This is equivalent to {@link Transformer#setListener(Listener)}. + *

This is equivalent to {@link Transformer#addListener(Listener)}. * * @param listener A {@link Transformer.Listener}. * @return This builder. */ - public Builder setListener(Transformer.Listener listener) { - this.listener = listener; + public Builder addListener(Transformer.Listener listener) { + this.listeners.add(listener); + return this; + } + + /** + * Removes a {@link Transformer.Listener}. + * + *

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}. + * + *

This is equivalent to {@link Transformer#removeAllListeners()}. + * + * @return This builder. + */ + public Builder removeAllListeners() { + this.listeners.clear(); return this; } @@ -288,6 +325,7 @@ public final class Transformer { */ public Builder setLooper(Looper looper) { this.looper = looper; + this.listeners = listeners.copy(looper, (listener, flags) -> {}); return this; } @@ -328,6 +366,7 @@ public final class Transformer { @VisibleForTesting /* package */ Builder setClock(Clock clock) { this.clock = clock; + this.listeners = listeners.copy(looper, clock, (listener, flags) -> {}); return this; } @@ -381,7 +420,7 @@ public final class Transformer { removeVideo, containerMimeType, transformationRequest, - listener, + listeners, looper, clock, encoderFactory, @@ -480,8 +519,8 @@ public final class Transformer { private final Codec.EncoderFactory encoderFactory; private final Codec.DecoderFactory decoderFactory; private final Transformer.DebugViewProvider debugViewProvider; + private final ListenerSet listeners; - private Transformer.Listener listener; @Nullable private MuxerWrapper muxerWrapper; @Nullable private ExoPlayer player; @ProgressState private int progressState; @@ -494,7 +533,7 @@ public final class Transformer { boolean removeVideo, String containerMimeType, TransformationRequest transformationRequest, - Transformer.Listener listener, + ListenerSet listeners, Looper looper, Clock clock, Codec.EncoderFactory encoderFactory, @@ -508,7 +547,7 @@ public final class Transformer { this.removeVideo = removeVideo; this.containerMimeType = containerMimeType; this.transformationRequest = transformationRequest; - this.listener = listener; + this.listeners = listeners; this.looper = looper; this.clock = clock; 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}. * @throws IllegalStateException If this method is called from the wrong thread. */ - public void setListener(Transformer.Listener listener) { + public void addListener(Transformer.Listener listener) { 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}. * - *

The transformation state is notified through the {@link Builder#setListener(Listener) + *

The transformation state is notified through the {@link Builder#addListener(Listener) * listener}. * *

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}. * - *

The transformation state is notified through the {@link Builder#setListener(Listener) + *

The transformation state is notified through the {@link Builder#addListener(Listener) * listener}. * *

Concurrent transformations on the same Transformer object are not allowed. @@ -840,16 +911,26 @@ public final class Transformer { } 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; } if (exception != null) { - listener.onTransformationError(mediaItem, exception); + listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onTransformationError(mediaItem, exception)); } if (resourceReleaseException != null) { - listener.onTransformationError(mediaItem, resourceReleaseException); + TransformationException finalResourceReleaseException = resourceReleaseException; + listeners.queueEvent( + /* eventFlag= */ C.INDEX_UNSET, + listener -> listener.onTransformationError(mediaItem, finalResourceReleaseException)); } + listeners.flushEvents(); } } } diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java index 910bac7c57..376a0e08b4 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTest.java @@ -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.common.truth.Truth.assertThat; 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.media.MediaCrypto; @@ -248,6 +251,77 @@ public final class TransformerTest { 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 public void startTransformation_flattenForSlowMotion_completesSuccessfully() throws Exception { Transformer transformer = diff --git a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java index ba3063f175..0b88257c73 100644 --- a/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java +++ b/library/transformer/src/test/java/com/google/android/exoplayer2/transformer/TransformerTestRunner.java @@ -69,7 +69,7 @@ public final class TransformerTestRunner { private static TransformationException runUntilListenerCalled(Transformer transformer) throws TimeoutException { TransformationResult transformationResult = new TransformationResult(); - Transformer.Listener listener = + transformer.addListener( new Transformer.Listener() { @Override public void onTransformationCompleted(MediaItem inputMediaItem) { @@ -81,8 +81,7 @@ public final class TransformerTestRunner { MediaItem inputMediaItem, TransformationException exception) { transformationResult.exception = exception; } - }; - transformer.setListener(listener); + }); runLooperUntil( transformer.getApplicationLooper(), () -> transformationResult.isCompleted || transformationResult.exception != null);