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
This commit is contained in:
ibaker 2020-03-16 16:06:36 +00:00 committed by Oliver Woodman
parent c294e0cb89
commit 354d5aea09
4 changed files with 242 additions and 29 deletions

View file

@ -143,12 +143,12 @@ public abstract class BaseMediaSource implements MediaSource {
@Override
public final void addDrmEventListener(Handler handler, DrmSessionEventListener eventListener) {
eventDispatcher.addEventListener(handler, eventListener);
eventDispatcher.addEventListener(handler, eventListener, DrmSessionEventListener.class);
}
@Override
public final void removeDrmEventListener(DrmSessionEventListener eventListener) {
eventDispatcher.removeEventListener(eventListener);
eventDispatcher.removeEventListener(eventListener, DrmSessionEventListener.class);
}
@Override

View file

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source;
import android.net.Uri;
import android.os.Handler;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
@ -174,7 +175,7 @@ public interface MediaSourceEventListener {
}
private EventDispatcher(
CopyOnWriteMultiset<ListenerAndHandler> listeners,
CopyOnWriteMultiset<ListenerInfo> listeners,
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
long mediaTimeOffsetMs) {
@ -184,8 +185,34 @@ public interface MediaSourceEventListener {
@Override
public EventDispatcher withParameters(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
return new EventDispatcher(
listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
return new EventDispatcher(listenerInfos, 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() {

View file

@ -50,25 +50,25 @@ public class MediaSourceEventDispatcher {
@Nullable public final MediaPeriodId mediaPeriodId;
// 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.
protected final long mediaTimeOffsetMs;
/** Creates an event dispatcher. */
public MediaSourceEventDispatcher() {
this(
/* listenerAndHandlers= */ new CopyOnWriteMultiset<>(),
/* listenerInfos= */ new CopyOnWriteMultiset<>(),
/* windowIndex= */ 0,
/* mediaPeriodId= */ null,
/* mediaTimeOffsetMs= */ 0);
}
protected MediaSourceEventDispatcher(
CopyOnWriteMultiset<ListenerAndHandler> listenerAndHandlers,
CopyOnWriteMultiset<ListenerInfo> listenerInfos,
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
long mediaTimeOffsetMs) {
this.listenerAndHandlers = listenerAndHandlers;
this.listenerInfos = listenerInfos;
this.windowIndex = windowIndex;
this.mediaPeriodId = mediaPeriodId;
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
@ -87,30 +87,45 @@ public class MediaSourceEventDispatcher {
public MediaSourceEventDispatcher withParameters(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
return new MediaSourceEventDispatcher(
listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
listenerInfos, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
}
/**
* 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 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(eventListener);
listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener));
listenerInfos.add(new ListenerInfo(handler, eventListener, listenerClass));
}
/**
* Removes a listener from the event dispatcher.
*
* @param eventListener The listener to be removed.
* @param listenerClass The listener type passed to {@link #addEventListener(Handler, Object,
* Class)}.
*/
public void removeEventListener(Object eventListener) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
if (listenerAndHandler.listener == eventListener) {
listenerAndHandlers.remove(listenerAndHandler);
public <T> void removeEventListener(T eventListener, Class<T> listenerClass) {
for (ListenerInfo listenerInfo : listenerInfos) {
if (listenerInfo.listener == eventListener
&& listenerInfo.listenerClass.equals(listenerClass)) {
listenerInfos.remove(listenerInfo);
}
}
}
@ -118,11 +133,11 @@ public class MediaSourceEventDispatcher {
/** Dispatches {@code event} to all registered listeners of type {@code listenerClass}. */
@SuppressWarnings("unchecked") // The cast is gated with listenerClass.isInstance()
public <T> void dispatch(EventWithPeriodId<T> event, Class<T> listenerClass) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers.elementSet()) {
if (listenerClass.isInstance(listenerAndHandler.listener)) {
for (ListenerInfo listenerInfo : listenerInfos.elementSet()) {
if (listenerInfo.listenerClass.equals(listenerClass)) {
postOrRun(
listenerAndHandler.handler,
() -> event.sendTo((T) listenerAndHandler.listener, windowIndex, mediaPeriodId));
listenerInfo.handler,
() -> event.sendTo((T) listenerInfo.listener, windowIndex, mediaPeriodId));
}
}
}
@ -140,15 +155,17 @@ public class MediaSourceEventDispatcher {
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
/** Container class for a {@link Handler} and {@code listener} object. */
protected static final class ListenerAndHandler {
/** Container class for a {@link Handler}, {@code listener} and {@code listenerClass}. */
protected static final class ListenerInfo {
public final Handler handler;
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.listener = listener;
this.listenerClass = listenerClass;
}
@Override
@ -156,19 +173,21 @@ public class MediaSourceEventDispatcher {
if (this == o) {
return true;
}
if (!(o instanceof ListenerAndHandler)) {
if (!(o instanceof ListenerInfo)) {
return false;
}
// We deliberately only consider listener (and not handler) in equals() and hashcode()
// because the handler used to process the callbacks is an implementation detail.
ListenerAndHandler that = (ListenerAndHandler) o;
return listener.equals(that.listener);
ListenerInfo that = (ListenerInfo) o;
// We deliberately only consider listener and listenerClass (and not handler) in equals() and
// hashcode() because the handler used to process the callbacks is an implementation detail.
return listener.equals(that.listener) && listenerClass.equals(that.listenerClass);
}
@Override
public int hashCode() {
return listener.hashCode();
int result = 31 * listener.hashCode();
return result + 31 * listenerClass.hashCode();
}
}
}

View file

@ -0,0 +1,167 @@
/*
* 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 {}
}