media/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
tonihei dd7fb8178a Handle timeline updates where all periods in window have been replaced
This case is most likely to happen when re-preparing a multi-period
live stream after an error. The live timeline can easily move on to
new periods in the meantime, creating this type of update.

The behavior before this change has two bugs:
 - The player resolves the new start position to a subsequent period
   that existed in the old timeline, or ends playback if that cannot
   be found. The more useful behavior is to restart playback in the
   same live item if it still exists.
-  MaskingMediaSource creates a pending MaskingMediaPeriod using the
   old timeline and then attempts to create the real period from the
   updated source. This fails because MediaSource.createPeriod is
   called with a periodUid that does no longer exist at this point.
   We already have logic to not override the start position and need
   to extend this to also not prepare the real source.

Issue: androidx/media#1329
PiperOrigin-RevId: 634833030
2024-05-17 11:16:12 -07:00

3522 lines
132 KiB
Java

/*
* Copyright (C) 2016 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;
import static androidx.media3.common.C.TRACK_TYPE_AUDIO;
import static androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION;
import static androidx.media3.common.C.TRACK_TYPE_IMAGE;
import static androidx.media3.common.C.TRACK_TYPE_VIDEO;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
import static androidx.media3.exoplayer.Renderer.MSG_SET_AUDIO_ATTRIBUTES;
import static androidx.media3.exoplayer.Renderer.MSG_SET_AUDIO_SESSION_ID;
import static androidx.media3.exoplayer.Renderer.MSG_SET_AUX_EFFECT_INFO;
import static androidx.media3.exoplayer.Renderer.MSG_SET_CAMERA_MOTION_LISTENER;
import static androidx.media3.exoplayer.Renderer.MSG_SET_CHANGE_FRAME_RATE_STRATEGY;
import static androidx.media3.exoplayer.Renderer.MSG_SET_IMAGE_OUTPUT;
import static androidx.media3.exoplayer.Renderer.MSG_SET_PREFERRED_AUDIO_DEVICE;
import static androidx.media3.exoplayer.Renderer.MSG_SET_PRIORITY;
import static androidx.media3.exoplayer.Renderer.MSG_SET_SCALING_MODE;
import static androidx.media3.exoplayer.Renderer.MSG_SET_SKIP_SILENCE_ENABLED;
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_EFFECTS;
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER;
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_OUTPUT;
import static androidx.media3.exoplayer.Renderer.MSG_SET_VIDEO_OUTPUT_RESOLUTION;
import static androidx.media3.exoplayer.Renderer.MSG_SET_VOLUME;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaFormat;
import android.media.metrics.LogSessionId;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
import androidx.media3.common.BasePlayer;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Effect;
import androidx.media3.common.Format;
import androidx.media3.common.IllegalSeekPositionException;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player;
import androidx.media3.common.PriorityTaskManager;
import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoFrameProcessor;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.PlayerMessage.Target;
import androidx.media3.exoplayer.Renderer.MessageType;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.analytics.MediaMetricsListener;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.image.ImageOutput;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.ShuffleOrder;
import androidx.media3.exoplayer.source.TimelineWithUpdatedMediaItem;
import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.text.TextOutput;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.trackselection.TrackSelectionArray;
import androidx.media3.exoplayer.trackselection.TrackSelector;
import androidx.media3.exoplayer.trackselection.TrackSelectorResult;
import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.video.VideoDecoderOutputBufferRenderer;
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
import androidx.media3.exoplayer.video.VideoRendererEventListener;
import androidx.media3.exoplayer.video.spherical.CameraMotionListener;
import androidx.media3.exoplayer.video.spherical.SphericalGLSurfaceView;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeoutException;
/** The default implementation of {@link ExoPlayer}. */
/* package */ final class ExoPlayerImpl extends BasePlayer
implements ExoPlayer,
ExoPlayer.AudioComponent,
ExoPlayer.VideoComponent,
ExoPlayer.TextComponent,
ExoPlayer.DeviceComponent {
static {
MediaLibraryInfo.registerModule("media3.exoplayer");
}
private static final String TAG = "ExoPlayerImpl";
/**
* This empty track selector result can only be used for {@link PlaybackInfo#trackSelectorResult}
* when the player does not have any track selection made (such as when player is reset, or when
* player seeks to an unprepared period). It will not be used as result of any {@link
* TrackSelector#selectTracks(RendererCapabilities[], TrackGroupArray, MediaPeriodId, Timeline)}
* operation.
*/
/* package */ final TrackSelectorResult emptyTrackSelectorResult;
/* package */ final Commands permanentAvailableCommands;
private final ConditionVariable constructorFinished;
private final Context applicationContext;
private final Player wrappingPlayer;
private final Renderer[] renderers;
private final TrackSelector trackSelector;
private final HandlerWrapper playbackInfoUpdateHandler;
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
private final ExoPlayerImplInternal internalPlayer;
private final ListenerSet<Listener> listeners;
private final CopyOnWriteArraySet<AudioOffloadListener> audioOffloadListeners;
private final Timeline.Period period;
private final List<MediaSourceHolderSnapshot> mediaSourceHolderSnapshots;
private final boolean useLazyPreparation;
private final MediaSource.Factory mediaSourceFactory;
private final AnalyticsCollector analyticsCollector;
private final Looper applicationLooper;
private final BandwidthMeter bandwidthMeter;
private final long seekBackIncrementMs;
private final long seekForwardIncrementMs;
private final Clock clock;
private final ComponentListener componentListener;
private final FrameMetadataListener frameMetadataListener;
private final AudioBecomingNoisyManager audioBecomingNoisyManager;
private final AudioFocusManager audioFocusManager;
@Nullable private final StreamVolumeManager streamVolumeManager;
private final WakeLockManager wakeLockManager;
private final WifiLockManager wifiLockManager;
private final long detachSurfaceTimeoutMs;
@Nullable private AudioManager audioManager;
private final boolean suppressPlaybackOnUnsuitableOutput;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
private int pendingOperationAcks;
private @DiscontinuityReason int pendingDiscontinuityReason;
private boolean pendingDiscontinuity;
private @PlayWhenReadyChangeReason int pendingPlayWhenReadyChangeReason;
private boolean foregroundMode;
private SeekParameters seekParameters;
private ShuffleOrder shuffleOrder;
private PreloadConfiguration preloadConfiguration;
private boolean pauseAtEndOfMediaItems;
private Commands availableCommands;
private MediaMetadata mediaMetadata;
private MediaMetadata playlistMetadata;
@Nullable private Format videoFormat;
@Nullable private Format audioFormat;
@Nullable private AudioTrack keepSessionIdAudioTrack;
@Nullable private Object videoOutput;
@Nullable private Surface ownedSurface;
@Nullable private SurfaceHolder surfaceHolder;
@Nullable private SphericalGLSurfaceView sphericalGLSurfaceView;
private boolean surfaceHolderSurfaceIsVideoOutput;
@Nullable private TextureView textureView;
private @C.VideoScalingMode int videoScalingMode;
private @C.VideoChangeFrameRateStrategy int videoChangeFrameRateStrategy;
private Size surfaceSize;
@Nullable private DecoderCounters videoDecoderCounters;
@Nullable private DecoderCounters audioDecoderCounters;
private int audioSessionId;
private AudioAttributes audioAttributes;
private float volume;
private boolean skipSilenceEnabled;
private CueGroup currentCueGroup;
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
@Nullable private CameraMotionListener cameraMotionListener;
private boolean throwsWhenUsingWrongThread;
private boolean hasNotifiedFullWrongThreadWarning;
private @C.Priority int priority;
@Nullable private PriorityTaskManager priorityTaskManager;
private boolean isPriorityTaskManagerRegistered;
private boolean playerReleased;
private DeviceInfo deviceInfo;
private VideoSize videoSize;
// MediaMetadata built from static (TrackGroup Format) and dynamic (onMetadata(Metadata)) metadata
// sources.
private MediaMetadata staticAndDynamicMediaMetadata;
// Playback information when there is no pending seek/set source operation.
private PlaybackInfo playbackInfo;
// Playback information when there is a pending seek/set source operation.
private int maskingWindowIndex;
private int maskingPeriodIndex;
private long maskingWindowPositionMs;
@SuppressLint("HandlerLeak")
@SuppressWarnings("deprecation") // Control flow for old volume commands
public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer) {
constructorFinished = new ConditionVariable();
try {
Log.i(
TAG,
"Init "
+ Integer.toHexString(System.identityHashCode(this))
+ " ["
+ MediaLibraryInfo.VERSION_SLASHY
+ "] ["
+ Util.DEVICE_DEBUG_INFO
+ "]");
applicationContext = builder.context.getApplicationContext();
analyticsCollector = builder.analyticsCollectorFunction.apply(builder.clock);
priority = builder.priority;
priorityTaskManager = builder.priorityTaskManager;
audioAttributes = builder.audioAttributes;
videoScalingMode = builder.videoScalingMode;
videoChangeFrameRateStrategy = builder.videoChangeFrameRateStrategy;
skipSilenceEnabled = builder.skipSilenceEnabled;
detachSurfaceTimeoutMs = builder.detachSurfaceTimeoutMs;
componentListener = new ComponentListener();
frameMetadataListener = new FrameMetadataListener();
Handler eventHandler = new Handler(builder.looper);
renderers =
builder
.renderersFactorySupplier
.get()
.createRenderers(
eventHandler,
componentListener,
componentListener,
componentListener,
componentListener);
checkState(renderers.length > 0);
this.trackSelector = builder.trackSelectorSupplier.get();
this.mediaSourceFactory = builder.mediaSourceFactorySupplier.get();
this.bandwidthMeter = builder.bandwidthMeterSupplier.get();
this.useLazyPreparation = builder.useLazyPreparation;
this.seekParameters = builder.seekParameters;
this.seekBackIncrementMs = builder.seekBackIncrementMs;
this.seekForwardIncrementMs = builder.seekForwardIncrementMs;
this.pauseAtEndOfMediaItems = builder.pauseAtEndOfMediaItems;
this.applicationLooper = builder.looper;
this.clock = builder.clock;
this.wrappingPlayer = wrappingPlayer == null ? this : wrappingPlayer;
this.suppressPlaybackOnUnsuitableOutput = builder.suppressPlaybackOnUnsuitableOutput;
listeners =
new ListenerSet<>(
applicationLooper,
clock,
(listener, flags) -> listener.onEvents(this.wrappingPlayer, new Events(flags)));
audioOffloadListeners = new CopyOnWriteArraySet<>();
mediaSourceHolderSnapshots = new ArrayList<>();
shuffleOrder = new ShuffleOrder.DefaultShuffleOrder(/* length= */ 0);
preloadConfiguration = PreloadConfiguration.DEFAULT;
emptyTrackSelectorResult =
new TrackSelectorResult(
new RendererConfiguration[renderers.length],
new ExoTrackSelection[renderers.length],
Tracks.EMPTY,
/* info= */ null);
period = new Timeline.Period();
permanentAvailableCommands =
new Commands.Builder()
.addAll(
COMMAND_PLAY_PAUSE,
COMMAND_PREPARE,
COMMAND_STOP,
COMMAND_SET_SPEED_AND_PITCH,
COMMAND_SET_SHUFFLE_MODE,
COMMAND_SET_REPEAT_MODE,
COMMAND_GET_CURRENT_MEDIA_ITEM,
COMMAND_GET_TIMELINE,
COMMAND_GET_METADATA,
COMMAND_SET_PLAYLIST_METADATA,
COMMAND_SET_MEDIA_ITEM,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS,
COMMAND_GET_AUDIO_ATTRIBUTES,
COMMAND_SET_AUDIO_ATTRIBUTES,
COMMAND_GET_VOLUME,
COMMAND_SET_VOLUME,
COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT,
COMMAND_RELEASE)
.addIf(
COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported())
.addIf(COMMAND_GET_DEVICE_VOLUME, builder.deviceVolumeControlEnabled)
.addIf(COMMAND_SET_DEVICE_VOLUME, builder.deviceVolumeControlEnabled)
.addIf(COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS, builder.deviceVolumeControlEnabled)
.addIf(COMMAND_ADJUST_DEVICE_VOLUME, builder.deviceVolumeControlEnabled)
.addIf(COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS, builder.deviceVolumeControlEnabled)
.build();
availableCommands =
new Commands.Builder()
.addAll(permanentAvailableCommands)
.add(COMMAND_SEEK_TO_DEFAULT_POSITION)
.add(COMMAND_SEEK_TO_MEDIA_ITEM)
.build();
playbackInfoUpdateHandler = clock.createHandler(applicationLooper, /* callback= */ null);
playbackInfoUpdateListener =
playbackInfoUpdate ->
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
analyticsCollector.setPlayer(this.wrappingPlayer, applicationLooper);
PlayerId playerId =
Util.SDK_INT < 31
? new PlayerId(builder.playerName)
: Api31.registerMediaMetricsListener(
applicationContext,
/* player= */ this,
builder.usePlatformDiagnostics,
builder.playerName);
internalPlayer =
new ExoPlayerImplInternal(
renderers,
trackSelector,
emptyTrackSelectorResult,
builder.loadControlSupplier.get(),
bandwidthMeter,
repeatMode,
shuffleModeEnabled,
analyticsCollector,
seekParameters,
builder.livePlaybackSpeedControl,
builder.releaseTimeoutMs,
pauseAtEndOfMediaItems,
applicationLooper,
clock,
playbackInfoUpdateListener,
playerId,
builder.playbackLooper,
preloadConfiguration);
volume = 1;
repeatMode = Player.REPEAT_MODE_OFF;
mediaMetadata = MediaMetadata.EMPTY;
playlistMetadata = MediaMetadata.EMPTY;
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
maskingWindowIndex = C.INDEX_UNSET;
if (Util.SDK_INT < 21) {
audioSessionId = initializeKeepSessionIdAudioTrack(C.AUDIO_SESSION_ID_UNSET);
} else {
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
}
currentCueGroup = CueGroup.EMPTY_TIME_ZERO;
throwsWhenUsingWrongThread = true;
addListener(analyticsCollector);
bandwidthMeter.addEventListener(new Handler(applicationLooper), analyticsCollector);
addAudioOffloadListener(componentListener);
if (builder.foregroundModeTimeoutMs > 0) {
internalPlayer.experimentalSetForegroundModeTimeoutMs(builder.foregroundModeTimeoutMs);
}
audioBecomingNoisyManager =
new AudioBecomingNoisyManager(builder.context, eventHandler, componentListener);
audioBecomingNoisyManager.setEnabled(builder.handleAudioBecomingNoisy);
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
Api23.registerAudioDeviceCallback(
audioManager,
new NoSuitableOutputPlaybackSuppressionAudioDeviceCallback(),
new Handler(applicationLooper));
}
if (builder.deviceVolumeControlEnabled) {
streamVolumeManager =
new StreamVolumeManager(builder.context, eventHandler, componentListener);
streamVolumeManager.setStreamType(Util.getStreamTypeForAudioUsage(audioAttributes.usage));
} else {
streamVolumeManager = null;
}
wakeLockManager = new WakeLockManager(builder.context);
wakeLockManager.setEnabled(builder.wakeMode != C.WAKE_MODE_NONE);
wifiLockManager = new WifiLockManager(builder.context);
wifiLockManager.setEnabled(builder.wakeMode == C.WAKE_MODE_NETWORK);
deviceInfo = createDeviceInfo(streamVolumeManager);
videoSize = VideoSize.UNKNOWN;
surfaceSize = Size.UNKNOWN;
trackSelector.setAudioAttributes(audioAttributes);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, audioAttributes);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_SCALING_MODE, videoScalingMode);
sendRendererMessage(
TRACK_TYPE_VIDEO, MSG_SET_CHANGE_FRAME_RATE_STRATEGY, videoChangeFrameRateStrategy);
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled);
sendRendererMessage(
TRACK_TYPE_VIDEO, MSG_SET_VIDEO_FRAME_METADATA_LISTENER, frameMetadataListener);
sendRendererMessage(
TRACK_TYPE_CAMERA_MOTION, MSG_SET_CAMERA_MOTION_LISTENER, frameMetadataListener);
sendRendererMessage(MSG_SET_PRIORITY, priority);
} finally {
constructorFinished.open();
}
}
@CanIgnoreReturnValue
@SuppressWarnings("deprecation") // Returning deprecated class.
@Override
@Deprecated
public AudioComponent getAudioComponent() {
verifyApplicationThread();
return this;
}
@CanIgnoreReturnValue
@SuppressWarnings("deprecation") // Returning deprecated class.
@Override
@Deprecated
public VideoComponent getVideoComponent() {
verifyApplicationThread();
return this;
}
@CanIgnoreReturnValue
@SuppressWarnings("deprecation") // Returning deprecated class.
@Override
@Deprecated
public TextComponent getTextComponent() {
verifyApplicationThread();
return this;
}
@CanIgnoreReturnValue
@SuppressWarnings("deprecation") // Returning deprecated class.
@Override
@Deprecated
public DeviceComponent getDeviceComponent() {
verifyApplicationThread();
return this;
}
@Override
public boolean isSleepingForOffload() {
verifyApplicationThread();
return playbackInfo.sleepingForOffload;
}
@Override
public Looper getPlaybackLooper() {
// Don't verify application thread. We allow calls to this method from any thread.
return internalPlayer.getPlaybackLooper();
}
@Override
public Looper getApplicationLooper() {
// Don't verify application thread. We allow calls to this method from any thread.
return applicationLooper;
}
@Override
public Clock getClock() {
// Don't verify application thread. We allow calls to this method from any thread.
return clock;
}
@Override
public void addAudioOffloadListener(AudioOffloadListener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
audioOffloadListeners.add(listener);
}
@Override
public void removeAudioOffloadListener(AudioOffloadListener listener) {
verifyApplicationThread();
audioOffloadListeners.remove(listener);
}
@Override
public Commands getAvailableCommands() {
verifyApplicationThread();
return availableCommands;
}
@Override
public @State int getPlaybackState() {
verifyApplicationThread();
return playbackInfo.playbackState;
}
@Override
public @PlaybackSuppressionReason int getPlaybackSuppressionReason() {
verifyApplicationThread();
return playbackInfo.playbackSuppressionReason;
}
@Override
@Nullable
public ExoPlaybackException getPlayerError() {
verifyApplicationThread();
return playbackInfo.playbackError;
}
@Override
public void prepare() {
verifyApplicationThread();
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING);
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
if (playbackInfo.playbackState != Player.STATE_IDLE) {
return;
}
PlaybackInfo playbackInfo = this.playbackInfo.copyWithPlaybackError(null);
playbackInfo =
playbackInfo.copyWithPlaybackState(
playbackInfo.timeline.isEmpty() ? STATE_ENDED : STATE_BUFFERING);
// Trigger internal prepare first before updating the playback info and notifying external
// listeners to ensure that new operations issued in the listener notifications reach the
// player after this prepare. The internal player can't change the playback info immediately
// because it uses a callback.
pendingOperationAcks++;
internalPlayer.prepare();
updatePlaybackInfo(
playbackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
@Deprecated
public void prepare(MediaSource mediaSource) {
verifyApplicationThread();
setMediaSource(mediaSource);
prepare();
}
@Override
@Deprecated
public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) {
verifyApplicationThread();
setMediaSource(mediaSource, resetPosition);
prepare();
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
verifyApplicationThread();
setMediaSources(createMediaSources(mediaItems), resetPosition);
}
@Override
public void setMediaItems(List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
verifyApplicationThread();
setMediaSources(createMediaSources(mediaItems), startIndex, startPositionMs);
}
@Override
public void setMediaSource(MediaSource mediaSource) {
verifyApplicationThread();
setMediaSources(Collections.singletonList(mediaSource));
}
@Override
public void setMediaSource(MediaSource mediaSource, long startPositionMs) {
verifyApplicationThread();
setMediaSources(
Collections.singletonList(mediaSource), /* startWindowIndex= */ 0, startPositionMs);
}
@Override
public void setMediaSource(MediaSource mediaSource, boolean resetPosition) {
verifyApplicationThread();
setMediaSources(Collections.singletonList(mediaSource), resetPosition);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread();
setMediaSources(mediaSources, /* resetPosition= */ true);
}
@Override
public void setMediaSources(List<MediaSource> mediaSources, boolean resetPosition) {
verifyApplicationThread();
setMediaSourcesInternal(
mediaSources,
/* startWindowIndex= */ C.INDEX_UNSET,
/* startPositionMs= */ C.TIME_UNSET,
/* resetToDefaultPosition= */ resetPosition);
}
@Override
public void setMediaSources(
List<MediaSource> mediaSources, int startWindowIndex, long startPositionMs) {
verifyApplicationThread();
setMediaSourcesInternal(
mediaSources, startWindowIndex, startPositionMs, /* resetToDefaultPosition= */ false);
}
@Override
public void addMediaItems(int index, List<MediaItem> mediaItems) {
verifyApplicationThread();
addMediaSources(index, createMediaSources(mediaItems));
}
@Override
public void addMediaSource(MediaSource mediaSource) {
verifyApplicationThread();
addMediaSources(Collections.singletonList(mediaSource));
}
@Override
public void addMediaSource(int index, MediaSource mediaSource) {
verifyApplicationThread();
addMediaSources(index, Collections.singletonList(mediaSource));
}
@Override
public void addMediaSources(List<MediaSource> mediaSources) {
verifyApplicationThread();
addMediaSources(/* index= */ mediaSourceHolderSnapshots.size(), mediaSources);
}
@Override
public void addMediaSources(int index, List<MediaSource> mediaSources) {
verifyApplicationThread();
checkArgument(index >= 0);
index = min(index, mediaSourceHolderSnapshots.size());
if (mediaSourceHolderSnapshots.isEmpty()) {
// Handle initial items in a playlist as a set operation to ensure state changes and initial
// position are updated correctly.
setMediaSources(mediaSources, /* resetPosition= */ maskingWindowIndex == C.INDEX_UNSET);
return;
}
PlaybackInfo newPlaybackInfo = addMediaSourcesInternal(playbackInfo, index, mediaSources);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread();
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
int playlistSize = mediaSourceHolderSnapshots.size();
toIndex = min(toIndex, playlistSize);
if (fromIndex >= playlistSize || fromIndex == toIndex) {
// Do nothing.
return;
}
PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(playbackInfo, fromIndex, toIndex);
boolean positionDiscontinuity =
!newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
positionDiscontinuity,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
verifyApplicationThread();
checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newFromIndex >= 0);
int playlistSize = mediaSourceHolderSnapshots.size();
toIndex = min(toIndex, playlistSize);
newFromIndex = min(newFromIndex, playlistSize - (toIndex - fromIndex));
if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newFromIndex) {
// Do nothing.
return;
}
Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++;
Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex);
Timeline newTimeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionUsAfterTimelineChanged(
oldTimeline,
newTimeline,
getCurrentWindowIndexInternal(playbackInfo),
getContentPositionInternal(playbackInfo)));
internalPlayer.moveMediaSources(fromIndex, toIndex, newFromIndex, shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void replaceMediaItems(int fromIndex, int toIndex, List<MediaItem> mediaItems) {
verifyApplicationThread();
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
int playlistSize = mediaSourceHolderSnapshots.size();
if (fromIndex > playlistSize) {
// Do nothing.
return;
}
toIndex = min(toIndex, playlistSize);
if (canUpdateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems)) {
// Update MediaSources directly without creating new ones if possible.
updateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems);
return;
}
List<MediaSource> mediaSources = createMediaSources(mediaItems);
if (mediaSourceHolderSnapshots.isEmpty()) {
// Handle initial items in a playlist as a set operation to ensure state changes and initial
// position are updated correctly.
setMediaSources(mediaSources, /* resetPosition= */ maskingWindowIndex == C.INDEX_UNSET);
return;
}
PlaybackInfo newPlaybackInfo = addMediaSourcesInternal(playbackInfo, toIndex, mediaSources);
newPlaybackInfo = removeMediaItemsInternal(newPlaybackInfo, fromIndex, toIndex);
boolean positionDiscontinuity =
!newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
positionDiscontinuity,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void setShuffleOrder(ShuffleOrder shuffleOrder) {
verifyApplicationThread();
checkArgument(shuffleOrder.getLength() == mediaSourceHolderSnapshots.size());
this.shuffleOrder = shuffleOrder;
Timeline timeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(
timeline, getCurrentMediaItemIndex(), getCurrentPosition()));
pendingOperationAcks++;
internalPlayer.setShuffleOrder(shuffleOrder);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public void setPauseAtEndOfMediaItems(boolean pauseAtEndOfMediaItems) {
verifyApplicationThread();
if (this.pauseAtEndOfMediaItems == pauseAtEndOfMediaItems) {
return;
}
this.pauseAtEndOfMediaItems = pauseAtEndOfMediaItems;
internalPlayer.setPauseAtEndOfWindow(pauseAtEndOfMediaItems);
}
@Override
public boolean getPauseAtEndOfMediaItems() {
verifyApplicationThread();
return pauseAtEndOfMediaItems;
}
@Override
public void setPlayWhenReady(boolean playWhenReady) {
verifyApplicationThread();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
}
@Override
public boolean getPlayWhenReady() {
verifyApplicationThread();
return playbackInfo.playWhenReady;
}
@Override
public void setRepeatMode(@RepeatMode int repeatMode) {
verifyApplicationThread();
if (this.repeatMode != repeatMode) {
this.repeatMode = repeatMode;
internalPlayer.setRepeatMode(repeatMode);
listeners.queueEvent(
Player.EVENT_REPEAT_MODE_CHANGED, listener -> listener.onRepeatModeChanged(repeatMode));
updateAvailableCommands();
listeners.flushEvents();
}
}
@Override
public @RepeatMode int getRepeatMode() {
verifyApplicationThread();
return repeatMode;
}
@Override
public void setShuffleModeEnabled(boolean shuffleModeEnabled) {
verifyApplicationThread();
if (this.shuffleModeEnabled != shuffleModeEnabled) {
this.shuffleModeEnabled = shuffleModeEnabled;
internalPlayer.setShuffleModeEnabled(shuffleModeEnabled);
listeners.queueEvent(
Player.EVENT_SHUFFLE_MODE_ENABLED_CHANGED,
listener -> listener.onShuffleModeEnabledChanged(shuffleModeEnabled));
updateAvailableCommands();
listeners.flushEvents();
}
}
@Override
public boolean getShuffleModeEnabled() {
verifyApplicationThread();
return shuffleModeEnabled;
}
@Override
public void setPreloadConfiguration(PreloadConfiguration preloadConfiguration) {
verifyApplicationThread();
if (this.preloadConfiguration.equals(preloadConfiguration)) {
return;
}
this.preloadConfiguration = preloadConfiguration;
internalPlayer.setPreloadConfiguration(preloadConfiguration);
}
@Override
public PreloadConfiguration getPreloadConfiguration() {
return preloadConfiguration;
}
@Override
public boolean isLoading() {
verifyApplicationThread();
return playbackInfo.isLoading;
}
@Override
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
verifyApplicationThread();
if (mediaItemIndex == C.INDEX_UNSET) {
return;
}
checkArgument(mediaItemIndex >= 0);
Timeline timeline = playbackInfo.timeline;
if (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount()) {
return;
}
analyticsCollector.notifySeekStarted();
pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate =
new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo);
playbackInfoUpdate.incrementPendingOperationAcks(1);
playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate);
return;
}
PlaybackInfo newPlaybackInfo = playbackInfo;
if (playbackInfo.playbackState == Player.STATE_READY
|| (playbackInfo.playbackState == Player.STATE_ENDED && !timeline.isEmpty())) {
newPlaybackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_BUFFERING);
}
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingMediaItemIndex,
isRepeatingCurrentItem);
}
@Override
public long getSeekBackIncrement() {
verifyApplicationThread();
return seekBackIncrementMs;
}
@Override
public long getSeekForwardIncrement() {
verifyApplicationThread();
return seekForwardIncrementMs;
}
@Override
public long getMaxSeekToPreviousPosition() {
verifyApplicationThread();
return C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS;
}
@Override
public void setPlaybackParameters(PlaybackParameters playbackParameters) {
verifyApplicationThread();
if (playbackParameters == null) {
playbackParameters = PlaybackParameters.DEFAULT;
}
if (playbackInfo.playbackParameters.equals(playbackParameters)) {
return;
}
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackParameters(playbackParameters);
pendingOperationAcks++;
internalPlayer.setPlaybackParameters(playbackParameters);
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@Override
public PlaybackParameters getPlaybackParameters() {
verifyApplicationThread();
return playbackInfo.playbackParameters;
}
@Override
public void setSeekParameters(@Nullable SeekParameters seekParameters) {
verifyApplicationThread();
if (seekParameters == null) {
seekParameters = SeekParameters.DEFAULT;
}
if (!this.seekParameters.equals(seekParameters)) {
this.seekParameters = seekParameters;
internalPlayer.setSeekParameters(seekParameters);
}
}
@Override
public SeekParameters getSeekParameters() {
verifyApplicationThread();
return seekParameters;
}
@Override
public void setForegroundMode(boolean foregroundMode) {
verifyApplicationThread();
if (this.foregroundMode != foregroundMode) {
this.foregroundMode = foregroundMode;
if (!internalPlayer.setForegroundMode(foregroundMode)) {
// One of the renderers timed out releasing its resources.
stopInternal(
ExoPlaybackException.createForUnexpected(
new ExoTimeoutException(ExoTimeoutException.TIMEOUT_OPERATION_SET_FOREGROUND_MODE),
PlaybackException.ERROR_CODE_TIMEOUT));
}
}
}
@Override
public void stop() {
verifyApplicationThread();
audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE);
stopInternal(/* error= */ null);
currentCueGroup = new CueGroup(ImmutableList.of(), playbackInfo.positionUs);
}
@Override
public void release() {
Log.i(
TAG,
"Release "
+ Integer.toHexString(System.identityHashCode(this))
+ " ["
+ MediaLibraryInfo.VERSION_SLASHY
+ "] ["
+ Util.DEVICE_DEBUG_INFO
+ "] ["
+ MediaLibraryInfo.registeredModules()
+ "]");
verifyApplicationThread();
if (Util.SDK_INT < 21 && keepSessionIdAudioTrack != null) {
keepSessionIdAudioTrack.release();
keepSessionIdAudioTrack = null;
}
audioBecomingNoisyManager.setEnabled(false);
if (streamVolumeManager != null) {
streamVolumeManager.release();
}
wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
audioFocusManager.release();
if (!internalPlayer.release()) {
// One of the renderers timed out releasing its resources.
listeners.sendEvent(
Player.EVENT_PLAYER_ERROR,
listener ->
listener.onPlayerError(
ExoPlaybackException.createForUnexpected(
new ExoTimeoutException(ExoTimeoutException.TIMEOUT_OPERATION_RELEASE),
PlaybackException.ERROR_CODE_TIMEOUT)));
}
listeners.release();
playbackInfoUpdateHandler.removeCallbacksAndMessages(null);
bandwidthMeter.removeEventListener(analyticsCollector);
if (playbackInfo.sleepingForOffload) {
playbackInfo = playbackInfo.copyWithEstimatedPosition();
}
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
playbackInfo.totalBufferedDurationUs = 0;
analyticsCollector.release();
trackSelector.release();
removeSurfaceCallbacks();
if (ownedSurface != null) {
ownedSurface.release();
ownedSurface = null;
}
if (isPriorityTaskManagerRegistered) {
checkNotNull(priorityTaskManager).remove(priority);
isPriorityTaskManagerRegistered = false;
}
currentCueGroup = CueGroup.EMPTY_TIME_ZERO;
playerReleased = true;
}
@Override
public PlayerMessage createMessage(Target target) {
verifyApplicationThread();
return createMessageInternal(target);
}
@Override
public int getCurrentPeriodIndex() {
verifyApplicationThread();
if (playbackInfo.timeline.isEmpty()) {
return maskingPeriodIndex;
} else {
return playbackInfo.timeline.getIndexOfPeriod(playbackInfo.periodId.periodUid);
}
}
@Override
public int getCurrentMediaItemIndex() {
verifyApplicationThread();
int currentWindowIndex = getCurrentWindowIndexInternal(playbackInfo);
return currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex;
}
@Override
public long getDuration() {
verifyApplicationThread();
if (isPlayingAd()) {
MediaPeriodId periodId = playbackInfo.periodId;
playbackInfo.timeline.getPeriodByUid(periodId.periodUid, period);
long adDurationUs = period.getAdDurationUs(periodId.adGroupIndex, periodId.adIndexInAdGroup);
return Util.usToMs(adDurationUs);
}
return getContentDuration();
}
@Override
public long getCurrentPosition() {
verifyApplicationThread();
return Util.usToMs(getCurrentPositionUsInternal(playbackInfo));
}
@Override
public long getBufferedPosition() {
verifyApplicationThread();
if (isPlayingAd()) {
return playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)
? Util.usToMs(playbackInfo.bufferedPositionUs)
: getDuration();
}
return getContentBufferedPosition();
}
@Override
public long getTotalBufferedDuration() {
verifyApplicationThread();
return Util.usToMs(playbackInfo.totalBufferedDurationUs);
}
@Override
public boolean isPlayingAd() {
verifyApplicationThread();
return playbackInfo.periodId.isAd();
}
@Override
public int getCurrentAdGroupIndex() {
verifyApplicationThread();
return isPlayingAd() ? playbackInfo.periodId.adGroupIndex : C.INDEX_UNSET;
}
@Override
public int getCurrentAdIndexInAdGroup() {
verifyApplicationThread();
return isPlayingAd() ? playbackInfo.periodId.adIndexInAdGroup : C.INDEX_UNSET;
}
@Override
public long getContentPosition() {
verifyApplicationThread();
return getContentPositionInternal(playbackInfo);
}
@Override
public long getContentBufferedPosition() {
verifyApplicationThread();
if (playbackInfo.timeline.isEmpty()) {
return maskingWindowPositionMs;
}
if (playbackInfo.loadingMediaPeriodId.windowSequenceNumber
!= playbackInfo.periodId.windowSequenceNumber) {
return playbackInfo.timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
long contentBufferedPositionUs = playbackInfo.bufferedPositionUs;
if (playbackInfo.loadingMediaPeriodId.isAd()) {
Timeline.Period loadingPeriod =
playbackInfo.timeline.getPeriodByUid(playbackInfo.loadingMediaPeriodId.periodUid, period);
contentBufferedPositionUs =
loadingPeriod.getAdGroupTimeUs(playbackInfo.loadingMediaPeriodId.adGroupIndex);
if (contentBufferedPositionUs == C.TIME_END_OF_SOURCE) {
contentBufferedPositionUs = loadingPeriod.durationUs;
}
}
return Util.usToMs(
periodPositionUsToWindowPositionUs(
playbackInfo.timeline, playbackInfo.loadingMediaPeriodId, contentBufferedPositionUs));
}
@Override
public int getRendererCount() {
verifyApplicationThread();
return renderers.length;
}
@Override
public @C.TrackType int getRendererType(int index) {
verifyApplicationThread();
return renderers[index].getTrackType();
}
@Override
public Renderer getRenderer(int index) {
verifyApplicationThread();
return renderers[index];
}
@Override
public TrackSelector getTrackSelector() {
verifyApplicationThread();
return trackSelector;
}
@Override
public TrackGroupArray getCurrentTrackGroups() {
verifyApplicationThread();
return playbackInfo.trackGroups;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
verifyApplicationThread();
return new TrackSelectionArray(playbackInfo.trackSelectorResult.selections);
}
@Override
public Tracks getCurrentTracks() {
verifyApplicationThread();
return playbackInfo.trackSelectorResult.tracks;
}
@Override
public TrackSelectionParameters getTrackSelectionParameters() {
verifyApplicationThread();
return trackSelector.getParameters();
}
@Override
public void setTrackSelectionParameters(TrackSelectionParameters parameters) {
verifyApplicationThread();
if (!trackSelector.isSetParametersSupported()
|| parameters.equals(trackSelector.getParameters())) {
return;
}
trackSelector.setParameters(parameters);
listeners.sendEvent(
EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
listener -> listener.onTrackSelectionParametersChanged(parameters));
}
@Override
public MediaMetadata getMediaMetadata() {
verifyApplicationThread();
return mediaMetadata;
}
@Override
public MediaMetadata getPlaylistMetadata() {
verifyApplicationThread();
return playlistMetadata;
}
@Override
public void setPlaylistMetadata(MediaMetadata playlistMetadata) {
verifyApplicationThread();
checkNotNull(playlistMetadata);
if (playlistMetadata.equals(this.playlistMetadata)) {
return;
}
this.playlistMetadata = playlistMetadata;
listeners.sendEvent(
EVENT_PLAYLIST_METADATA_CHANGED,
listener -> listener.onPlaylistMetadataChanged(this.playlistMetadata));
}
@Override
public Timeline getCurrentTimeline() {
verifyApplicationThread();
return playbackInfo.timeline;
}
@Override
public void setVideoEffects(List<Effect> videoEffects) {
verifyApplicationThread();
try {
// LINT.IfChange(set_video_effects)
Class.forName("androidx.media3.effect.PreviewingSingleInputVideoGraph$Factory")
.getConstructor(VideoFrameProcessor.Factory.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
throw new IllegalStateException("Could not find required lib-effect dependencies.", e);
}
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_VIDEO_EFFECTS, videoEffects);
}
@Override
public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
verifyApplicationThread();
this.videoScalingMode = videoScalingMode;
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_SCALING_MODE, videoScalingMode);
}
@Override
public @C.VideoScalingMode int getVideoScalingMode() {
verifyApplicationThread();
return videoScalingMode;
}
@Override
public void setVideoChangeFrameRateStrategy(
@C.VideoChangeFrameRateStrategy int videoChangeFrameRateStrategy) {
verifyApplicationThread();
if (this.videoChangeFrameRateStrategy == videoChangeFrameRateStrategy) {
return;
}
this.videoChangeFrameRateStrategy = videoChangeFrameRateStrategy;
sendRendererMessage(
TRACK_TYPE_VIDEO, MSG_SET_CHANGE_FRAME_RATE_STRATEGY, videoChangeFrameRateStrategy);
}
@Override
public @C.VideoChangeFrameRateStrategy int getVideoChangeFrameRateStrategy() {
verifyApplicationThread();
return videoChangeFrameRateStrategy;
}
@Override
public VideoSize getVideoSize() {
verifyApplicationThread();
return videoSize;
}
@Override
public Size getSurfaceSize() {
verifyApplicationThread();
return surfaceSize;
}
@Override
public void clearVideoSurface() {
verifyApplicationThread();
removeSurfaceCallbacks();
setVideoOutputInternal(/* videoOutput= */ null);
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
}
@Override
public void clearVideoSurface(@Nullable Surface surface) {
verifyApplicationThread();
if (surface != null && surface == videoOutput) {
clearVideoSurface();
}
}
@Override
public void setVideoSurface(@Nullable Surface surface) {
verifyApplicationThread();
removeSurfaceCallbacks();
setVideoOutputInternal(surface);
int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET;
maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize);
}
@Override
public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
verifyApplicationThread();
if (surfaceHolder == null) {
clearVideoSurface();
} else {
removeSurfaceCallbacks();
this.surfaceHolderSurfaceIsVideoOutput = true;
this.surfaceHolder = surfaceHolder;
surfaceHolder.addCallback(componentListener);
Surface surface = surfaceHolder.getSurface();
if (surface != null && surface.isValid()) {
setVideoOutputInternal(surface);
Rect surfaceSize = surfaceHolder.getSurfaceFrame();
maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());
} else {
setVideoOutputInternal(/* videoOutput= */ null);
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
}
}
}
@Override
public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
verifyApplicationThread();
if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) {
clearVideoSurface();
}
}
@Override
public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
verifyApplicationThread();
if (surfaceView instanceof VideoDecoderOutputBufferRenderer) {
removeSurfaceCallbacks();
setVideoOutputInternal(surfaceView);
setNonVideoOutputSurfaceHolderInternal(surfaceView.getHolder());
} else if (surfaceView instanceof SphericalGLSurfaceView) {
removeSurfaceCallbacks();
sphericalGLSurfaceView = (SphericalGLSurfaceView) surfaceView;
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_SPHERICAL_SURFACE_VIEW)
.setPayload(sphericalGLSurfaceView)
.send();
sphericalGLSurfaceView.addVideoSurfaceListener(componentListener);
setVideoOutputInternal(sphericalGLSurfaceView.getVideoSurface());
setNonVideoOutputSurfaceHolderInternal(surfaceView.getHolder());
} else {
setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
}
}
@Override
public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
verifyApplicationThread();
clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder());
}
@Override
public void setVideoTextureView(@Nullable TextureView textureView) {
verifyApplicationThread();
if (textureView == null) {
clearVideoSurface();
} else {
removeSurfaceCallbacks();
this.textureView = textureView;
if (textureView.getSurfaceTextureListener() != null) {
Log.w(TAG, "Replacing existing SurfaceTextureListener.");
}
textureView.setSurfaceTextureListener(componentListener);
@Nullable
SurfaceTexture surfaceTexture =
textureView.isAvailable() ? textureView.getSurfaceTexture() : null;
if (surfaceTexture == null) {
setVideoOutputInternal(/* videoOutput= */ null);
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
} else {
setSurfaceTextureInternal(surfaceTexture);
maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight());
}
}
}
@Override
public void clearVideoTextureView(@Nullable TextureView textureView) {
verifyApplicationThread();
if (textureView != null && textureView == this.textureView) {
clearVideoSurface();
}
}
@Override
public void setAudioAttributes(AudioAttributes newAudioAttributes, boolean handleAudioFocus) {
verifyApplicationThread();
if (playerReleased) {
return;
}
if (!Util.areEqual(this.audioAttributes, newAudioAttributes)) {
this.audioAttributes = newAudioAttributes;
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_ATTRIBUTES, newAudioAttributes);
if (streamVolumeManager != null) {
streamVolumeManager.setStreamType(
Util.getStreamTypeForAudioUsage(newAudioAttributes.usage));
}
// Queue event only and flush after updating playWhenReady in case both events are triggered.
listeners.queueEvent(
EVENT_AUDIO_ATTRIBUTES_CHANGED,
listener -> listener.onAudioAttributesChanged(newAudioAttributes));
}
audioFocusManager.setAudioAttributes(handleAudioFocus ? newAudioAttributes : null);
trackSelector.setAudioAttributes(newAudioAttributes);
boolean playWhenReady = getPlayWhenReady();
@AudioFocusManager.PlayerCommand
int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState());
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
listeners.flushEvents();
}
@Override
public AudioAttributes getAudioAttributes() {
verifyApplicationThread();
return audioAttributes;
}
@Override
public void setAudioSessionId(int audioSessionId) {
verifyApplicationThread();
if (this.audioSessionId == audioSessionId) {
return;
}
if (audioSessionId == C.AUDIO_SESSION_ID_UNSET) {
if (Util.SDK_INT < 21) {
audioSessionId = initializeKeepSessionIdAudioTrack(C.AUDIO_SESSION_ID_UNSET);
} else {
audioSessionId = Util.generateAudioSessionIdV21(applicationContext);
}
} else if (Util.SDK_INT < 21) {
// We need to re-initialize keepSessionIdAudioTrack to make sure the session is kept alive for
// as long as the player is using it.
initializeKeepSessionIdAudioTrack(audioSessionId);
}
this.audioSessionId = audioSessionId;
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
sendRendererMessage(TRACK_TYPE_VIDEO, MSG_SET_AUDIO_SESSION_ID, audioSessionId);
int finalAudioSessionId = audioSessionId;
listeners.sendEvent(
EVENT_AUDIO_SESSION_ID, listener -> listener.onAudioSessionIdChanged(finalAudioSessionId));
}
@Override
public int getAudioSessionId() {
verifyApplicationThread();
return audioSessionId;
}
@Override
public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) {
verifyApplicationThread();
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_AUX_EFFECT_INFO, auxEffectInfo);
}
@Override
public void clearAuxEffectInfo() {
verifyApplicationThread();
setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f));
}
@RequiresApi(23)
@Override
public void setPreferredAudioDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
verifyApplicationThread();
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_PREFERRED_AUDIO_DEVICE, audioDeviceInfo);
}
@Override
public void setVolume(float volume) {
verifyApplicationThread();
volume = Util.constrainValue(volume, /* min= */ 0, /* max= */ 1);
if (this.volume == volume) {
return;
}
this.volume = volume;
sendVolumeToRenderers();
float finalVolume = volume;
listeners.sendEvent(EVENT_VOLUME_CHANGED, listener -> listener.onVolumeChanged(finalVolume));
}
@Override
public float getVolume() {
verifyApplicationThread();
return volume;
}
@Override
public boolean getSkipSilenceEnabled() {
verifyApplicationThread();
return skipSilenceEnabled;
}
@Override
public void setSkipSilenceEnabled(boolean newSkipSilenceEnabled) {
verifyApplicationThread();
if (skipSilenceEnabled == newSkipSilenceEnabled) {
return;
}
skipSilenceEnabled = newSkipSilenceEnabled;
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_SKIP_SILENCE_ENABLED, newSkipSilenceEnabled);
listeners.sendEvent(
EVENT_SKIP_SILENCE_ENABLED_CHANGED,
listener -> listener.onSkipSilenceEnabledChanged(newSkipSilenceEnabled));
}
@Override
public AnalyticsCollector getAnalyticsCollector() {
verifyApplicationThread();
return analyticsCollector;
}
@Override
public void addAnalyticsListener(AnalyticsListener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
analyticsCollector.addListener(checkNotNull(listener));
}
@Override
public void removeAnalyticsListener(AnalyticsListener listener) {
verifyApplicationThread();
analyticsCollector.removeListener(checkNotNull(listener));
}
@Override
public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) {
verifyApplicationThread();
if (playerReleased) {
return;
}
audioBecomingNoisyManager.setEnabled(handleAudioBecomingNoisy);
}
@Override
public void setPriority(@C.Priority int priority) {
verifyApplicationThread();
if (this.priority == priority) {
return;
}
if (isPriorityTaskManagerRegistered) {
PriorityTaskManager priorityTaskManager = checkNotNull(this.priorityTaskManager);
priorityTaskManager.add(priority);
priorityTaskManager.remove(this.priority);
}
this.priority = priority;
sendRendererMessage(MSG_SET_PRIORITY, priority);
}
@Override
public void setPriorityTaskManager(@Nullable PriorityTaskManager priorityTaskManager) {
verifyApplicationThread();
if (Util.areEqual(this.priorityTaskManager, priorityTaskManager)) {
return;
}
if (isPriorityTaskManagerRegistered) {
checkNotNull(this.priorityTaskManager).remove(priority);
}
if (priorityTaskManager != null && isLoading()) {
priorityTaskManager.add(priority);
isPriorityTaskManagerRegistered = true;
} else {
isPriorityTaskManagerRegistered = false;
}
this.priorityTaskManager = priorityTaskManager;
}
@Override
@Nullable
public Format getVideoFormat() {
verifyApplicationThread();
return videoFormat;
}
@Override
@Nullable
public Format getAudioFormat() {
verifyApplicationThread();
return audioFormat;
}
@Override
@Nullable
public DecoderCounters getVideoDecoderCounters() {
verifyApplicationThread();
return videoDecoderCounters;
}
@Override
@Nullable
public DecoderCounters getAudioDecoderCounters() {
verifyApplicationThread();
return audioDecoderCounters;
}
@Override
public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) {
verifyApplicationThread();
videoFrameMetadataListener = listener;
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_VIDEO_FRAME_METADATA_LISTENER)
.setPayload(listener)
.send();
}
@Override
public void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener) {
verifyApplicationThread();
if (videoFrameMetadataListener != listener) {
return;
}
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_VIDEO_FRAME_METADATA_LISTENER)
.setPayload(null)
.send();
}
@Override
public void setCameraMotionListener(CameraMotionListener listener) {
verifyApplicationThread();
cameraMotionListener = listener;
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_CAMERA_MOTION_LISTENER)
.setPayload(listener)
.send();
}
@Override
public void clearCameraMotionListener(CameraMotionListener listener) {
verifyApplicationThread();
if (cameraMotionListener != listener) {
return;
}
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_CAMERA_MOTION_LISTENER)
.setPayload(null)
.send();
}
@Override
public CueGroup getCurrentCues() {
verifyApplicationThread();
return currentCueGroup;
}
@Override
public void addListener(Listener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
listeners.add(checkNotNull(listener));
}
@Override
public void removeListener(Listener listener) {
verifyApplicationThread();
listeners.remove(checkNotNull(listener));
}
@Override
public void setWakeMode(@C.WakeMode int wakeMode) {
verifyApplicationThread();
switch (wakeMode) {
case C.WAKE_MODE_NONE:
wakeLockManager.setEnabled(false);
wifiLockManager.setEnabled(false);
break;
case C.WAKE_MODE_LOCAL:
wakeLockManager.setEnabled(true);
wifiLockManager.setEnabled(false);
break;
case C.WAKE_MODE_NETWORK:
wakeLockManager.setEnabled(true);
wifiLockManager.setEnabled(true);
break;
default:
break;
}
}
@Override
public DeviceInfo getDeviceInfo() {
verifyApplicationThread();
return deviceInfo;
}
@Override
public int getDeviceVolume() {
verifyApplicationThread();
if (streamVolumeManager != null) {
return streamVolumeManager.getVolume();
} else {
return 0;
}
}
@Override
public boolean isDeviceMuted() {
verifyApplicationThread();
if (streamVolumeManager != null) {
return streamVolumeManager.isMuted();
} else {
return false;
}
}
/**
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@Deprecated
@Override
public void setDeviceVolume(int volume) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.setVolume(volume, C.VOLUME_FLAG_SHOW_UI);
}
}
@Override
public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.setVolume(volume, flags);
}
}
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override
public void increaseDeviceVolume() {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.increaseVolume(C.VOLUME_FLAG_SHOW_UI);
}
}
@Override
public void increaseDeviceVolume(@C.VolumeFlags int flags) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.increaseVolume(flags);
}
}
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
*/
@Deprecated
@Override
public void decreaseDeviceVolume() {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.decreaseVolume(C.VOLUME_FLAG_SHOW_UI);
}
}
@Override
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.decreaseVolume(flags);
}
}
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@Deprecated
@Override
public void setDeviceMuted(boolean muted) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.setMuted(muted, C.VOLUME_FLAG_SHOW_UI);
}
}
@Override
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
verifyApplicationThread();
if (streamVolumeManager != null) {
streamVolumeManager.setMuted(muted, flags);
}
}
@Override
public boolean isTunnelingEnabled() {
verifyApplicationThread();
for (@Nullable
RendererConfiguration config : playbackInfo.trackSelectorResult.rendererConfigurations) {
if (config != null && config.tunneling) {
return true;
}
}
return false;
}
@Override
public void setImageOutput(ImageOutput imageOutput) {
verifyApplicationThread();
sendRendererMessage(TRACK_TYPE_IMAGE, MSG_SET_IMAGE_OUTPUT, imageOutput);
}
@SuppressWarnings("deprecation") // Calling deprecated methods.
/* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
if (analyticsCollector instanceof DefaultAnalyticsCollector) {
((DefaultAnalyticsCollector) analyticsCollector)
.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
}
}
/**
* Stops the player.
*
* @param error An optional {@link ExoPlaybackException} to set.
*/
private void stopInternal(@Nullable ExoPlaybackException error) {
PlaybackInfo playbackInfo =
this.playbackInfo.copyWithLoadingMediaPeriodId(this.playbackInfo.periodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
playbackInfo.totalBufferedDurationUs = 0;
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
if (error != null) {
playbackInfo = playbackInfo.copyWithPlaybackError(error);
}
pendingOperationAcks++;
internalPlayer.stop();
updatePlaybackInfo(
playbackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
private int getCurrentWindowIndexInternal(PlaybackInfo playbackInfo) {
if (playbackInfo.timeline.isEmpty()) {
return maskingWindowIndex;
}
return playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period)
.windowIndex;
}
private long getContentPositionInternal(PlaybackInfo playbackInfo) {
if (playbackInfo.periodId.isAd()) {
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
return playbackInfo.requestedContentPositionUs == C.TIME_UNSET
? playbackInfo
.timeline
.getWindow(getCurrentWindowIndexInternal(playbackInfo), window)
.getDefaultPositionMs()
: period.getPositionInWindowMs() + Util.usToMs(playbackInfo.requestedContentPositionUs);
}
return Util.usToMs(getCurrentPositionUsInternal(playbackInfo));
}
private long getCurrentPositionUsInternal(PlaybackInfo playbackInfo) {
if (playbackInfo.timeline.isEmpty()) {
return Util.msToUs(maskingWindowPositionMs);
}
long positionUs =
playbackInfo.sleepingForOffload
? playbackInfo.getEstimatedPositionUs()
: playbackInfo.positionUs;
if (playbackInfo.periodId.isAd()) {
return positionUs;
}
return periodPositionUsToWindowPositionUs(
playbackInfo.timeline, playbackInfo.periodId, positionUs);
}
private List<MediaSource> createMediaSources(List<MediaItem> mediaItems) {
List<MediaSource> mediaSources = new ArrayList<>();
for (int i = 0; i < mediaItems.size(); i++) {
mediaSources.add(mediaSourceFactory.createMediaSource(mediaItems.get(i)));
}
return mediaSources;
}
private void handlePlaybackInfo(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate) {
pendingOperationAcks -= playbackInfoUpdate.operationAcks;
if (playbackInfoUpdate.positionDiscontinuity) {
pendingDiscontinuityReason = playbackInfoUpdate.discontinuityReason;
pendingDiscontinuity = true;
}
if (playbackInfoUpdate.hasPlayWhenReadyChangeReason) {
pendingPlayWhenReadyChangeReason = playbackInfoUpdate.playWhenReadyChangeReason;
}
if (pendingOperationAcks == 0) {
Timeline newTimeline = playbackInfoUpdate.playbackInfo.timeline;
if (!this.playbackInfo.timeline.isEmpty() && newTimeline.isEmpty()) {
// Update the masking variables, which are used when the timeline becomes empty because a
// ConcatenatingMediaSource has been cleared.
maskingWindowIndex = C.INDEX_UNSET;
maskingWindowPositionMs = 0;
maskingPeriodIndex = 0;
}
if (!newTimeline.isEmpty()) {
List<Timeline> timelines = ((PlaylistTimeline) newTimeline).getChildTimelines();
checkState(timelines.size() == mediaSourceHolderSnapshots.size());
for (int i = 0; i < timelines.size(); i++) {
mediaSourceHolderSnapshots.get(i).updateTimeline(timelines.get(i));
}
}
boolean positionDiscontinuity = false;
long discontinuityWindowStartPositionUs = C.TIME_UNSET;
if (pendingDiscontinuity) {
positionDiscontinuity =
!playbackInfoUpdate.playbackInfo.periodId.equals(playbackInfo.periodId)
|| playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs
!= playbackInfo.positionUs;
if (positionDiscontinuity) {
discontinuityWindowStartPositionUs =
newTimeline.isEmpty() || playbackInfoUpdate.playbackInfo.periodId.isAd()
? playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs
: periodPositionUsToWindowPositionUs(
newTimeline,
playbackInfoUpdate.playbackInfo.periodId,
playbackInfoUpdate.playbackInfo.discontinuityStartPositionUs);
}
}
pendingDiscontinuity = false;
updatePlaybackInfo(
playbackInfoUpdate.playbackInfo,
TIMELINE_CHANGE_REASON_SOURCE_UPDATE,
pendingPlayWhenReadyChangeReason,
positionDiscontinuity,
pendingDiscontinuityReason,
discontinuityWindowStartPositionUs,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
}
// Calling deprecated listeners.
@SuppressWarnings("deprecation")
private void updatePlaybackInfo(
PlaybackInfo playbackInfo,
@TimelineChangeReason int timelineChangeReason,
@PlayWhenReadyChangeReason int playWhenReadyChangeReason,
boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason,
long discontinuityWindowStartPositionUs,
int oldMaskingMediaItemIndex,
boolean repeatCurrentMediaItem) {
// Assign playback info immediately such that all getters return the right values, but keep
// snapshot of previous and new state so that listener invocations are triggered correctly.
PlaybackInfo previousPlaybackInfo = this.playbackInfo;
PlaybackInfo newPlaybackInfo = playbackInfo;
this.playbackInfo = playbackInfo;
boolean timelineChanged = !previousPlaybackInfo.timeline.equals(newPlaybackInfo.timeline);
Pair<Boolean, Integer> mediaItemTransitionInfo =
evaluateMediaItemTransitionReason(
newPlaybackInfo,
previousPlaybackInfo,
positionDiscontinuity,
positionDiscontinuityReason,
timelineChanged,
repeatCurrentMediaItem);
boolean mediaItemTransitioned = mediaItemTransitionInfo.first;
int mediaItemTransitionReason = mediaItemTransitionInfo.second;
@Nullable MediaItem mediaItem = null;
if (mediaItemTransitioned) {
if (!newPlaybackInfo.timeline.isEmpty()) {
int windowIndex =
newPlaybackInfo.timeline.getPeriodByUid(newPlaybackInfo.periodId.periodUid, period)
.windowIndex;
mediaItem = newPlaybackInfo.timeline.getWindow(windowIndex, window).mediaItem;
}
staticAndDynamicMediaMetadata = MediaMetadata.EMPTY;
}
if (mediaItemTransitioned
|| !previousPlaybackInfo.staticMetadata.equals(newPlaybackInfo.staticMetadata)) {
staticAndDynamicMediaMetadata =
staticAndDynamicMediaMetadata
.buildUpon()
.populateFromMetadata(newPlaybackInfo.staticMetadata)
.build();
}
MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata();
boolean metadataChanged = !newMediaMetadata.equals(mediaMetadata);
mediaMetadata = newMediaMetadata;
boolean playWhenReadyChanged =
previousPlaybackInfo.playWhenReady != newPlaybackInfo.playWhenReady;
boolean playbackStateChanged =
previousPlaybackInfo.playbackState != newPlaybackInfo.playbackState;
if (playbackStateChanged || playWhenReadyChanged) {
updateWakeAndWifiLock();
}
boolean isLoadingChanged = previousPlaybackInfo.isLoading != newPlaybackInfo.isLoading;
if (isLoadingChanged) {
updatePriorityTaskManagerForIsLoadingChange(newPlaybackInfo.isLoading);
}
if (timelineChanged) {
listeners.queueEvent(
Player.EVENT_TIMELINE_CHANGED,
listener -> listener.onTimelineChanged(newPlaybackInfo.timeline, timelineChangeReason));
}
if (positionDiscontinuity) {
PositionInfo previousPositionInfo =
getPreviousPositionInfo(
positionDiscontinuityReason, previousPlaybackInfo, oldMaskingMediaItemIndex);
PositionInfo positionInfo = getPositionInfo(discontinuityWindowStartPositionUs);
listeners.queueEvent(
Player.EVENT_POSITION_DISCONTINUITY,
listener -> {
listener.onPositionDiscontinuity(positionDiscontinuityReason);
listener.onPositionDiscontinuity(
previousPositionInfo, positionInfo, positionDiscontinuityReason);
});
}
if (mediaItemTransitioned) {
@Nullable final MediaItem finalMediaItem = mediaItem;
listeners.queueEvent(
Player.EVENT_MEDIA_ITEM_TRANSITION,
listener -> listener.onMediaItemTransition(finalMediaItem, mediaItemTransitionReason));
}
if (previousPlaybackInfo.playbackError != newPlaybackInfo.playbackError) {
listeners.queueEvent(
Player.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerErrorChanged(newPlaybackInfo.playbackError));
if (newPlaybackInfo.playbackError != null) {
listeners.queueEvent(
Player.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerError(newPlaybackInfo.playbackError));
}
}
if (previousPlaybackInfo.trackSelectorResult != newPlaybackInfo.trackSelectorResult) {
trackSelector.onSelectionActivated(newPlaybackInfo.trackSelectorResult.info);
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED,
listener -> listener.onTracksChanged(newPlaybackInfo.trackSelectorResult.tracks));
}
if (metadataChanged) {
final MediaMetadata finalMediaMetadata = mediaMetadata;
listeners.queueEvent(
EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(finalMediaMetadata));
}
if (isLoadingChanged) {
listeners.queueEvent(
Player.EVENT_IS_LOADING_CHANGED,
listener -> {
listener.onLoadingChanged(newPlaybackInfo.isLoading);
listener.onIsLoadingChanged(newPlaybackInfo.isLoading);
});
}
if (playbackStateChanged || playWhenReadyChanged) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onPlayerStateChanged(
newPlaybackInfo.playWhenReady, newPlaybackInfo.playbackState));
}
if (playbackStateChanged) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_STATE_CHANGED,
listener -> listener.onPlaybackStateChanged(newPlaybackInfo.playbackState));
}
if (playWhenReadyChanged) {
listeners.queueEvent(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
listener ->
listener.onPlayWhenReadyChanged(
newPlaybackInfo.playWhenReady, playWhenReadyChangeReason));
}
if (previousPlaybackInfo.playbackSuppressionReason
!= newPlaybackInfo.playbackSuppressionReason) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_SUPPRESSION_REASON_CHANGED,
listener ->
listener.onPlaybackSuppressionReasonChanged(
newPlaybackInfo.playbackSuppressionReason));
}
if (previousPlaybackInfo.isPlaying() != newPlaybackInfo.isPlaying()) {
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED,
listener -> listener.onIsPlayingChanged(newPlaybackInfo.isPlaying()));
}
if (!previousPlaybackInfo.playbackParameters.equals(newPlaybackInfo.playbackParameters)) {
listeners.queueEvent(
Player.EVENT_PLAYBACK_PARAMETERS_CHANGED,
listener -> listener.onPlaybackParametersChanged(newPlaybackInfo.playbackParameters));
}
updateAvailableCommands();
listeners.flushEvents();
if (previousPlaybackInfo.sleepingForOffload != newPlaybackInfo.sleepingForOffload) {
for (AudioOffloadListener listener : audioOffloadListeners) {
listener.onSleepingForOffloadChanged(newPlaybackInfo.sleepingForOffload);
}
}
}
private PositionInfo getPreviousPositionInfo(
@DiscontinuityReason int positionDiscontinuityReason,
PlaybackInfo oldPlaybackInfo,
int oldMaskingMediaItemIndex) {
@Nullable Object oldWindowUid = null;
@Nullable Object oldPeriodUid = null;
int oldMediaItemIndex = oldMaskingMediaItemIndex;
int oldPeriodIndex = C.INDEX_UNSET;
@Nullable MediaItem oldMediaItem = null;
Timeline.Period oldPeriod = new Timeline.Period();
if (!oldPlaybackInfo.timeline.isEmpty()) {
oldPeriodUid = oldPlaybackInfo.periodId.periodUid;
oldPlaybackInfo.timeline.getPeriodByUid(oldPeriodUid, oldPeriod);
oldMediaItemIndex = oldPeriod.windowIndex;
oldPeriodIndex = oldPlaybackInfo.timeline.getIndexOfPeriod(oldPeriodUid);
oldWindowUid = oldPlaybackInfo.timeline.getWindow(oldMediaItemIndex, window).uid;
oldMediaItem = window.mediaItem;
}
long oldPositionUs;
long oldContentPositionUs;
if (positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
if (oldPlaybackInfo.periodId.isAd()) {
// The old position is the end of the previous ad.
oldPositionUs =
oldPeriod.getAdDurationUs(
oldPlaybackInfo.periodId.adGroupIndex, oldPlaybackInfo.periodId.adIndexInAdGroup);
// The ad cue point is stored in the old requested content position.
oldContentPositionUs = getRequestedContentPositionUs(oldPlaybackInfo);
} else if (oldPlaybackInfo.periodId.nextAdGroupIndex != C.INDEX_UNSET) {
// The old position is the end of a clipped content before an ad group. Use the exact ad
// cue point as the transition position.
oldPositionUs = getRequestedContentPositionUs(playbackInfo);
oldContentPositionUs = oldPositionUs;
} else {
// The old position is the end of a Timeline period. Use the exact duration.
oldPositionUs = oldPeriod.positionInWindowUs + oldPeriod.durationUs;
oldContentPositionUs = oldPositionUs;
}
} else if (oldPlaybackInfo.periodId.isAd()) {
oldPositionUs = oldPlaybackInfo.positionUs;
oldContentPositionUs = getRequestedContentPositionUs(oldPlaybackInfo);
} else {
oldPositionUs = oldPeriod.positionInWindowUs + oldPlaybackInfo.positionUs;
oldContentPositionUs = oldPositionUs;
}
return new PositionInfo(
oldWindowUid,
oldMediaItemIndex,
oldMediaItem,
oldPeriodUid,
oldPeriodIndex,
Util.usToMs(oldPositionUs),
Util.usToMs(oldContentPositionUs),
oldPlaybackInfo.periodId.adGroupIndex,
oldPlaybackInfo.periodId.adIndexInAdGroup);
}
private PositionInfo getPositionInfo(long discontinuityWindowStartPositionUs) {
@Nullable Object newWindowUid = null;
@Nullable Object newPeriodUid = null;
int newMediaItemIndex = getCurrentMediaItemIndex();
int newPeriodIndex = C.INDEX_UNSET;
@Nullable MediaItem newMediaItem = null;
if (!playbackInfo.timeline.isEmpty()) {
newPeriodUid = playbackInfo.periodId.periodUid;
playbackInfo.timeline.getPeriodByUid(newPeriodUid, period);
newPeriodIndex = playbackInfo.timeline.getIndexOfPeriod(newPeriodUid);
newWindowUid = playbackInfo.timeline.getWindow(newMediaItemIndex, window).uid;
newMediaItem = window.mediaItem;
}
long positionMs = Util.usToMs(discontinuityWindowStartPositionUs);
return new PositionInfo(
newWindowUid,
newMediaItemIndex,
newMediaItem,
newPeriodUid,
newPeriodIndex,
positionMs,
/* contentPositionMs= */ playbackInfo.periodId.isAd()
? Util.usToMs(getRequestedContentPositionUs(playbackInfo))
: positionMs,
playbackInfo.periodId.adGroupIndex,
playbackInfo.periodId.adIndexInAdGroup);
}
private static long getRequestedContentPositionUs(PlaybackInfo playbackInfo) {
Timeline.Window window = new Timeline.Window();
Timeline.Period period = new Timeline.Period();
playbackInfo.timeline.getPeriodByUid(playbackInfo.periodId.periodUid, period);
return playbackInfo.requestedContentPositionUs == C.TIME_UNSET
? playbackInfo.timeline.getWindow(period.windowIndex, window).getDefaultPositionUs()
: period.getPositionInWindowUs() + playbackInfo.requestedContentPositionUs;
}
private Pair<Boolean, Integer> evaluateMediaItemTransitionReason(
PlaybackInfo playbackInfo,
PlaybackInfo oldPlaybackInfo,
boolean positionDiscontinuity,
@DiscontinuityReason int positionDiscontinuityReason,
boolean timelineChanged,
boolean repeatCurrentMediaItem) {
Timeline oldTimeline = oldPlaybackInfo.timeline;
Timeline newTimeline = playbackInfo.timeline;
if (newTimeline.isEmpty() && oldTimeline.isEmpty()) {
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET);
} else if (newTimeline.isEmpty() != oldTimeline.isEmpty()) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED);
}
int oldWindowIndex =
oldTimeline.getPeriodByUid(oldPlaybackInfo.periodId.periodUid, period).windowIndex;
Object oldWindowUid = oldTimeline.getWindow(oldWindowIndex, window).uid;
int newWindowIndex =
newTimeline.getPeriodByUid(playbackInfo.periodId.periodUid, period).windowIndex;
Object newWindowUid = newTimeline.getWindow(newWindowIndex, window).uid;
if (!oldWindowUid.equals(newWindowUid)) {
@Player.MediaItemTransitionReason int transitionReason;
if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_AUTO;
} else if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_SEEK;
} else if (timelineChanged) {
transitionReason = MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED;
} else {
// A change in window uid must be justified by one of the reasons above.
throw new IllegalStateException();
}
return new Pair<>(/* isTransitioning */ true, transitionReason);
} else {
// Only mark changes within the current item as a transition if we are repeating automatically
// or via a seek to next/previous.
if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_AUTO_TRANSITION
&& oldPlaybackInfo.periodId.windowSequenceNumber
< playbackInfo.periodId.windowSequenceNumber) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_REPEAT);
}
if (positionDiscontinuity
&& positionDiscontinuityReason == DISCONTINUITY_REASON_SEEK
&& repeatCurrentMediaItem) {
return new Pair<>(/* isTransitioning */ true, MEDIA_ITEM_TRANSITION_REASON_SEEK);
}
}
return new Pair<>(/* isTransitioning */ false, /* mediaItemTransitionReason */ C.INDEX_UNSET);
}
private void updateAvailableCommands() {
Commands previousAvailableCommands = availableCommands;
availableCommands = Util.getAvailableCommands(wrappingPlayer, permanentAvailableCommands);
if (!availableCommands.equals(previousAvailableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(availableCommands));
}
}
private void setMediaSourcesInternal(
List<MediaSource> mediaSources,
int startWindowIndex,
long startPositionMs,
boolean resetToDefaultPosition) {
int currentWindowIndex = getCurrentWindowIndexInternal(playbackInfo);
long currentPositionMs = getCurrentPosition();
pendingOperationAcks++;
if (!mediaSourceHolderSnapshots.isEmpty()) {
removeMediaSourceHolders(
/* fromIndex= */ 0, /* toIndexExclusive= */ mediaSourceHolderSnapshots.size());
}
List<MediaSourceList.MediaSourceHolder> holders =
addMediaSourceHolders(/* index= */ 0, mediaSources);
Timeline timeline = createMaskingTimeline();
if (!timeline.isEmpty() && startWindowIndex >= timeline.getWindowCount()) {
throw new IllegalSeekPositionException(timeline, startWindowIndex, startPositionMs);
}
// Evaluate the actual start position.
if (resetToDefaultPosition) {
startWindowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
startPositionMs = C.TIME_UNSET;
} else if (startWindowIndex == C.INDEX_UNSET) {
startWindowIndex = currentWindowIndex;
startPositionMs = currentPositionMs;
}
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, startWindowIndex, startPositionMs));
// Mask the playback state.
int maskingPlaybackState = newPlaybackInfo.playbackState;
if (startWindowIndex != C.INDEX_UNSET && newPlaybackInfo.playbackState != STATE_IDLE) {
// Position reset to startWindowIndex (results in pending initial seek).
if (timeline.isEmpty() || startWindowIndex >= timeline.getWindowCount()) {
// Setting an empty timeline or invalid seek transitions to ended.
maskingPlaybackState = STATE_ENDED;
} else {
maskingPlaybackState = STATE_BUFFERING;
}
}
newPlaybackInfo = newPlaybackInfo.copyWithPlaybackState(maskingPlaybackState);
internalPlayer.setMediaSources(
holders, startWindowIndex, Util.msToUs(startPositionMs), shuffleOrder);
boolean positionDiscontinuity =
!playbackInfo.periodId.periodUid.equals(newPlaybackInfo.periodId.periodUid)
&& !playbackInfo.timeline.isEmpty();
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* positionDiscontinuity= */ positionDiscontinuity,
DISCONTINUITY_REASON_REMOVE,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
private List<MediaSourceList.MediaSourceHolder> addMediaSourceHolders(
int index, List<MediaSource> mediaSources) {
List<MediaSourceList.MediaSourceHolder> holders = new ArrayList<>();
for (int i = 0; i < mediaSources.size(); i++) {
MediaSourceList.MediaSourceHolder holder =
new MediaSourceList.MediaSourceHolder(mediaSources.get(i), useLazyPreparation);
holders.add(holder);
mediaSourceHolderSnapshots.add(
i + index, new MediaSourceHolderSnapshot(holder.uid, holder.mediaSource));
}
shuffleOrder =
shuffleOrder.cloneAndInsert(
/* insertionIndex= */ index, /* insertionCount= */ holders.size());
return holders;
}
private PlaybackInfo addMediaSourcesInternal(
PlaybackInfo playbackInfo, int index, List<MediaSource> mediaSources) {
Timeline oldTimeline = playbackInfo.timeline;
pendingOperationAcks++;
List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
Timeline newTimeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionUsAfterTimelineChanged(
oldTimeline,
newTimeline,
getCurrentWindowIndexInternal(playbackInfo),
getContentPositionInternal(playbackInfo)));
internalPlayer.addMediaSources(index, holders, shuffleOrder);
return newPlaybackInfo;
}
private PlaybackInfo removeMediaItemsInternal(
PlaybackInfo playbackInfo, int fromIndex, int toIndex) {
int currentIndex = getCurrentWindowIndexInternal(playbackInfo);
long contentPositionMs = getContentPositionInternal(playbackInfo);
Timeline oldTimeline = playbackInfo.timeline;
int currentMediaSourceCount = mediaSourceHolderSnapshots.size();
pendingOperationAcks++;
removeMediaSourceHolders(fromIndex, /* toIndexExclusive= */ toIndex);
Timeline newTimeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo =
maskTimelineAndPosition(
playbackInfo,
newTimeline,
getPeriodPositionUsAfterTimelineChanged(
oldTimeline, newTimeline, currentIndex, contentPositionMs));
// Player transitions to STATE_ENDED if the current index is part of the removed tail.
final boolean transitionsToEnded =
newPlaybackInfo.playbackState != STATE_IDLE
&& newPlaybackInfo.playbackState != STATE_ENDED
&& fromIndex < toIndex
&& toIndex == currentMediaSourceCount
&& currentIndex >= newPlaybackInfo.timeline.getWindowCount();
if (transitionsToEnded) {
newPlaybackInfo = newPlaybackInfo.copyWithPlaybackState(STATE_ENDED);
}
internalPlayer.removeMediaSources(fromIndex, toIndex, shuffleOrder);
return newPlaybackInfo;
}
private void removeMediaSourceHolders(int fromIndex, int toIndexExclusive) {
for (int i = toIndexExclusive - 1; i >= fromIndex; i--) {
mediaSourceHolderSnapshots.remove(i);
}
shuffleOrder = shuffleOrder.cloneAndRemove(fromIndex, toIndexExclusive);
}
private Timeline createMaskingTimeline() {
return new PlaylistTimeline(mediaSourceHolderSnapshots, shuffleOrder);
}
private PlaybackInfo maskTimelineAndPosition(
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPositionUs) {
checkArgument(timeline.isEmpty() || periodPositionUs != null);
// Get the old timeline and position before updating playbackInfo.
Timeline oldTimeline = playbackInfo.timeline;
long oldContentPositionMs = getContentPositionInternal(playbackInfo);
// Mask the timeline.
playbackInfo = playbackInfo.copyWithTimeline(timeline);
if (timeline.isEmpty()) {
// Reset periodId and loadingPeriodId.
MediaPeriodId dummyMediaPeriodId = PlaybackInfo.getDummyPeriodForEmptyTimeline();
long positionUs = Util.msToUs(maskingWindowPositionMs);
playbackInfo =
playbackInfo.copyWithNewPosition(
dummyMediaPeriodId,
positionUs,
/* requestedContentPositionUs= */ positionUs,
/* discontinuityStartPositionUs= */ positionUs,
/* totalBufferedDurationUs= */ 0,
TrackGroupArray.EMPTY,
emptyTrackSelectorResult,
/* staticMetadata= */ ImmutableList.of());
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(dummyMediaPeriodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
return playbackInfo;
}
Object oldPeriodUid = playbackInfo.periodId.periodUid;
boolean playingPeriodChanged = !oldPeriodUid.equals(castNonNull(periodPositionUs).first);
MediaPeriodId newPeriodId =
playingPeriodChanged ? new MediaPeriodId(periodPositionUs.first) : playbackInfo.periodId;
long newContentPositionUs = periodPositionUs.second;
long oldContentPositionUs = Util.msToUs(oldContentPositionMs);
if (!oldTimeline.isEmpty()) {
oldContentPositionUs -=
oldTimeline.getPeriodByUid(oldPeriodUid, period).getPositionInWindowUs();
}
if (playingPeriodChanged || newContentPositionUs < oldContentPositionUs) {
checkState(!newPeriodId.isAd());
// The playing period changes or a backwards seek within the playing period occurs.
playbackInfo =
playbackInfo.copyWithNewPosition(
newPeriodId,
/* positionUs= */ newContentPositionUs,
/* requestedContentPositionUs= */ newContentPositionUs,
/* discontinuityStartPositionUs= */ newContentPositionUs,
/* totalBufferedDurationUs= */ 0,
playingPeriodChanged ? TrackGroupArray.EMPTY : playbackInfo.trackGroups,
playingPeriodChanged ? emptyTrackSelectorResult : playbackInfo.trackSelectorResult,
playingPeriodChanged ? ImmutableList.of() : playbackInfo.staticMetadata);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(newPeriodId);
playbackInfo.bufferedPositionUs = newContentPositionUs;
} else if (newContentPositionUs == oldContentPositionUs) {
// Period position remains unchanged.
int loadingPeriodIndex =
timeline.getIndexOfPeriod(playbackInfo.loadingMediaPeriodId.periodUid);
if (loadingPeriodIndex == C.INDEX_UNSET
|| timeline.getPeriod(loadingPeriodIndex, period).windowIndex
!= timeline.getPeriodByUid(newPeriodId.periodUid, period).windowIndex) {
// Discard periods after the playing period, if the loading period is discarded or the
// playing and loading period are not in the same window.
timeline.getPeriodByUid(newPeriodId.periodUid, period);
long maskedBufferedPositionUs =
newPeriodId.isAd()
? period.getAdDurationUs(newPeriodId.adGroupIndex, newPeriodId.adIndexInAdGroup)
: period.durationUs;
playbackInfo =
playbackInfo.copyWithNewPosition(
newPeriodId,
/* positionUs= */ playbackInfo.positionUs,
/* requestedContentPositionUs= */ playbackInfo.positionUs,
playbackInfo.discontinuityStartPositionUs,
/* totalBufferedDurationUs= */ maskedBufferedPositionUs - playbackInfo.positionUs,
playbackInfo.trackGroups,
playbackInfo.trackSelectorResult,
playbackInfo.staticMetadata);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(newPeriodId);
playbackInfo.bufferedPositionUs = maskedBufferedPositionUs;
}
} else {
checkState(!newPeriodId.isAd());
// A forward seek within the playing period (timeline did not change).
long maskedTotalBufferedDurationUs =
max(
0,
playbackInfo.totalBufferedDurationUs - (newContentPositionUs - oldContentPositionUs));
long maskedBufferedPositionUs = playbackInfo.bufferedPositionUs;
if (playbackInfo.loadingMediaPeriodId.equals(playbackInfo.periodId)) {
maskedBufferedPositionUs = newContentPositionUs + maskedTotalBufferedDurationUs;
}
playbackInfo =
playbackInfo.copyWithNewPosition(
newPeriodId,
/* positionUs= */ newContentPositionUs,
/* requestedContentPositionUs= */ newContentPositionUs,
/* discontinuityStartPositionUs= */ newContentPositionUs,
maskedTotalBufferedDurationUs,
playbackInfo.trackGroups,
playbackInfo.trackSelectorResult,
playbackInfo.staticMetadata);
playbackInfo.bufferedPositionUs = maskedBufferedPositionUs;
}
return playbackInfo;
}
@Nullable
private Pair<Object, Long> getPeriodPositionUsAfterTimelineChanged(
Timeline oldTimeline,
Timeline newTimeline,
int currentWindowIndexInternal,
long contentPositionMs) {
if (oldTimeline.isEmpty() || newTimeline.isEmpty()) {
boolean isCleared = !oldTimeline.isEmpty() && newTimeline.isEmpty();
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline,
isCleared ? C.INDEX_UNSET : currentWindowIndexInternal,
isCleared ? C.TIME_UNSET : contentPositionMs);
}
@Nullable
Pair<Object, Long> oldPeriodPositionUs =
oldTimeline.getPeriodPositionUs(
window, period, currentWindowIndexInternal, Util.msToUs(contentPositionMs));
Object periodUid = castNonNull(oldPeriodPositionUs).first;
if (newTimeline.getIndexOfPeriod(periodUid) != C.INDEX_UNSET) {
// The old period position is still available in the new timeline.
return oldPeriodPositionUs;
}
// Period uid not found in new timeline. Try to get subsequent period.
int newWindowIndex =
ExoPlayerImplInternal.resolveSubsequentPeriod(
window, period, repeatMode, shuffleModeEnabled, periodUid, oldTimeline, newTimeline);
if (newWindowIndex != C.INDEX_UNSET) {
// Reset position to the default position of the window of the subsequent period.
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline,
newWindowIndex,
newTimeline.getWindow(newWindowIndex, window).getDefaultPositionMs());
} else {
// No subsequent period found and the new timeline is not empty. Use the default position.
return maskWindowPositionMsOrGetPeriodPositionUs(
newTimeline, /* windowIndex= */ C.INDEX_UNSET, /* windowPositionMs= */ C.TIME_UNSET);
}
}
@Nullable
private Pair<Object, Long> maskWindowPositionMsOrGetPeriodPositionUs(
Timeline timeline, int windowIndex, long windowPositionMs) {
if (timeline.isEmpty()) {
// If empty we store the initial seek in the masking variables.
maskingWindowIndex = windowIndex;
maskingWindowPositionMs = windowPositionMs == C.TIME_UNSET ? 0 : windowPositionMs;
maskingPeriodIndex = 0;
return null;
}
if (windowIndex == C.INDEX_UNSET || windowIndex >= timeline.getWindowCount()) {
// Use default position of timeline if window index still unset or if a previous initial seek
// now turns out to be invalid.
windowIndex = timeline.getFirstWindowIndex(shuffleModeEnabled);
windowPositionMs = timeline.getWindow(windowIndex, window).getDefaultPositionMs();
}
return timeline.getPeriodPositionUs(window, period, windowIndex, Util.msToUs(windowPositionMs));
}
private long periodPositionUsToWindowPositionUs(
Timeline timeline, MediaPeriodId periodId, long positionUs) {
timeline.getPeriodByUid(periodId.periodUid, period);
positionUs += period.getPositionInWindowUs();
return positionUs;
}
private PlayerMessage createMessageInternal(Target target) {
int currentWindowIndex = getCurrentWindowIndexInternal(playbackInfo);
return new PlayerMessage(
internalPlayer,
target,
playbackInfo.timeline,
currentWindowIndex == C.INDEX_UNSET ? 0 : currentWindowIndex,
clock,
internalPlayer.getPlaybackLooper());
}
/**
* Builds a {@link MediaMetadata} from the main sources.
*
* <p>{@link MediaItem} {@link MediaMetadata} is prioritized, with any gaps/missing fields
* populated by metadata from static ({@link TrackGroup} {@link Format}) and dynamic ({@link
* MetadataOutput#onMetadata(Metadata)}) sources.
*/
private MediaMetadata buildUpdatedMediaMetadata() {
Timeline timeline = getCurrentTimeline();
if (timeline.isEmpty()) {
return staticAndDynamicMediaMetadata;
}
MediaItem mediaItem = timeline.getWindow(getCurrentMediaItemIndex(), window).mediaItem;
// MediaItem metadata is prioritized over metadata within the media.
return staticAndDynamicMediaMetadata.buildUpon().populate(mediaItem.mediaMetadata).build();
}
private void removeSurfaceCallbacks() {
if (sphericalGLSurfaceView != null) {
createMessageInternal(frameMetadataListener)
.setType(FrameMetadataListener.MSG_SET_SPHERICAL_SURFACE_VIEW)
.setPayload(null)
.send();
sphericalGLSurfaceView.removeVideoSurfaceListener(componentListener);
sphericalGLSurfaceView = null;
}
if (textureView != null) {
if (textureView.getSurfaceTextureListener() != componentListener) {
Log.w(TAG, "SurfaceTextureListener already unset or replaced.");
} else {
textureView.setSurfaceTextureListener(null);
}
textureView = null;
}
if (surfaceHolder != null) {
surfaceHolder.removeCallback(componentListener);
surfaceHolder = null;
}
}
private void setSurfaceTextureInternal(SurfaceTexture surfaceTexture) {
Surface surface = new Surface(surfaceTexture);
setVideoOutputInternal(surface);
ownedSurface = surface;
}
private void setVideoOutputInternal(@Nullable Object videoOutput) {
// Note: We don't turn this method into a no-op if the output is being replaced with itself so
// as to ensure onRenderedFirstFrame callbacks are still called in this case.
List<PlayerMessage> messages = new ArrayList<>();
for (Renderer renderer : renderers) {
if (renderer.getTrackType() == TRACK_TYPE_VIDEO) {
messages.add(
createMessageInternal(renderer)
.setType(MSG_SET_VIDEO_OUTPUT)
.setPayload(videoOutput)
.send());
}
}
boolean messageDeliveryTimedOut = false;
if (this.videoOutput != null && this.videoOutput != videoOutput) {
// We're replacing an output. Block to ensure that this output will not be accessed by the
// renderers after this method returns.
try {
for (PlayerMessage message : messages) {
message.blockUntilDelivered(detachSurfaceTimeoutMs);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (TimeoutException e) {
messageDeliveryTimedOut = true;
}
if (this.videoOutput == ownedSurface) {
// We're replacing a surface that we are responsible for releasing.
ownedSurface.release();
ownedSurface = null;
}
}
this.videoOutput = videoOutput;
if (messageDeliveryTimedOut) {
stopInternal(
ExoPlaybackException.createForUnexpected(
new ExoTimeoutException(ExoTimeoutException.TIMEOUT_OPERATION_DETACH_SURFACE),
PlaybackException.ERROR_CODE_TIMEOUT));
}
}
/**
* Sets the holder of the surface that will be displayed to the user, but which should
* <em>not</em> be the output for video renderers. This case occurs when video frames need to be
* rendered to an intermediate surface (which is not the one held by the provided holder).
*
* @param nonVideoOutputSurfaceHolder The holder of the surface that will eventually be displayed
* to the user.
*/
private void setNonVideoOutputSurfaceHolderInternal(SurfaceHolder nonVideoOutputSurfaceHolder) {
// Although we won't use the view's surface directly as the video output, still use the holder
// to query the surface size, to be informed in changes to the size via componentListener, and
// for equality checking in clearVideoSurfaceHolder.
surfaceHolderSurfaceIsVideoOutput = false;
surfaceHolder = nonVideoOutputSurfaceHolder;
surfaceHolder.addCallback(componentListener);
Surface surface = surfaceHolder.getSurface();
if (surface != null && surface.isValid()) {
Rect surfaceSize = surfaceHolder.getSurfaceFrame();
maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height());
} else {
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
}
}
private void maybeNotifySurfaceSizeChanged(int width, int height) {
if (width != surfaceSize.getWidth() || height != surfaceSize.getHeight()) {
surfaceSize = new Size(width, height);
listeners.sendEvent(
EVENT_SURFACE_SIZE_CHANGED, listener -> listener.onSurfaceSizeChanged(width, height));
sendRendererMessage(
TRACK_TYPE_VIDEO, MSG_SET_VIDEO_OUTPUT_RESOLUTION, new Size(width, height));
}
}
private void sendVolumeToRenderers() {
float scaledVolume = volume * audioFocusManager.getVolumeMultiplier();
sendRendererMessage(TRACK_TYPE_AUDIO, MSG_SET_VOLUME, scaledVolume);
}
private void updatePlayWhenReady(
boolean playWhenReady,
@AudioFocusManager.PlayerCommand int playerCommand,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY;
@PlaybackSuppressionReason
int playbackSuppressionReason = computePlaybackSuppressionReason(playWhenReady, playerCommand);
if (playbackInfo.playWhenReady == playWhenReady
&& playbackInfo.playbackSuppressionReason == playbackSuppressionReason) {
return;
}
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playWhenReady, playWhenReadyChangeReason, playbackSuppressionReason);
}
private void updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
boolean playWhenReady,
@Player.PlayWhenReadyChangeReason int playWhenReadyChangeReason,
@PlaybackSuppressionReason int playbackSuppressionReason) {
pendingOperationAcks++;
// Position estimation and copy must occur before changing/masking playback state.
PlaybackInfo newPlaybackInfo =
this.playbackInfo.sleepingForOffload
? this.playbackInfo.copyWithEstimatedPosition()
: this.playbackInfo;
newPlaybackInfo =
newPlaybackInfo.copyWithPlayWhenReady(playWhenReady, playbackSuppressionReason);
internalPlayer.setPlayWhenReady(playWhenReady, playbackSuppressionReason);
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
playWhenReadyChangeReason,
/* positionDiscontinuity= */ false,
/* ignored */ DISCONTINUITY_REASON_INTERNAL,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* repeatCurrentMediaItem= */ false);
}
@PlaybackSuppressionReason
private int computePlaybackSuppressionReason(
boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) {
if (playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY) {
return Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS;
}
if (suppressPlaybackOnUnsuitableOutput) {
if (playWhenReady && !hasSupportedAudioOutput()) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
}
if (!playWhenReady
&& playbackInfo.playbackSuppressionReason
== PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
return Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT;
}
}
return Player.PLAYBACK_SUPPRESSION_REASON_NONE;
}
private boolean hasSupportedAudioOutput() {
if (audioManager == null || Util.SDK_INT < 23) {
// The Audio Manager API to determine the list of connected audio devices is available only in
// API >= 23.
return true;
}
return Api23.isSuitableAudioOutputPresentInAudioDeviceInfoList(
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
}
private void updateWakeAndWifiLock() {
@State int playbackState = getPlaybackState();
switch (playbackState) {
case Player.STATE_READY:
case Player.STATE_BUFFERING:
boolean isSleeping = isSleepingForOffload();
wakeLockManager.setStayAwake(getPlayWhenReady() && !isSleeping);
// The wifi lock is not released while sleeping to avoid interrupting downloads.
wifiLockManager.setStayAwake(getPlayWhenReady());
break;
case Player.STATE_ENDED:
case Player.STATE_IDLE:
wakeLockManager.setStayAwake(false);
wifiLockManager.setStayAwake(false);
break;
default:
throw new IllegalStateException();
}
}
private void verifyApplicationThread() {
// The constructor may be executed on a background thread. Wait with accessing the player from
// the app thread until the constructor finished executing.
constructorFinished.blockUninterruptible();
if (Thread.currentThread() != getApplicationLooper().getThread()) {
String message =
Util.formatInvariant(
"Player is accessed on the wrong thread.\n"
+ "Current thread: '%s'\n"
+ "Expected thread: '%s'\n"
+ "See https://developer.android.com/guide/topics/media/issues/"
+ "player-accessed-on-wrong-thread",
Thread.currentThread().getName(), getApplicationLooper().getThread().getName());
if (throwsWhenUsingWrongThread) {
throw new IllegalStateException(message);
}
Log.w(TAG, message, hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());
hasNotifiedFullWrongThreadWarning = true;
}
}
private void sendRendererMessage(int messageType, @Nullable Object payload) {
sendRendererMessage(/* trackType= */ -1, messageType, payload);
}
private void sendRendererMessage(
@C.TrackType int trackType, int messageType, @Nullable Object payload) {
for (Renderer renderer : renderers) {
if (trackType == -1 || renderer.getTrackType() == trackType) {
createMessageInternal(renderer).setType(messageType).setPayload(payload).send();
}
}
}
/**
* Initializes {@link #keepSessionIdAudioTrack} to keep an audio session ID alive. If the audio
* session ID is {@link C#AUDIO_SESSION_ID_UNSET} then a new audio session ID is generated.
*
* <p>Use of this method is only required on API level 21 and earlier.
*
* @param audioSessionId The audio session ID, or {@link C#AUDIO_SESSION_ID_UNSET} to generate a
* new one.
* @return The audio session ID.
*/
private int initializeKeepSessionIdAudioTrack(int audioSessionId) {
if (keepSessionIdAudioTrack != null
&& keepSessionIdAudioTrack.getAudioSessionId() != audioSessionId) {
keepSessionIdAudioTrack.release();
keepSessionIdAudioTrack = null;
}
if (keepSessionIdAudioTrack == null) {
int sampleRate = 4000; // Minimum sample rate supported by the platform.
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
@C.PcmEncoding int encoding = C.ENCODING_PCM_16BIT;
int bufferSize = 2; // Use a two byte buffer, as it is not actually used for playback.
keepSessionIdAudioTrack =
new AudioTrack(
C.STREAM_TYPE_DEFAULT,
sampleRate,
channelConfig,
encoding,
bufferSize,
AudioTrack.MODE_STATIC,
audioSessionId);
}
return keepSessionIdAudioTrack.getAudioSessionId();
}
private void updatePriorityTaskManagerForIsLoadingChange(boolean isLoading) {
if (priorityTaskManager != null) {
if (isLoading && !isPriorityTaskManagerRegistered) {
priorityTaskManager.add(priority);
isPriorityTaskManagerRegistered = true;
} else if (!isLoading && isPriorityTaskManagerRegistered) {
priorityTaskManager.remove(priority);
isPriorityTaskManagerRegistered = false;
}
}
}
private boolean canUpdateMediaSourcesWithMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
if (toIndex - fromIndex != mediaItems.size()) {
// Number of items doesn't match.
return false;
}
for (int i = fromIndex; i < toIndex; i++) {
MediaSource mediaSource = mediaSourceHolderSnapshots.get(i).mediaSource;
if (!mediaSource.canUpdateMediaItem(mediaItems.get(i - fromIndex))) {
return false;
}
}
return true;
}
private void updateMediaSourcesWithMediaItems(
int fromIndex, int toIndex, List<MediaItem> mediaItems) {
pendingOperationAcks++;
internalPlayer.updateMediaSourcesWithMediaItems(fromIndex, toIndex, mediaItems);
for (int i = fromIndex; i < toIndex; i++) {
MediaSourceHolderSnapshot snapshot = mediaSourceHolderSnapshots.get(i);
snapshot.updateTimeline(
new TimelineWithUpdatedMediaItem(snapshot.getTimeline(), mediaItems.get(i - fromIndex)));
}
Timeline newTimeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithTimeline(newTimeline);
updatePlaybackInfo(
newPlaybackInfo,
/* timelineChangeReason= */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* ignored */ false,
/* ignored */ DISCONTINUITY_REASON_REMOVE,
/* ignored */ C.TIME_UNSET,
/* ignored */ C.INDEX_UNSET,
/* ignored */ false);
}
private static DeviceInfo createDeviceInfo(@Nullable StreamVolumeManager streamVolumeManager) {
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_LOCAL)
.setMinVolume(streamVolumeManager != null ? streamVolumeManager.getMinVolume() : 0)
.setMaxVolume(streamVolumeManager != null ? streamVolumeManager.getMaxVolume() : 0)
.build();
}
private static int getPlayWhenReadyChangeReason(boolean playWhenReady, int playerCommand) {
return playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY
? PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS
: PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
}
private static final class MediaSourceHolderSnapshot implements MediaSourceInfoHolder {
private final Object uid;
private final MediaSource mediaSource;
private Timeline timeline;
public MediaSourceHolderSnapshot(Object uid, MaskingMediaSource mediaSource) {
this.uid = uid;
this.mediaSource = mediaSource;
this.timeline = mediaSource.getTimeline();
}
@Override
public Object getUid() {
return uid;
}
@Override
public Timeline getTimeline() {
return timeline;
}
public void updateTimeline(Timeline timeline) {
this.timeline = timeline;
}
}
private final class ComponentListener
implements VideoRendererEventListener,
AudioRendererEventListener,
TextOutput,
MetadataOutput,
SurfaceHolder.Callback,
TextureView.SurfaceTextureListener,
SphericalGLSurfaceView.VideoSurfaceListener,
AudioFocusManager.PlayerControl,
AudioBecomingNoisyManager.EventListener,
StreamVolumeManager.Listener,
AudioOffloadListener {
// VideoRendererEventListener implementation
@Override
public void onVideoEnabled(DecoderCounters counters) {
videoDecoderCounters = counters;
analyticsCollector.onVideoEnabled(counters);
}
@Override
public void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
analyticsCollector.onVideoDecoderInitialized(
decoderName, initializedTimestampMs, initializationDurationMs);
}
@Override
public void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
videoFormat = format;
analyticsCollector.onVideoInputFormatChanged(format, decoderReuseEvaluation);
}
@Override
public void onDroppedFrames(int count, long elapsed) {
analyticsCollector.onDroppedFrames(count, elapsed);
}
@Override
public void onVideoSizeChanged(VideoSize newVideoSize) {
videoSize = newVideoSize;
listeners.sendEvent(
EVENT_VIDEO_SIZE_CHANGED, listener -> listener.onVideoSizeChanged(newVideoSize));
}
@Override
public void onRenderedFirstFrame(Object output, long renderTimeMs) {
analyticsCollector.onRenderedFirstFrame(output, renderTimeMs);
if (videoOutput == output) {
listeners.sendEvent(EVENT_RENDERED_FIRST_FRAME, Listener::onRenderedFirstFrame);
}
}
@Override
public void onVideoDecoderReleased(String decoderName) {
analyticsCollector.onVideoDecoderReleased(decoderName);
}
@Override
public void onVideoDisabled(DecoderCounters counters) {
analyticsCollector.onVideoDisabled(counters);
videoFormat = null;
videoDecoderCounters = null;
}
@Override
public void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) {
analyticsCollector.onVideoFrameProcessingOffset(totalProcessingOffsetUs, frameCount);
}
@Override
public void onVideoCodecError(Exception videoCodecError) {
analyticsCollector.onVideoCodecError(videoCodecError);
}
// AudioRendererEventListener implementation
@Override
public void onAudioEnabled(DecoderCounters counters) {
audioDecoderCounters = counters;
analyticsCollector.onAudioEnabled(counters);
}
@Override
public void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) {
analyticsCollector.onAudioDecoderInitialized(
decoderName, initializedTimestampMs, initializationDurationMs);
}
@Override
public void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
audioFormat = format;
analyticsCollector.onAudioInputFormatChanged(format, decoderReuseEvaluation);
}
@Override
public void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
analyticsCollector.onAudioPositionAdvancing(playoutStartSystemTimeMs);
}
@Override
public void onAudioUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
analyticsCollector.onAudioUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
}
@Override
public void onAudioDecoderReleased(String decoderName) {
analyticsCollector.onAudioDecoderReleased(decoderName);
}
@Override
public void onAudioDisabled(DecoderCounters counters) {
analyticsCollector.onAudioDisabled(counters);
audioFormat = null;
audioDecoderCounters = null;
}
@Override
public void onSkipSilenceEnabledChanged(boolean newSkipSilenceEnabled) {
if (skipSilenceEnabled == newSkipSilenceEnabled) {
return;
}
skipSilenceEnabled = newSkipSilenceEnabled;
listeners.sendEvent(
EVENT_SKIP_SILENCE_ENABLED_CHANGED,
listener -> listener.onSkipSilenceEnabledChanged(newSkipSilenceEnabled));
}
@Override
public void onAudioSinkError(Exception audioSinkError) {
analyticsCollector.onAudioSinkError(audioSinkError);
}
@Override
public void onAudioCodecError(Exception audioCodecError) {
analyticsCollector.onAudioCodecError(audioCodecError);
}
@Override
public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
analyticsCollector.onAudioTrackInitialized(audioTrackConfig);
}
@Override
public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
analyticsCollector.onAudioTrackReleased(audioTrackConfig);
}
// TextOutput implementation
@SuppressWarnings("deprecation") // Intentionally forwarding deprecating callback
@Override
public void onCues(List<Cue> cues) {
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cues));
}
@Override
public void onCues(CueGroup cueGroup) {
currentCueGroup = cueGroup;
listeners.sendEvent(EVENT_CUES, listener -> listener.onCues(cueGroup));
}
// MetadataOutput implementation
@Override
public void onMetadata(Metadata metadata) {
staticAndDynamicMediaMetadata =
staticAndDynamicMediaMetadata.buildUpon().populateFromMetadata(metadata).build();
MediaMetadata newMediaMetadata = buildUpdatedMediaMetadata();
if (!newMediaMetadata.equals(mediaMetadata)) {
mediaMetadata = newMediaMetadata;
listeners.queueEvent(
EVENT_MEDIA_METADATA_CHANGED,
listener -> listener.onMediaMetadataChanged(mediaMetadata));
}
listeners.queueEvent(EVENT_METADATA, listener -> listener.onMetadata(metadata));
listeners.flushEvents();
}
// SurfaceHolder.Callback implementation
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (surfaceHolderSurfaceIsVideoOutput) {
setVideoOutputInternal(holder.getSurface());
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (surfaceHolderSurfaceIsVideoOutput) {
setVideoOutputInternal(/* videoOutput= */ null);
}
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
}
// TextureView.SurfaceTextureListener implementation
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
setSurfaceTextureInternal(surfaceTexture);
maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
maybeNotifySurfaceSizeChanged(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
setVideoOutputInternal(/* videoOutput= */ null);
maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0);
return true;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
// Do nothing.
}
// SphericalGLSurfaceView.VideoSurfaceListener
@Override
public void onVideoSurfaceCreated(Surface surface) {
setVideoOutputInternal(surface);
}
@Override
public void onVideoSurfaceDestroyed(Surface surface) {
setVideoOutputInternal(/* videoOutput= */ null);
}
// AudioFocusManager.PlayerControl implementation
@Override
public void setVolumeMultiplier(float volumeMultiplier) {
sendVolumeToRenderers();
}
@Override
public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) {
boolean playWhenReady = getPlayWhenReady();
updatePlayWhenReady(
playWhenReady, playerCommand, getPlayWhenReadyChangeReason(playWhenReady, playerCommand));
}
// AudioBecomingNoisyManager.EventListener implementation.
@Override
public void onAudioBecomingNoisy() {
updatePlayWhenReady(
/* playWhenReady= */ false,
AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY,
Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_BECOMING_NOISY);
}
// StreamVolumeManager.Listener implementation.
@Override
public void onStreamTypeChanged(@C.StreamType int streamType) {
DeviceInfo newDeviceInfo = createDeviceInfo(streamVolumeManager);
if (!newDeviceInfo.equals(deviceInfo)) {
deviceInfo = newDeviceInfo;
listeners.sendEvent(
EVENT_DEVICE_INFO_CHANGED, listener -> listener.onDeviceInfoChanged(newDeviceInfo));
}
}
@Override
public void onStreamVolumeChanged(int streamVolume, boolean streamMuted) {
listeners.sendEvent(
EVENT_DEVICE_VOLUME_CHANGED,
listener -> listener.onDeviceVolumeChanged(streamVolume, streamMuted));
}
// Player.AudioOffloadListener implementation.
@Override
public void onSleepingForOffloadChanged(boolean sleepingForOffload) {
updateWakeAndWifiLock();
}
}
/** Listeners that are called on the playback thread. */
private static final class FrameMetadataListener
implements VideoFrameMetadataListener, CameraMotionListener, PlayerMessage.Target {
public static final @MessageType int MSG_SET_VIDEO_FRAME_METADATA_LISTENER =
Renderer.MSG_SET_VIDEO_FRAME_METADATA_LISTENER;
public static final @MessageType int MSG_SET_CAMERA_MOTION_LISTENER =
Renderer.MSG_SET_CAMERA_MOTION_LISTENER;
public static final @MessageType int MSG_SET_SPHERICAL_SURFACE_VIEW = Renderer.MSG_CUSTOM_BASE;
@Nullable private VideoFrameMetadataListener videoFrameMetadataListener;
@Nullable private CameraMotionListener cameraMotionListener;
@Nullable private VideoFrameMetadataListener internalVideoFrameMetadataListener;
@Nullable private CameraMotionListener internalCameraMotionListener;
@Override
public void handleMessage(@MessageType int messageType, @Nullable Object message) {
switch (messageType) {
case MSG_SET_VIDEO_FRAME_METADATA_LISTENER:
videoFrameMetadataListener = (VideoFrameMetadataListener) message;
break;
case MSG_SET_CAMERA_MOTION_LISTENER:
cameraMotionListener = (CameraMotionListener) message;
break;
case MSG_SET_SPHERICAL_SURFACE_VIEW:
@Nullable SphericalGLSurfaceView surfaceView = (SphericalGLSurfaceView) message;
if (surfaceView == null) {
internalVideoFrameMetadataListener = null;
internalCameraMotionListener = null;
} else {
internalVideoFrameMetadataListener = surfaceView.getVideoFrameMetadataListener();
internalCameraMotionListener = surfaceView.getCameraMotionListener();
}
break;
case Renderer.MSG_SET_AUDIO_ATTRIBUTES:
case Renderer.MSG_SET_AUDIO_SESSION_ID:
case Renderer.MSG_SET_AUX_EFFECT_INFO:
case Renderer.MSG_SET_CHANGE_FRAME_RATE_STRATEGY:
case Renderer.MSG_SET_SCALING_MODE:
case Renderer.MSG_SET_SKIP_SILENCE_ENABLED:
case Renderer.MSG_SET_VIDEO_OUTPUT:
case Renderer.MSG_SET_VOLUME:
case Renderer.MSG_SET_WAKEUP_LISTENER:
default:
break;
}
}
// VideoFrameMetadataListener
@Override
public void onVideoFrameAboutToBeRendered(
long presentationTimeUs,
long releaseTimeNs,
Format format,
@Nullable MediaFormat mediaFormat) {
if (internalVideoFrameMetadataListener != null) {
internalVideoFrameMetadataListener.onVideoFrameAboutToBeRendered(
presentationTimeUs, releaseTimeNs, format, mediaFormat);
}
if (videoFrameMetadataListener != null) {
videoFrameMetadataListener.onVideoFrameAboutToBeRendered(
presentationTimeUs, releaseTimeNs, format, mediaFormat);
}
}
// CameraMotionListener
@Override
public void onCameraMotion(long timeUs, float[] rotation) {
if (internalCameraMotionListener != null) {
internalCameraMotionListener.onCameraMotion(timeUs, rotation);
}
if (cameraMotionListener != null) {
cameraMotionListener.onCameraMotion(timeUs, rotation);
}
}
@Override
public void onCameraMotionReset() {
if (internalCameraMotionListener != null) {
internalCameraMotionListener.onCameraMotionReset();
}
if (cameraMotionListener != null) {
cameraMotionListener.onCameraMotionReset();
}
}
}
@RequiresApi(31)
private static final class Api31 {
private Api31() {}
@DoNotInline
public static PlayerId registerMediaMetricsListener(
Context context, ExoPlayerImpl player, boolean usePlatformDiagnostics, String playerName) {
@Nullable MediaMetricsListener listener = MediaMetricsListener.create(context);
if (listener == null) {
Log.w(TAG, "MediaMetricsService unavailable.");
return new PlayerId(LogSessionId.LOG_SESSION_ID_NONE, playerName);
}
if (usePlatformDiagnostics) {
player.addAnalyticsListener(listener);
}
return new PlayerId(listener.getLogSessionId(), playerName);
}
}
@RequiresApi(23)
private static final class Api23 {
private Api23() {}
@DoNotInline
public static boolean isSuitableAudioOutputPresentInAudioDeviceInfoList(
Context context, AudioDeviceInfo[] audioDeviceInfos) {
if (!Util.isWear(context)) {
return true;
}
for (AudioDeviceInfo device : audioDeviceInfos) {
if (device.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP
|| device.getType() == AudioDeviceInfo.TYPE_LINE_ANALOG
|| device.getType() == AudioDeviceInfo.TYPE_LINE_DIGITAL
|| device.getType() == AudioDeviceInfo.TYPE_USB_DEVICE
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
|| device.getType() == AudioDeviceInfo.TYPE_WIRED_HEADSET) {
return true;
}
if (Util.SDK_INT >= 26 && device.getType() == AudioDeviceInfo.TYPE_USB_HEADSET) {
return true;
}
if (Util.SDK_INT >= 28 && device.getType() == AudioDeviceInfo.TYPE_HEARING_AID) {
return true;
}
if (Util.SDK_INT >= 31
&& (device.getType() == AudioDeviceInfo.TYPE_BLE_HEADSET
|| device.getType() == AudioDeviceInfo.TYPE_BLE_SPEAKER)) {
return true;
}
if (Util.SDK_INT >= 33 && device.getType() == AudioDeviceInfo.TYPE_BLE_BROADCAST) {
return true;
}
}
return false;
}
@DoNotInline
public static void registerAudioDeviceCallback(
AudioManager audioManager, AudioDeviceCallback audioDeviceCallback, Handler handler) {
audioManager.registerAudioDeviceCallback(audioDeviceCallback, handler);
}
}
/**
* A {@link AudioDeviceCallback} to change playback suppression reason when suitable audio outputs
* are either added in unsuitable output based playback suppression state or removed during an
* ongoing playback.
*/
@RequiresApi(23)
private final class NoSuitableOutputPlaybackSuppressionAudioDeviceCallback
extends AudioDeviceCallback {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
if (hasSupportedAudioOutput()
&& playbackInfo.playbackSuppressionReason
== Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT) {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_NONE);
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
if (!hasSupportedAudioOutput()) {
updatePlaybackInfoForPlayWhenReadyAndSuppressionReasonStates(
playbackInfo.playWhenReady,
PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
Player.PLAYBACK_SUPPRESSION_REASON_UNSUITABLE_AUDIO_OUTPUT);
}
}
}
}