mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
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:
parent
c294e0cb89
commit
354d5aea09
4 changed files with 242 additions and 29 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
}
|
||||
Loading…
Reference in a new issue