/* * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.source; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import androidx.annotation.CheckResult; import androidx.annotation.Nullable; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.Player; import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DataSpec; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; /** Interface for callbacks to be notified of {@link MediaSource} events. */ public interface MediaSourceEventListener { /** Media source load event information. */ final class LoadEventInfo { /** Defines the requested data. */ public final DataSpec dataSpec; /** * The {@link Uri} from which data is being read. The uri will be identical to the one in {@link * #dataSpec}.uri unless redirection has occurred. If redirection has occurred, this is the uri * after redirection. */ public final Uri uri; /** The response headers associated with the load, or an empty map if unavailable. */ public final Map> responseHeaders; /** The value of {@link SystemClock#elapsedRealtime} at the time of the load event. */ public final long elapsedRealtimeMs; /** The duration of the load up to the event time. */ public final long loadDurationMs; /** The number of bytes that were loaded up to the event time. */ public final long bytesLoaded; /** * Creates load event info. * * @param dataSpec Defines the requested data. * @param uri The {@link Uri} from which data is being read. The uri must be identical to the * one in {@code dataSpec.uri} unless redirection has occurred. If redirection has occurred, * this is the uri after redirection. * @param responseHeaders The response headers associated with the load, or an empty map if * unavailable. * @param elapsedRealtimeMs The value of {@link SystemClock#elapsedRealtime} at the time of the * load event. * @param loadDurationMs The duration of the load up to the event time. * @param bytesLoaded The number of bytes that were loaded up to the event time. For compressed * network responses, this is the decompressed size. */ public LoadEventInfo( DataSpec dataSpec, Uri uri, Map> responseHeaders, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { this.dataSpec = dataSpec; this.uri = uri; this.responseHeaders = responseHeaders; this.elapsedRealtimeMs = elapsedRealtimeMs; this.loadDurationMs = loadDurationMs; this.bytesLoaded = bytesLoaded; } } /** Descriptor for data being loaded or selected by a media source. */ final class MediaLoadData { /** One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. */ public final int dataType; /** * One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds to media of a * specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. */ public final int trackType; /** * The format of the track to which the data belongs. Null if the data does not belong to a * specific track. */ @Nullable public final Format trackFormat; /** * One of the {@link C} {@code SELECTION_REASON_*} constants if the data belongs to a track. * {@link C#SELECTION_REASON_UNKNOWN} otherwise. */ public final int trackSelectionReason; /** * Optional data associated with the selection of the track to which the data belongs. Null if * the data does not belong to a track. */ @Nullable public final Object trackSelectionData; /** * The start time of the media, or {@link C#TIME_UNSET} if the data does not belong to a * specific media period. */ public final long mediaStartTimeMs; /** * The end time of the media, or {@link C#TIME_UNSET} if the data does not belong to a specific * media period or the end time is unknown. */ public final long mediaEndTimeMs; /** * Creates media load data. * * @param dataType One of the {@link C} {@code DATA_TYPE_*} constants defining the type of data. * @param trackType One of the {@link C} {@code TRACK_TYPE_*} constants if the data corresponds * to media of a specific type. {@link C#TRACK_TYPE_UNKNOWN} otherwise. * @param trackFormat The format of the track to which the data belongs. Null if the data does * not belong to a track. * @param trackSelectionReason One of the {@link C} {@code SELECTION_REASON_*} constants if the * data belongs to a track. {@link C#SELECTION_REASON_UNKNOWN} otherwise. * @param trackSelectionData Optional data associated with the selection of the track to which * the data belongs. Null if the data does not belong to a track. * @param mediaStartTimeMs The start time of the media, or {@link C#TIME_UNSET} if the data does * not belong to a specific media period. * @param mediaEndTimeMs The end time of the media, or {@link C#TIME_UNSET} if the data does not * belong to a specific media period or the end time is unknown. */ public MediaLoadData( int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeMs, long mediaEndTimeMs) { this.dataType = dataType; this.trackType = trackType; this.trackFormat = trackFormat; this.trackSelectionReason = trackSelectionReason; this.trackSelectionData = trackSelectionData; this.mediaStartTimeMs = mediaStartTimeMs; this.mediaEndTimeMs = mediaEndTimeMs; } } /** * Called when a media period is created by the media source. * * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the created media period. */ default void onMediaPeriodCreated(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * Called when a media period is released by the media source. * * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the released media period. */ default void onMediaPeriodReleased(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * 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. * *

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 * not be called in addition to this method. * *

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 and continue. Hence applications should * not implement this method to display a user visible error or initiate an application * level retry ({@link Player.EventListener#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 a media period is first being read from. * * @param windowIndex The window index in the timeline this media period belongs to. * @param mediaPeriodId The {@link MediaPeriodId} of the media period being read from. */ default void onReadingStarted(int windowIndex, MediaPeriodId mediaPeriodId) {} /** * 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}s. */ final 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 listenerAndHandlers; private final long mediaTimeOffsetMs; /** Creates an event dispatcher. */ public EventDispatcher() { this( /* listenerAndHandlers= */ new CopyOnWriteArrayList<>(), /* windowIndex= */ 0, /* mediaPeriodId= */ null, /* mediaTimeOffsetMs= */ 0); } private EventDispatcher( CopyOnWriteArrayList 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.checkArgument(handler != null && eventListener != null); 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 #onMediaPeriodCreated(int, MediaPeriodId)}. */ public void mediaPeriodCreated() { MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onMediaPeriodCreated(windowIndex, mediaPeriodId)); } } /** Dispatches {@link #onMediaPeriodReleased(int, MediaPeriodId)}. */ public void mediaPeriodReleased() { MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onMediaPeriodReleased(windowIndex, mediaPeriodId)); } } /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted(DataSpec dataSpec, int dataType, long elapsedRealtimeMs) { loadStarted( dataSpec, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs); } /** Dispatches {@link #onLoadStarted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadStarted( DataSpec dataSpec, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, long mediaEndTimeUs, long elapsedRealtimeMs) { loadStarted( new LoadEventInfo( dataSpec, dataSpec.uri, /* responseHeaders= */ Collections.emptyMap(), elapsedRealtimeMs, /* loadDurationMs= */ 0, /* bytesLoaded= */ 0), 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) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onLoadStarted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); } } /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCompleted( dataSpec, uri, responseHeaders, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); } /** Dispatches {@link #onLoadCompleted(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCompleted( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, long mediaEndTimeUs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCompleted( new LoadEventInfo( dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), 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) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onLoadCompleted(windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData)); } } /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCanceled( dataSpec, uri, responseHeaders, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded); } /** Dispatches {@link #onLoadCanceled(int, MediaPeriodId, LoadEventInfo, MediaLoadData)}. */ public void loadCanceled( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, long mediaEndTimeUs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded) { loadCanceled( new LoadEventInfo( dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), 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( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { loadError( dataSpec, uri, responseHeaders, dataType, C.TRACK_TYPE_UNKNOWN, null, C.SELECTION_REASON_UNKNOWN, null, C.TIME_UNSET, C.TIME_UNSET, elapsedRealtimeMs, loadDurationMs, bytesLoaded, error, wasCanceled); } /** * Dispatches {@link #onLoadError(int, MediaPeriodId, LoadEventInfo, MediaLoadData, IOException, * boolean)}. */ public void loadError( DataSpec dataSpec, Uri uri, Map> responseHeaders, int dataType, int trackType, @Nullable Format trackFormat, int trackSelectionReason, @Nullable Object trackSelectionData, long mediaStartTimeUs, long mediaEndTimeUs, long elapsedRealtimeMs, long loadDurationMs, long bytesLoaded, IOException error, boolean wasCanceled) { loadError( new LoadEventInfo( dataSpec, uri, responseHeaders, elapsedRealtimeMs, loadDurationMs, bytesLoaded), 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) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onLoadError( windowIndex, mediaPeriodId, loadEventInfo, mediaLoadData, error, wasCanceled)); } } /** Dispatches {@link #onReadingStarted(int, MediaPeriodId)}. */ public void readingStarted() { MediaPeriodId mediaPeriodId = Assertions.checkNotNull(this.mediaPeriodId); for (ListenerAndHandler listenerAndHandler : listenerAndHandlers) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onReadingStarted(windowIndex, mediaPeriodId)); } } /** 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) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onUpstreamDiscarded(windowIndex, mediaPeriodId, mediaLoadData)); } } /** Dispatches {@link #onDownstreamFormatChanged(int, MediaPeriodId, MediaLoadData)}. */ public void downstreamFormatChanged( int trackType, @Nullable Format trackFormat, 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) { final MediaSourceEventListener listener = listenerAndHandler.listener; postOrRun( listenerAndHandler.handler, () -> listener.onDownstreamFormatChanged(windowIndex, mediaPeriodId, mediaLoadData)); } } private long adjustMediaTime(long mediaTimeUs) { long mediaTimeMs = C.usToMs(mediaTimeUs); return mediaTimeMs == C.TIME_UNSET ? C.TIME_UNSET : mediaTimeOffsetMs + mediaTimeMs; } private void postOrRun(Handler handler, Runnable runnable) { if (handler.getLooper() == Looper.myLooper()) { runnable.run(); } else { handler.post(runnable); } } private static final class ListenerAndHandler { public final Handler handler; public final MediaSourceEventListener listener; public ListenerAndHandler(Handler handler, MediaSourceEventListener listener) { this.handler = handler; this.listener = listener; } } } }