From 725b861f54b2a7decd8e112bef7025cf47ea54a7 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 --- .../media3/common/util/ListenerSet.java | 20 +++ .../transformer/mh/AndroidTestUtil.java | 2 +- .../media3/transformer/Transformer.java | 121 +++++++++++++++--- .../media3/transformer/TransformerTest.java | 74 +++++++++++ .../transformer/TransformerTestRunner.java | 5 +- 5 files changed, 198 insertions(+), 24 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java b/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java index a05d1a82d7..077141b05c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ListenerSet.java @@ -118,6 +118,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); } @@ -152,6 +167,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/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/AndroidTestUtil.java index 7ecd8805ac..5f7eea3f7c 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/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/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 917a8b07b4..dd7d46a9c9 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -43,6 +43,7 @@ import androidx.media3.common.Player; import androidx.media3.common.Timeline; import androidx.media3.common.TracksInfo; import androidx.media3.common.util.Clock; +import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.DefaultLoadControl; @@ -100,7 +101,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; @@ -110,9 +111,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; @@ -127,9 +128,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; @@ -145,7 +146,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; @@ -267,15 +268,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; } @@ -290,6 +327,7 @@ public final class Transformer { */ public Builder setLooper(Looper looper) { this.looper = looper; + this.listeners = listeners.copy(looper, (listener, flags) -> {}); return this; } @@ -330,6 +368,7 @@ public final class Transformer { @VisibleForTesting /* package */ Builder setClock(Clock clock) { this.clock = clock; + this.listeners = listeners.copy(looper, clock, (listener, flags) -> {}); return this; } @@ -383,7 +422,7 @@ public final class Transformer { removeVideo, containerMimeType, transformationRequest, - listener, + listeners, looper, clock, encoderFactory, @@ -482,8 +521,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; @@ -496,7 +535,7 @@ public final class Transformer { boolean removeVideo, String containerMimeType, TransformationRequest transformationRequest, - Transformer.Listener listener, + ListenerSet listeners, Looper looper, Clock clock, Codec.EncoderFactory encoderFactory, @@ -510,7 +549,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; @@ -525,20 +564,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. @@ -561,7 +632,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. @@ -842,16 +913,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/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java index 4e3a5139ea..bc7fe5a9cb 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTest.java @@ -22,6 +22,9 @@ import static androidx.media3.transformer.Transformer.PROGRESS_STATE_UNAVAILABLE import static androidx.media3.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/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTestRunner.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTestRunner.java index 748f4c477d..a739428697 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerTestRunner.java +++ b/libraries/transformer/src/test/java/androidx/media3/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);