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 b0ae7c04d5
commit 725b861f54
5 changed files with 198 additions and 24 deletions

View File

@ -118,6 +118,21 @@ public final class ListenerSet<T extends @NonNull Object> {
*/
@CheckResult
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);
}
@ -152,6 +167,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. */
public int size() {
return listeners.size();

View File

@ -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) {

View File

@ -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<Transformer.Listener> 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.
*
* <p>This is equivalent to {@link Transformer#setListener(Listener)}.
* <p>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}.
*
* <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;
}
@ -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<Transformer.Listener> 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<Transformer.Listener> 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}.
*
* <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}.
*
* <p>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}.
*
* <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}.
*
* <p>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();
}
}
}

View File

@ -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 =

View File

@ -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);