/*
* Copyright (C) 2017 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 androidx.media3.exoplayer.source;
import static androidx.media3.common.util.Util.postOrRun;
import android.os.Handler;
import androidx.annotation.CheckResult;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.C.DataType;
import androidx.media3.common.Format;
import androidx.media3.common.Player;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;
/** Interface for callbacks to be notified of {@link MediaSource} events. */
@UnstableApi
public interface MediaSourceEventListener {
/**
* Called when a load begins.
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
* belong to a specific media period.
* @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The value of {@link
* LoadEventInfo#uri} won't reflect potential redirection yet and {@link
* LoadEventInfo#responseHeaders} will be empty.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
default void onLoadStarted(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {}
/**
* Called when a load ends.
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
* belong to a specific media period.
* @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
* LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
* corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
* event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
default void onLoadCompleted(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {}
/**
* Called when a load is canceled.
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
* belong to a specific media period.
* @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
* LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
* corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
* event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
*/
default void onLoadCanceled(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData) {}
/**
* Called when a load error occurs.
*
* <p>The error may or may not have resulted in the load being canceled, as indicated by the
* {@code wasCanceled} parameter. If the load was canceled, {@link #onLoadCanceled} will
* <em>not</em> be called in addition to this method.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.Listener#onPlayerError} is the appropriate place to implement such behavior. This
* method is called to provide the application with an opportunity to log the error if it wishes
* to do so.
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} this load belongs to. Null if the load does not
* belong to a specific media period.
* @param loadEventInfo The {@link LoadEventInfo} corresponding to the event. The values of {@link
* LoadEventInfo#elapsedRealtimeMs} and {@link LoadEventInfo#bytesLoaded} are relative to the
* corresponding {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}
* event.
* @param mediaLoadData The {@link MediaLoadData} defining the data being loaded.
* @param error The load error.
* @param wasCanceled Whether the load was canceled as a result of the error.
*/
default void onLoadError(
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {}
/**
* Called when data is removed from the back of a media buffer, typically so that it can be
* re-buffered in a different format.
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.
* @param mediaLoadData The {@link MediaLoadData} defining the media being discarded.
*/
default void onUpstreamDiscarded(
int windowIndex, MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}
/**
* Called when a downstream format change occurs (i.e. when the format of the media being read
* from one or more {@link SampleStream}s provided by the source changes).
*
* @param windowIndex The window index in the timeline of the media source this load belongs to.
* @param mediaPeriodId The {@link MediaPeriodId} the media belongs to.
* @param mediaLoadData The {@link MediaLoadData} defining the newly selected downstream data.
*/
default void onDownstreamFormatChanged(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, MediaLoadData mediaLoadData) {}
/** Dispatches events to {@link MediaSourceEventListener MediaSourceEventListeners}. */
class EventDispatcher {
/** The timeline window index reported with the events. */
public final int windowIndex;
/** The {@link MediaPeriodId} reported with the events. */
@Nullable public final MediaPeriodId mediaPeriodId;
private final CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers;
private final long mediaTimeOffsetMs;
/** Creates an event dispatcher. */
public EventDispatcher() {
this(
/* listenerAndHandlers= */ new CopyOnWriteArrayList<>(),
/* windowIndex= */ 0,
/* mediaPeriodId= */ null,
/* mediaTimeOffsetMs= */ 0);
}
private EventDispatcher(
CopyOnWriteArrayList<ListenerAndHandler> listenerAndHandlers,
int windowIndex,
@Nullable MediaPeriodId mediaPeriodId,
long mediaTimeOffsetMs) {
this.listenerAndHandlers = listenerAndHandlers;
this.windowIndex = windowIndex;
this.mediaPeriodId = mediaPeriodId;
this.mediaTimeOffsetMs = mediaTimeOffsetMs;
}
/**
* Creates a view of the event dispatcher with pre-configured window index, media period id, and
* media time offset.
*
* @param windowIndex The timeline window index to be reported with the events.
* @param mediaPeriodId The {@link MediaPeriodId} to be reported with the events.
* @param mediaTimeOffsetMs The offset to be added to all media times, in milliseconds.
* @return A view of the event dispatcher with the pre-configured parameters.
*/
@CheckResult
public EventDispatcher withParameters(
int windowIndex, @Nullable MediaPeriodId mediaPeriodId, long mediaTimeOffsetMs) {
return new EventDispatcher(
listenerAndHandlers, windowIndex, mediaPeriodId, mediaTimeOffsetMs);
}
/**
* Adds a listener to the event dispatcher.
*
* @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) {
Assertions.checkNotNull(handler);
Assertions.checkNotNull(eventListener);
listenerAndHandlers.add(new ListenerAndHandler(handler, eventListener));
}
/**
* Removes a listener from the event dispatcher.
*
* @param eventListener The listener to be removed.
*/
public void removeEventListener(MediaSourceEventListener eventListener) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
if (listenerAndHandler.listener == eventListener) {
listenerAndHandlers.remove(listenerAndHandler);
}
}
}
/** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadStarted(LoadEventInfo loadEventInfo, @DataType int dataType) {
loadStarted(
loadEventInfo,
dataType,
/* trackType= */ C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
/* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ C.TIME_UNSET,
/* mediaEndTimeUs= */ C.TIME_UNSET);
}
/** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadStarted(
LoadEventInfo loadEventInfo,
@DataType int dataType,
@C.TrackType int trackType,
@Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs) {
loadStarted(
loadEventInfo,
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
/** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadStarted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
}
}
/** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCompleted(LoadEventInfo loadEventInfo, @DataType int dataType) {
loadCompleted(
loadEventInfo,
dataType,
/* trackType= */ C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
/* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ C.TIME_UNSET,
/* mediaEndTimeUs= */ C.TIME_UNSET);
}
/** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCompleted(
LoadEventInfo loadEventInfo,
@DataType int dataType,
@C.TrackType int trackType,
@Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs) {
loadCompleted(
loadEventInfo,
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
/** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCompleted(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() ->
listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
}
}
/** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(LoadEventInfo loadEventInfo, @DataType int dataType) {
loadCanceled(
loadEventInfo,
dataType,
/* trackType= */ C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
/* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ C.TIME_UNSET,
/* mediaEndTimeUs= */ C.TIME_UNSET);
}
/** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(
LoadEventInfo loadEventInfo,
@DataType int dataType,
@C.TrackType int trackType,
@Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs) {
loadCanceled(
loadEventInfo,
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
/** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */
public void loadCanceled(LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() ->
listener.onLoadCanceled(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData));
}
}
/**
* Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
* boolean)}.
*/
public void loadError(
LoadEventInfo loadEventInfo,
@DataType int dataType,
IOException error,
boolean wasCanceled) {
loadError(
loadEventInfo,
dataType,
/* trackType= */ C.TRACK_TYPE_UNKNOWN,
/* trackFormat= */ null,
/* trackSelectionReason= */ C.SELECTION_REASON_UNKNOWN,
/* trackSelectionData= */ null,
/* mediaStartTimeUs= */ C.TIME_UNSET,
/* mediaEndTimeUs= */ C.TIME_UNSET,
error,
wasCanceled);
}
/**
* Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
* boolean)}.
*/
public void loadError(
LoadEventInfo loadEventInfo,
@DataType int dataType,
@C.TrackType int trackType,
@Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaStartTimeUs,
long mediaEndTimeUs,
IOException error,
boolean wasCanceled) {
loadError(
loadEventInfo,
new MediaLoadData(
dataType,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)),
error,
wasCanceled);
}
/**
* Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException,
* boolean)}.
*/
public void loadError(
LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData,
IOException error,
boolean wasCanceled) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() ->
listener.onLoadError(
windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled));
}
}
/** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */
public void upstreamDiscarded(int trackType, long mediaStartTimeUs, long mediaEndTimeUs) {
upstreamDiscarded(
new MediaLoadData(
C.DATA_TYPE_MEDIA,
trackType,
/* trackFormat= */ null,
C.SELECTION_REASON_ADAPTIVE,
/* trackSelectionData= */ null,
adjustMediaTime(mediaStartTimeUs),
adjustMediaTime(mediaEndTimeUs)));
}
/** Dispatches {@link #onUpstreamDiscarded(int, MediaPeriodId, MediaLoadData)}. */
public void upstreamDiscarded(MediaLoadData mediaLoadData) {
MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId);
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData));
}
}
/** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */
public void downstreamFormatChanged(
@C.TrackType int trackType,
@Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason,
@Nullable Object trackSelectionData,
long mediaTimeUs) {
downstreamFormatChanged(
new MediaLoadData(
C.DATA_TYPE_MEDIA,
trackType,
trackFormat,
trackSelectionReason,
trackSelectionData,
adjustMediaTime(mediaTimeUs),
/* mediaEndTimeMs= */ C.TIME_UNSET));
}
/** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */
public void downstreamFormatChanged(MediaLoadData mediaLoadData) {
for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) {
MediaSourceEventListener listener = listenerAndHandler.listener;
postOrRun(
listenerAndHandler.handler,
() -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData));
}
}
private long adjustMediaTime(long mediaTimeUs) {
long mediaTimeMs = Util.usToMs(mediaTimeUs);
return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs;
}
private static final class ListenerAndHandler {
public Handler handler;
public MediaSourceEventListener listener;
public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) {
this.handler = handler;
this.listener = listener;
}
}
}
}