Commit 354d5aea by ibaker Committed by Oliver Woodman

Add the listener type to MediaSourceEventDispatcher.add/removeListener

Without this change there's confusing behaviour if you pass e.g.
AnalyticsCollector (which implements both DrmSessionEventListener and
MediaSourceEventListener) to MediaSource.addEventListener: It will
receive DRM events too, even though you never passed it to
MediaSource.addDrmEventListener.

Also add some tests for MediaSourceEventDispatcher.

PiperOrigin-RevId: 301169915
parent c294e0cb
...@@ -143,12 +143,12 @@ public abstract class BaseMediaSource implements MediaSource { ...@@ -143,12 +143,12 @@ public abstract class BaseMediaSource implements MediaSource {
@Override @Override
public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) { public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) {
eventDispatcher.addEventListener(handler, eventListener); eventDispatcher.addEventListener(handler, eventListener, DrmSessionEventListener.class);
} }
@Override @Override
public final void removeDrmEventListener(DrmSessionEventListener eventListener) { public final void removeDrmEventListener(DrmSessionEventListener eventListener) {
eventDispatcher.removeEventListener(eventListener); eventDispatcher.removeEventListener(eventListener, DrmSessionEventListener.class);
} }
@Override @Override
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source; package com.google.android.exoplayer2.source;
import android.net.Uri; import android.net.Uri;
import android.os.Handler;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Format;
...@@ -174,7 +175,7 @@ public interface MediaSourceEventListener { ...@@ -174,7 +175,7 @@ public interface MediaSourceEventListener {
} }
private EventDispatcher( private EventDispatcher(
CopyOnWriteMultiset<ListenerAndHandler> listeners, CopyOnWriteMultiset<ListenerInfo> listeners,
int windowIndex, int windowIndex,
@Nullable MediaPeriodId mediaPeriodId, @Nullable MediaPeriodId mediaPeriodId,
long mediaTimeOffsetMs) { long mediaTimeOffsetMs) {
...@@ -184,8 +185,34 @@ public interface MediaSourceEventListener { ...@@ -184,8 +185,34 @@ public interface MediaSourceEventListener {
@Override @Override
public EventDispatcher withParameters( public EventDispatcher withParameters(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
return new EventDispatcher( return new EventDispatcher(listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); }
/**
* Adds a {@link MediaSourceEventListener} to the event dispatcher.
*
* <p>This is equivalent to {@link #addEventListener(Handler, Object, Class)} with {@code
* listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to
* using {@link MediaSourceEventDispatcher} everywhere.
*
* @param handler A handler on the which listener events will be posted.
* @param eventListener The listener to be added.
*/
public void addEventListener(Handler handler, MediaSourceEventListener eventListener) {
addEventListener(handler, eventListener, MediaSourceEventListener.class);
}
/**
* Removes a {@link MediaSourceEventListener} from the event dispatcher.
*
* <p>This is equivalent to {@link #removeEventListener(Object, Class)} with {@code
* listenerClass = MediaSourceEventListener.class} and is intended to ease the transition to
* using {@link MediaSourceEventDispatcher} everywhere.
*
* @param eventListener The listener to be removed.
*/
public void removeEventListener(MediaSourceEventListener eventListener) {
removeEventListener(eventListener, MediaSourceEventListener.class);
} }
public void mediaPeriodCreated() { public void mediaPeriodCreated() {
......
...@@ -50,25 +50,25 @@ public class MediaSourceEventDispatcher { ...@@ -50,25 +50,25 @@ public class MediaSourceEventDispatcher {
@Nullable public final MediaPeriodId mediaPeriodId; @Nullable public final MediaPeriodId mediaPeriodId;
// TODO: Make these private when MediaSourceEventListener.EventDispatcher is deleted. // TODO: Make these private when MediaSourceEventListener.EventDispatcher is deleted.
protected final CopyOnWriteMultiset<ListenerAndHandler> listenerAndHandlers; protected final CopyOnWriteMultiset<ListenerInfo> listenerInfos;
// TODO: Define exactly what this means, and check it's always set correctly. // TODO: Define exactly what this means, and check it's always set correctly.
protected final long mediaTimeOffsetMs; protected final long mediaTimeOffsetMs;
/** Creates an event dispatcher. */ /** Creates an event dispatcher. */
public MediaSourceEventDispatcher() { public MediaSourceEventDispatcher() {
this( this(
/* listenerAndHandlers= */ new CopyOnWriteMultiset<>(), /* listenerInfos= */ new CopyOnWriteMultiset<>(),
/* windowIndex= */ 0, /* windowIndex= */ 0,
/* mediaPeriodId= */ null, /* mediaPeriodId= */ null,
/* mediaTimeOffsetMs= */ 0); /* mediaTimeOffsetMs= */ 0);
} }
protected MediaSourceEventDispatcher( protected MediaSourceEventDispatcher(
CopyOnWriteMultiset<ListenerAndHandler> listenerAndHandlers, CopyOnWriteMultiset<ListenerInfo> listenerInfos,
int windowIndex, int windowIndex,
@Nullable MediaPeriodId mediaPeriodId, @Nullable MediaPeriodId mediaPeriodId,
long mediaTimeOffsetMs) { long mediaTimeOffsetMs) {
this.listenerAndHandlers = listenerAndHandlers; this.listenerInfos = listenerInfos;
this.windowIndex = windowIndex; this.windowIndex = windowIndex;
this.mediaPeriodId = mediaPeriodId; this.mediaPeriodId = mediaPeriodId;
this.mediaTimeOffsetMs = mediaTimeOffsetMs; this.mediaTimeOffsetMs = mediaTimeOffsetMs;
...@@ -87,30 +87,45 @@ public class MediaSourceEventDispatcher { ...@@ -87,30 +87,45 @@ public class MediaSourceEventDispatcher {
public MediaSourceEventDispatcher withParameters( public MediaSourceEventDispatcher withParameters(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) { int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
return new MediaSourceEventDispatcher( return new MediaSourceEventDispatcher(
listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs); listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
} }
/** /**
* Adds a listener to the event dispatcher. * Adds a listener to the event dispatcher.
* *
* <p>Calls to {@link #dispatch(EventWithPeriodId, Class)} will propagate to {@code eventListener}
* if the {@code listenerClass} types are equal.
*
* <p>The same listener instance can be added multiple times with different {@code listenerClass}
* values (i.e. if the instance implements multiple listener interfaces).
*
* <p>Duplicate {@code {eventListener, listenerClass}} pairs are also permitted. In this case an
* event dispatched to {@code listenerClass} will only be passed to the {@code eventListener}
* once.
*
* @param handler A handler on the which listener events will be posted. * @param handler A handler on the which listener events will be posted.
* @param eventListener The listener to be added. * @param eventListener The listener to be added.
* @param listenerClass The type used to register the listener. Can be a superclass of {@code
* eventListener}.
*/ */
public void addEventListener(Handler handler, Object eventListener) { public <T> void addEventListener(Handler handler, T eventListener, Class<T> listenerClass) {
Assertions.checkNotNull(handler); Assertions.checkNotNull(handler);
Assertions.checkNotNull(eventListener); Assertions.checkNotNull(eventListener);
listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener)); listenerInfos.add(new ListenerInfo(handler, eventListener, listenerClass));
} }
/** /**
* Removes a listener from the event dispatcher. * Removes a listener from the event dispatcher.
* *
* @param eventListener The listener to be removed. * @param eventListener The listener to be removed.
* @param listenerClass The listener type passed to {@link #addEventListener(Handler, Object,
* Class)}.
*/ */
public void removeEventListener(Object eventListener) { public <T> void removeEventListener(T eventListener, Class<T> listenerClass) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { for (ListenerInfo listenerInfo : listenerInfos) {
if (listenerAndHandler.listener == eventListener) { if (listenerInfo.listener == eventListener
listenerAndHandlers.remove(listenerAndHandler); && listenerInfo.listenerClass.equals(listenerClass)) {
listenerInfos.remove(listenerInfo);
} }
} }
} }
...@@ -118,11 +133,11 @@ public class MediaSourceEventDispatcher { ...@@ -118,11 +133,11 @@ public class MediaSourceEventDispatcher {
/** Dispatches {@code event} to all registered listeners of type {@code listenerClass}. */ /** Dispatches {@code event} to all registered listeners of type {@code listenerClass}. */
@SuppressWarnings("unchecked") // The cast is gated with listenerClass.isInstance() @SuppressWarnings("unchecked") // The cast is gated with listenerClass.isInstance()
public <T> void dispatch(EventWithPeriodId<T> event, Class<T> listenerClass) { public <T> void dispatch(EventWithPeriodId<T> event, Class<T> listenerClass) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers.elementSet()) { for (ListenerInfo listenerInfo : listenerInfos.elementSet()) {
if (listenerClass.isInstance(listenerAndHandler.listener)) { if (listenerInfo.listenerClass.equals(listenerClass)) {
postOrRun( postOrRun(
listenerAndHandler.handler, listenerInfo.handler,
() -> event.sendTo((T) listenerAndHandler.listener, windowIndex, mediaPeriodId)); () -> event.sendTo((T) listenerInfo.listener, windowIndex, mediaPeriodId));
} }
} }
} }
...@@ -140,15 +155,17 @@ public class MediaSourceEventDispatcher { ...@@ -140,15 +155,17 @@ public class MediaSourceEventDispatcher {
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
} }
/** Container class for a {@link Handler} and {@code listener} object. */ /** Container class for a {@link Handler}, {@code listener} and {@code listenerClass}. */
protected static final class ListenerAndHandler { protected static final class ListenerInfo {
public final Handler handler; public final Handler handler;
public final Object listener; public final Object listener;
public final Class<?> listenerClass;
public ListenerAndHandler(Handler handler, Object listener) { public ListenerInfo(Handler handler, Object listener, Class<?> listenerClass) {
this.handler = handler; this.handler = handler;
this.listener = listener; this.listener = listener;
this.listenerClass = listenerClass;
} }
@Override @Override
...@@ -156,19 +173,21 @@ public class MediaSourceEventDispatcher { ...@@ -156,19 +173,21 @@ public class MediaSourceEventDispatcher {
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof ListenerAndHandler)) { if (!(o instanceof ListenerInfo)) {
return false; return false;
} }
// We deliberately only consider listener (and not handler) in equals() and hashcode() ListenerInfo that = (ListenerInfo) o;
// because the handler used to process the callbacks is an implementation detail.
ListenerAndHandler that = (ListenerAndHandler) o; // We deliberately only consider listener and listenerClass (and not handler) in equals() and
return listener.equals(that.listener); // hashcode() because the handler used to process the callbacks is an implementation detail.
return listener.equals(that.listener) && listenerClass.equals(that.listenerClass);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return listener.hashCode(); int result = 31 * listener.hashCode();
return result + 31 * listenerClass.hashCode();
} }
} }
} }
/*
* Copyright (C) 2020 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 com.google.android.exoplayer2.util;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.os.Handler;
import android.os.Looper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSourceEventListener;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Tests for {@link MediaSourceEventDispatcher}. */
@RunWith(AndroidJUnit4.class)
public class MediaSourceEventDispatcherTest {
private static final MediaSource.MediaPeriodId MEDIA_PERIOD_ID =
new MediaSource.MediaPeriodId("test uid");
private static final int WINDOW_INDEX = 200;
private static final int MEDIA_TIME_OFFSET_MS = 1_000;
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
@Mock private MediaSourceEventListener mediaSourceEventListener;
@Mock private MediaAndDrmEventListener mediaAndDrmEventListener;
private MediaSourceEventDispatcher eventDispatcher;
@Before
public void setupEventDispatcher() {
eventDispatcher = new MediaSourceEventDispatcher();
eventDispatcher =
eventDispatcher.withParameters(WINDOW_INDEX, MEDIA_PERIOD_ID, MEDIA_TIME_OFFSET_MS);
}
@Test
public void listenerReceivesEventPopulatedWithMediaPeriodInfo() {
eventDispatcher.addEventListener(
Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID);
}
@Test
public void sameListenerObjectRegisteredTwiceOnlyReceivesEventsOnce() {
eventDispatcher.addEventListener(
Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class);
eventDispatcher.addEventListener(
Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
verify(mediaSourceEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID);
}
@Test
public void sameListenerInstanceCanBeRegisteredWithTwoTypes() {
eventDispatcher.addEventListener(
new Handler(Looper.getMainLooper()),
mediaAndDrmEventListener,
MediaSourceEventListener.class);
eventDispatcher.addEventListener(
new Handler(Looper.getMainLooper()),
mediaAndDrmEventListener,
DrmSessionEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
eventDispatcher.dispatch(
(listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(),
DrmSessionEventListener.class);
verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID);
verify(mediaAndDrmEventListener).onDrmKeysLoaded();
}
// If a listener is added that implements multiple types, it should only receive events for the
// type specified at registration time.
@Test
public void listenerOnlyReceivesEventsForRegisteredType() {
eventDispatcher.addEventListener(
new Handler(Looper.getMainLooper()),
mediaAndDrmEventListener,
MediaSourceEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
eventDispatcher.dispatch(
(listener, windowIndex, mediaPeriodId) -> listener.onDrmKeysLoaded(),
DrmSessionEventListener.class);
verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID);
verify(mediaAndDrmEventListener, never()).onDrmKeysLoaded();
}
@Test
public void listenersAreCopiedToNewDispatcher() {
eventDispatcher.addEventListener(
Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class);
MediaSource.MediaPeriodId newPeriodId = new MediaSource.MediaPeriodId("different uid");
MediaSourceEventDispatcher newEventDispatcher =
this.eventDispatcher.withParameters(
/* windowIndex= */ 250, newPeriodId, /* mediaTimeOffsetMs= */ 500);
newEventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
verify(mediaSourceEventListener).onMediaPeriodCreated(250, newPeriodId);
}
@Test
public void removingListenerStopsEventDispatch() {
eventDispatcher.addEventListener(
Util.createHandler(), mediaSourceEventListener, MediaSourceEventListener.class);
eventDispatcher.removeEventListener(mediaSourceEventListener, MediaSourceEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
verify(mediaSourceEventListener, never()).onMediaPeriodCreated(anyInt(), any());
}
@Test
public void removingListenerWithDifferentTypeToRegistrationDoesntRemove() {
eventDispatcher.addEventListener(
Util.createHandler(), mediaAndDrmEventListener, MediaSourceEventListener.class);
eventDispatcher.removeEventListener(mediaAndDrmEventListener, DrmSessionEventListener.class);
eventDispatcher.dispatch(
MediaSourceEventListener::onMediaPeriodCreated, MediaSourceEventListener.class);
verify(mediaAndDrmEventListener).onMediaPeriodCreated(WINDOW_INDEX, MEDIA_PERIOD_ID);
}
private interface MediaAndDrmEventListener
extends MediaSourceEventListener, DrmSessionEventListener {}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment