diff options
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java')
-rw-r--r-- | mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java | 1845 |
1 files changed, 1845 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java new file mode 100644 index 0000000000..7b632ed051 --- /dev/null +++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -0,0 +1,1845 @@ +/* + * 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 org.mozilla.thirdparty.com.google.android.exoplayer2; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.media.MediaCodec; +import android.media.PlaybackParams; +import android.os.Handler; +import android.os.Looper; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsCollector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.analytics.AnalyticsListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioAttributes; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AuxEffectInfo; +import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata; +import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.MetadataOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.MediaSource; +import org.mozilla.thirdparty.com.google.android.exoplayer2.source.TrackGroupArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.Cue; +import org.mozilla.thirdparty.com.google.android.exoplayer2.text.TextOutput; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import org.mozilla.thirdparty.com.google.android.exoplayer2.trackselection.TrackSelector; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.BandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Clock; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.PriorityTaskManager; +import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoDecoderOutputBufferRenderer; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoFrameMetadataListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoRendererEventListener; +import org.mozilla.thirdparty.com.google.android.exoplayer2.video.spherical.CameraMotionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * An {@link ExoPlayer} implementation that uses default {@link Renderer} components. Instances can + * be obtained from {@link SimpleExoPlayer.Builder}. + */ +public class SimpleExoPlayer extends BasePlayer + implements ExoPlayer, + Player.AudioComponent, + Player.VideoComponent, + Player.TextComponent, + Player.MetadataComponent { + + /** @deprecated Use {@link org.mozilla.thirdparty.com.google.android.exoplayer2video.VideoListener}. */ + @Deprecated + public interface VideoListener extends org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener {} + + /** + * A builder for {@link SimpleExoPlayer} instances. + * + * <p>See {@link #Builder(Context)} for the list of default values. + */ + public static final class Builder { + + private final Context context; + private final RenderersFactory renderersFactory; + + private Clock clock; + private TrackSelector trackSelector; + private LoadControl loadControl; + private BandwidthMeter bandwidthMeter; + private AnalyticsCollector analyticsCollector; + private Looper looper; + private boolean useLazyPreparation; + private boolean buildCalled; + + /** + * Creates a builder. + * + * <p>Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom + * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link + * DefaultRenderersFactory} from the APK. + * + * <p>The builder uses the following default values: + * + * <ul> + * <li>{@link RenderersFactory}: {@link DefaultRenderersFactory} + * <li>{@link TrackSelector}: {@link DefaultTrackSelector} + * <li>{@link LoadControl}: {@link DefaultLoadControl} + * <li>{@link BandwidthMeter}: {@link DefaultBandwidthMeter#getSingletonInstance(Context)} + * <li>{@link Looper}: The {@link Looper} associated with the current thread, or the {@link + * Looper} of the application's main thread if the current thread doesn't have a {@link + * Looper} + * <li>{@link AnalyticsCollector}: {@link AnalyticsCollector} with {@link Clock#DEFAULT} + * <li>{@code useLazyPreparation}: {@code true} + * <li>{@link Clock}: {@link Clock#DEFAULT} + * </ul> + * + * @param context A {@link Context}. + */ + public Builder(Context context) { + this(context, new DefaultRenderersFactory(context)); + } + + /** + * Creates a builder with a custom {@link RenderersFactory}. + * + * <p>See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + */ + public Builder(Context context, RenderersFactory renderersFactory) { + this( + context, + renderersFactory, + new DefaultTrackSelector(context), + new DefaultLoadControl(), + DefaultBandwidthMeter.getSingletonInstance(context), + Util.getLooper(), + new AnalyticsCollector(Clock.DEFAULT), + /* useLazyPreparation= */ true, + Clock.DEFAULT); + } + + /** + * Creates a builder with the specified custom components. + * + * <p>Note that this constructor is only useful if you try to ensure that ExoPlayer's default + * components can be removed by ProGuard or R8. For most components except renderers, there is + * only a marginal benefit of doing that. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + * @param trackSelector A {@link TrackSelector}. + * @param loadControl A {@link LoadControl}. + * @param bandwidthMeter A {@link BandwidthMeter}. + * @param looper A {@link Looper} that must be used for all calls to the player. + * @param analyticsCollector An {@link AnalyticsCollector}. + * @param useLazyPreparation Whether media sources should be initialized lazily. + * @param clock A {@link Clock}. Should always be {@link Clock#DEFAULT}. + */ + public Builder( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + Looper looper, + AnalyticsCollector analyticsCollector, + boolean useLazyPreparation, + Clock clock) { + this.context = context; + this.renderersFactory = renderersFactory; + this.trackSelector = trackSelector; + this.loadControl = loadControl; + this.bandwidthMeter = bandwidthMeter; + this.looper = looper; + this.analyticsCollector = analyticsCollector; + this.useLazyPreparation = useLazyPreparation; + this.clock = clock; + } + + /** + * Sets the {@link TrackSelector} that will be used by the player. + * + * @param trackSelector A {@link TrackSelector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setTrackSelector(TrackSelector trackSelector) { + Assertions.checkState(!buildCalled); + this.trackSelector = trackSelector; + return this; + } + + /** + * Sets the {@link LoadControl} that will be used by the player. + * + * @param loadControl A {@link LoadControl}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLoadControl(LoadControl loadControl) { + Assertions.checkState(!buildCalled); + this.loadControl = loadControl; + return this; + } + + /** + * Sets the {@link BandwidthMeter} that will be used by the player. + * + * @param bandwidthMeter A {@link BandwidthMeter}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setBandwidthMeter(BandwidthMeter bandwidthMeter) { + Assertions.checkState(!buildCalled); + this.bandwidthMeter = bandwidthMeter; + return this; + } + + /** + * Sets the {@link Looper} that must be used for all calls to the player and that is used to + * call listeners on. + * + * @param looper A {@link Looper}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setLooper(Looper looper) { + Assertions.checkState(!buildCalled); + this.looper = looper; + return this; + } + + /** + * Sets the {@link AnalyticsCollector} that will collect and forward all player events. + * + * @param analyticsCollector An {@link AnalyticsCollector}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setAnalyticsCollector(AnalyticsCollector analyticsCollector) { + Assertions.checkState(!buildCalled); + this.analyticsCollector = analyticsCollector; + return this; + } + + /** + * Sets whether media sources should be initialized lazily. + * + * <p>If false, all initial preparation steps (e.g., manifest loads) happen immediately. If + * true, these initial preparations are triggered only when the player starts buffering the + * media. + * + * @param useLazyPreparation Whether to use lazy preparation. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public Builder setUseLazyPreparation(boolean useLazyPreparation) { + Assertions.checkState(!buildCalled); + this.useLazyPreparation = useLazyPreparation; + return this; + } + + /** + * Sets the {@link Clock} that will be used by the player. Should only be set for testing + * purposes. + * + * @param clock A {@link Clock}. + * @return This builder. + * @throws IllegalStateException If {@link #build()} has already been called. + */ + @VisibleForTesting + public Builder setClock(Clock clock) { + Assertions.checkState(!buildCalled); + this.clock = clock; + return this; + } + + /** + * Builds a {@link SimpleExoPlayer} instance. + * + * @throws IllegalStateException If {@link #build()} has already been called. + */ + public SimpleExoPlayer build() { + Assertions.checkState(!buildCalled); + buildCalled = true; + return new SimpleExoPlayer( + context, + renderersFactory, + trackSelector, + loadControl, + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + } + + private static final String TAG = "SimpleExoPlayer"; + + protected final Renderer[] renderers; + + private final ExoPlayerImpl player; + private final Handler eventHandler; + private final ComponentListener componentListener; + private final CopyOnWriteArraySet<org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener> + videoListeners; + private final CopyOnWriteArraySet<AudioListener> audioListeners; + private final CopyOnWriteArraySet<TextOutput> textOutputs; + private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs; + private final CopyOnWriteArraySet<VideoRendererEventListener> videoDebugListeners; + private final CopyOnWriteArraySet<AudioRendererEventListener> audioDebugListeners; + private final BandwidthMeter bandwidthMeter; + private final AnalyticsCollector analyticsCollector; + + private final AudioBecomingNoisyManager audioBecomingNoisyManager; + private final AudioFocusManager audioFocusManager; + private final WakeLockManager wakeLockManager; + private final WifiLockManager wifiLockManager; + + @Nullable private Format videoFormat; + @Nullable private Format audioFormat; + + @Nullable private VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer; + @Nullable private Surface surface; + private boolean ownsSurface; + private @C.VideoScalingMode int videoScalingMode; + @Nullable private SurfaceHolder surfaceHolder; + @Nullable private TextureView textureView; + private int surfaceWidth; + private int surfaceHeight; + @Nullable private DecoderCounters videoDecoderCounters; + @Nullable private DecoderCounters audioDecoderCounters; + private int audioSessionId; + private AudioAttributes audioAttributes; + private float audioVolume; + @Nullable private MediaSource mediaSource; + private List<Cue> currentCues; + @Nullable private VideoFrameMetadataListener videoFrameMetadataListener; + @Nullable private CameraMotionListener cameraMotionListener; + private boolean hasNotifiedFullWrongThreadWarning; + @Nullable private PriorityTaskManager priorityTaskManager; + private boolean isPriorityTaskManagerRegistered; + private boolean playerReleased; + + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector A factory for creating the {@link AnalyticsCollector} that will + * collect and forward all player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + */ + @SuppressWarnings("deprecation") + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this( + context, + renderersFactory, + trackSelector, + loadControl, + DrmSessionManager.getDummyDrmSessionManager(), + bandwidthMeter, + analyticsCollector, + clock, + looper); + } + + /** + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer}s to be used by the instance. + * @param trackSelector The {@link TrackSelector} that will be used by the instance. + * @param loadControl The {@link LoadControl} that will be used by the instance. + * @param drmSessionManager An optional {@link DrmSessionManager}. May be null if the instance + * will not be used for DRM protected playbacks. + * @param bandwidthMeter The {@link BandwidthMeter} that will be used by the instance. + * @param analyticsCollector The {@link AnalyticsCollector} that will collect and forward all + * player events. + * @param clock The {@link Clock} that will be used by the instance. Should always be {@link + * Clock#DEFAULT}, unless the player is being used from a test. + * @param looper The {@link Looper} which must be used for all calls to the player and which is + * used to call listeners on. + * @deprecated Use {@link #SimpleExoPlayer(Context, RenderersFactory, TrackSelector, LoadControl, + * BandwidthMeter, AnalyticsCollector, Clock, Looper)} instead, and pass the {@link + * DrmSessionManager} to the {@link MediaSource} factories. + */ + @Deprecated + protected SimpleExoPlayer( + Context context, + RenderersFactory renderersFactory, + TrackSelector trackSelector, + LoadControl loadControl, + @Nullable DrmSessionManager<FrameworkMediaCrypto> drmSessionManager, + BandwidthMeter bandwidthMeter, + AnalyticsCollector analyticsCollector, + Clock clock, + Looper looper) { + this.bandwidthMeter = bandwidthMeter; + this.analyticsCollector = analyticsCollector; + componentListener = new ComponentListener(); + videoListeners = new CopyOnWriteArraySet<>(); + audioListeners = new CopyOnWriteArraySet<>(); + textOutputs = new CopyOnWriteArraySet<>(); + metadataOutputs = new CopyOnWriteArraySet<>(); + videoDebugListeners = new CopyOnWriteArraySet<>(); + audioDebugListeners = new CopyOnWriteArraySet<>(); + eventHandler = new Handler(looper); + renderers = + renderersFactory.createRenderers( + eventHandler, + componentListener, + componentListener, + componentListener, + componentListener, + drmSessionManager); + + // Set initial values. + audioVolume = 1; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + audioAttributes = AudioAttributes.DEFAULT; + videoScalingMode = C.VIDEO_SCALING_MODE_DEFAULT; + currentCues = Collections.emptyList(); + + // Build the player and associated objects. + player = + new ExoPlayerImpl(renderers, trackSelector, loadControl, bandwidthMeter, clock, looper); + analyticsCollector.setPlayer(player); + player.addListener(analyticsCollector); + player.addListener(componentListener); + videoDebugListeners.add(analyticsCollector); + videoListeners.add(analyticsCollector); + audioDebugListeners.add(analyticsCollector); + audioListeners.add(analyticsCollector); + addMetadataOutput(analyticsCollector); + bandwidthMeter.addEventListener(eventHandler, analyticsCollector); + if (drmSessionManager instanceof DefaultDrmSessionManager) { + ((DefaultDrmSessionManager) drmSessionManager).addListener(eventHandler, analyticsCollector); + } + audioBecomingNoisyManager = + new AudioBecomingNoisyManager(context, eventHandler, componentListener); + audioFocusManager = new AudioFocusManager(context, eventHandler, componentListener); + wakeLockManager = new WakeLockManager(context); + wifiLockManager = new WifiLockManager(context); + } + + @Override + @Nullable + public AudioComponent getAudioComponent() { + return this; + } + + @Override + @Nullable + public VideoComponent getVideoComponent() { + return this; + } + + @Override + @Nullable + public TextComponent getTextComponent() { + return this; + } + + @Override + @Nullable + public MetadataComponent getMetadataComponent() { + return this; + } + + /** + * Sets the video scaling mode. + * + * <p>Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} + * is enabled and if the output surface is owned by a {@link android.view.SurfaceView}. + * + * @param videoScalingMode The video scaling mode. + */ + @Override + public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) { + verifyApplicationThread(); + this.videoScalingMode = videoScalingMode; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_SCALING_MODE) + .setPayload(videoScalingMode) + .send(); + } + } + } + + @Override + public @C.VideoScalingMode int getVideoScalingMode() { + return videoScalingMode; + } + + @Override + public void clearVideoSurface() { + verifyApplicationThread(); + removeSurfaceCallbacks(); + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } + + @Override + public void clearVideoSurface(@Nullable Surface surface) { + verifyApplicationThread(); + if (surface != null && surface == this.surface) { + clearVideoSurface(); + } + } + + @Override + public void setVideoSurface(@Nullable Surface surface) { + verifyApplicationThread(); + removeSurfaceCallbacks(); + if (surface != null) { + clearVideoDecoderOutputBufferRenderer(); + } + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + int newSurfaceSize = surface == null ? 0 : C.LENGTH_UNSET; + maybeNotifySurfaceSizeChanged(/* width= */ newSurfaceSize, /* height= */ newSurfaceSize); + } + + @Override + public void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + verifyApplicationThread(); + removeSurfaceCallbacks(); + if (surfaceHolder != null) { + clearVideoDecoderOutputBufferRenderer(); + } + this.surfaceHolder = surfaceHolder; + if (surfaceHolder == null) { + setVideoSurfaceInternal(null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + surfaceHolder.addCallback(componentListener); + Surface surface = surfaceHolder.getSurface(); + if (surface != null && surface.isValid()) { + setVideoSurfaceInternal(surface, /* ownsSurface= */ false); + Rect surfaceSize = surfaceHolder.getSurfaceFrame(); + maybeNotifySurfaceSizeChanged(surfaceSize.width(), surfaceSize.height()); + } else { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } + } + } + + @Override + public void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) { + verifyApplicationThread(); + if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { + setVideoSurfaceHolder(null); + } + } + + @Override + public void setVideoSurfaceView(@Nullable SurfaceView surfaceView) { + setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); + } + + @Override + public void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) { + clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); + } + + @Override + public void setVideoTextureView(@Nullable TextureView textureView) { + verifyApplicationThread(); + removeSurfaceCallbacks(); + if (textureView != null) { + clearVideoDecoderOutputBufferRenderer(); + } + this.textureView = textureView; + if (textureView == null) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + if (textureView.getSurfaceTextureListener() != null) { + Log.w(TAG, "Replacing existing SurfaceTextureListener."); + } + textureView.setSurfaceTextureListener(componentListener); + SurfaceTexture surfaceTexture = + textureView.isAvailable() ? textureView.getSurfaceTexture() : null; + if (surfaceTexture == null) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } else { + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(textureView.getWidth(), textureView.getHeight()); + } + } + } + + @Override + public void clearVideoTextureView(@Nullable TextureView textureView) { + verifyApplicationThread(); + if (textureView != null && textureView == this.textureView) { + setVideoTextureView(null); + } + } + + @Override + public void setVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null) { + clearVideoSurface(); + } + setVideoDecoderOutputBufferRendererInternal(videoDecoderOutputBufferRenderer); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer() { + verifyApplicationThread(); + setVideoDecoderOutputBufferRendererInternal(/* videoDecoderOutputBufferRenderer= */ null); + } + + @Override + public void clearVideoDecoderOutputBufferRenderer( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + verifyApplicationThread(); + if (videoDecoderOutputBufferRenderer != null + && videoDecoderOutputBufferRenderer == this.videoDecoderOutputBufferRenderer) { + clearVideoDecoderOutputBufferRenderer(); + } + } + + @Override + public void addAudioListener(AudioListener listener) { + audioListeners.add(listener); + } + + @Override + public void removeAudioListener(AudioListener listener) { + audioListeners.remove(listener); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes) { + setAudioAttributes(audioAttributes, /* handleAudioFocus= */ false); + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + verifyApplicationThread(); + if (playerReleased) { + return; + } + if (!Util.areEqual(this.audioAttributes, audioAttributes)) { + this.audioAttributes = audioAttributes; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUDIO_ATTRIBUTES) + .setPayload(audioAttributes) + .send(); + } + } + for (AudioListener audioListener : audioListeners) { + audioListener.onAudioAttributesChanged(audioAttributes); + } + } + + audioFocusManager.setAudioAttributes(handleAudioFocus ? audioAttributes : null); + boolean playWhenReady = getPlayWhenReady(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); + } + + @Override + public AudioAttributes getAudioAttributes() { + return audioAttributes; + } + + @Override + public int getAudioSessionId() { + return audioSessionId; + } + + @Override + public void setAuxEffectInfo(AuxEffectInfo auxEffectInfo) { + verifyApplicationThread(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_AUX_EFFECT_INFO) + .setPayload(auxEffectInfo) + .send(); + } + } + } + + @Override + public void clearAuxEffectInfo() { + setAuxEffectInfo(new AuxEffectInfo(AuxEffectInfo.NO_AUX_EFFECT_ID, /* sendLevel= */ 0f)); + } + + @Override + public void setVolume(float audioVolume) { + verifyApplicationThread(); + audioVolume = Util.constrainValue(audioVolume, /* min= */ 0, /* max= */ 1); + if (this.audioVolume == audioVolume) { + return; + } + this.audioVolume = audioVolume; + sendVolumeToRenderers(); + for (AudioListener audioListener : audioListeners) { + audioListener.onVolumeChanged(audioVolume); + } + } + + @Override + public float getVolume() { + return audioVolume; + } + + /** + * Sets the stream type for audio playback, used by the underlying audio track. + * + * <p>Setting the stream type during playback may introduce a short gap in audio output as the + * audio track is recreated. A new audio session id will also be generated. + * + * <p>Calling this method overwrites any attributes set previously by calling {@link + * #setAudioAttributes(AudioAttributes)}. + * + * @deprecated Use {@link #setAudioAttributes(AudioAttributes)}. + * @param streamType The stream type for audio playback. + */ + @Deprecated + public void setAudioStreamType(@C.StreamType int streamType) { + @C.AudioUsage int usage = Util.getAudioUsageForStreamType(streamType); + @C.AudioContentType int contentType = Util.getAudioContentTypeForStreamType(streamType); + AudioAttributes audioAttributes = + new AudioAttributes.Builder().setUsage(usage).setContentType(contentType).build(); + setAudioAttributes(audioAttributes); + } + + /** + * Returns the stream type for audio playback. + * + * @deprecated Use {@link #getAudioAttributes()}. + */ + @Deprecated + public @C.StreamType int getAudioStreamType() { + return Util.getStreamTypeForAudioUsage(audioAttributes.usage); + } + + /** Returns the {@link AnalyticsCollector} used for collecting analytics events. */ + public AnalyticsCollector getAnalyticsCollector() { + return analyticsCollector; + } + + /** + * Adds an {@link AnalyticsListener} to receive analytics events. + * + * @param listener The listener to be added. + */ + public void addAnalyticsListener(AnalyticsListener listener) { + verifyApplicationThread(); + analyticsCollector.addListener(listener); + } + + /** + * Removes an {@link AnalyticsListener}. + * + * @param listener The listener to be removed. + */ + public void removeAnalyticsListener(AnalyticsListener listener) { + verifyApplicationThread(); + analyticsCollector.removeListener(listener); + } + + /** + * Sets whether the player should pause automatically when audio is rerouted from a headset to + * device speakers. See the <a + * href="https://developer.android.com/guide/topics/media-apps/volume-and-earphones#becoming-noisy">audio + * becoming noisy</a> documentation for more information. + * + * <p>This feature is not enabled by default. + * + * @param handleAudioBecomingNoisy Whether the player should pause automatically when audio is + * rerouted from a headset to device speakers. + */ + public void setHandleAudioBecomingNoisy(boolean handleAudioBecomingNoisy) { + verifyApplicationThread(); + if (playerReleased) { + return; + } + audioBecomingNoisyManager.setEnabled(handleAudioBecomingNoisy); + } + + /** + * Sets a {@link PriorityTaskManager}, or null to clear a previously set priority task manager. + * + * <p>The priority {@link C#PRIORITY_PLAYBACK} will be set while the player is loading. + * + * @param priorityTaskManager The {@link PriorityTaskManager}, or null to clear a previously set + * priority task manager. + */ + public void setPriorityTaskManager(@Nullable PriorityTaskManager priorityTaskManager) { + verifyApplicationThread(); + if (Util.areEqual(this.priorityTaskManager, priorityTaskManager)) { + return; + } + if (isPriorityTaskManagerRegistered) { + Assertions.checkNotNull(this.priorityTaskManager).remove(C.PRIORITY_PLAYBACK); + } + if (priorityTaskManager != null && isLoading()) { + priorityTaskManager.add(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = true; + } else { + isPriorityTaskManagerRegistered = false; + } + this.priorityTaskManager = priorityTaskManager; + } + + /** + * Sets the {@link PlaybackParams} governing audio playback. + * + * @deprecated Use {@link #setPlaybackParameters(PlaybackParameters)}. + * @param params The {@link PlaybackParams}, or null to clear any previously set parameters. + */ + @Deprecated + @TargetApi(23) + public void setPlaybackParams(@Nullable PlaybackParams params) { + PlaybackParameters playbackParameters; + if (params != null) { + params.allowDefaults(); + playbackParameters = new PlaybackParameters(params.getSpeed(), params.getPitch()); + } else { + playbackParameters = null; + } + setPlaybackParameters(playbackParameters); + } + + /** Returns the video format currently being played, or null if no video is being played. */ + @Nullable + public Format getVideoFormat() { + return videoFormat; + } + + /** Returns the audio format currently being played, or null if no audio is being played. */ + @Nullable + public Format getAudioFormat() { + return audioFormat; + } + + /** Returns {@link DecoderCounters} for video, or null if no video is being played. */ + @Nullable + public DecoderCounters getVideoDecoderCounters() { + return videoDecoderCounters; + } + + /** Returns {@link DecoderCounters} for audio, or null if no audio is being played. */ + @Nullable + public DecoderCounters getAudioDecoderCounters() { + return audioDecoderCounters; + } + + @Override + public void addVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener listener) { + videoListeners.add(listener); + } + + @Override + public void removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener listener) { + videoListeners.remove(listener); + } + + @Override + public void setVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + verifyApplicationThread(); + videoFrameMetadataListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearVideoFrameMetadataListener(VideoFrameMetadataListener listener) { + verifyApplicationThread(); + if (videoFrameMetadataListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_FRAME_METADATA_LISTENER) + .setPayload(null) + .send(); + } + } + } + + @Override + public void setCameraMotionListener(CameraMotionListener listener) { + verifyApplicationThread(); + cameraMotionListener = listener; + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(listener) + .send(); + } + } + } + + @Override + public void clearCameraMotionListener(CameraMotionListener listener) { + verifyApplicationThread(); + if (cameraMotionListener != listener) { + return; + } + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_CAMERA_MOTION) { + player + .createMessage(renderer) + .setType(C.MSG_SET_CAMERA_MOTION_LISTENER) + .setPayload(null) + .send(); + } + } + } + + /** + * Sets a listener to receive video events, removing all existing listeners. + * + * @param listener The listener. + * @deprecated Use {@link #addVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public void setVideoListener(VideoListener listener) { + videoListeners.clear(); + if (listener != null) { + addVideoListener(listener); + } + } + + /** + * Equivalent to {@link #removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. + * + * @param listener The listener to clear. + * @deprecated Use {@link + * #removeVideoListener(org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener)}. + */ + @Deprecated + @SuppressWarnings("deprecation") + public void clearVideoListener(VideoListener listener) { + removeVideoListener(listener); + } + + @Override + public void addTextOutput(TextOutput listener) { + if (!currentCues.isEmpty()) { + listener.onCues(currentCues); + } + textOutputs.add(listener); + } + + @Override + public void removeTextOutput(TextOutput listener) { + textOutputs.remove(listener); + } + + /** + * Sets an output to receive text events, removing all existing outputs. + * + * @param output The output. + * @deprecated Use {@link #addTextOutput(TextOutput)}. + */ + @Deprecated + public void setTextOutput(TextOutput output) { + textOutputs.clear(); + if (output != null) { + addTextOutput(output); + } + } + + /** + * Equivalent to {@link #removeTextOutput(TextOutput)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeTextOutput(TextOutput)}. + */ + @Deprecated + public void clearTextOutput(TextOutput output) { + removeTextOutput(output); + } + + @Override + public void addMetadataOutput(MetadataOutput listener) { + metadataOutputs.add(listener); + } + + @Override + public void removeMetadataOutput(MetadataOutput listener) { + metadataOutputs.remove(listener); + } + + /** + * Sets an output to receive metadata events, removing all existing outputs. + * + * @param output The output. + * @deprecated Use {@link #addMetadataOutput(MetadataOutput)}. + */ + @Deprecated + public void setMetadataOutput(MetadataOutput output) { + metadataOutputs.retainAll(Collections.singleton(analyticsCollector)); + if (output != null) { + addMetadataOutput(output); + } + } + + /** + * Equivalent to {@link #removeMetadataOutput(MetadataOutput)}. + * + * @param output The output to clear. + * @deprecated Use {@link #removeMetadataOutput(MetadataOutput)}. + */ + @Deprecated + public void clearMetadataOutput(MetadataOutput output) { + removeMetadataOutput(output); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + @SuppressWarnings("deprecation") + public void setVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.retainAll(Collections.singleton(analyticsCollector)); + if (listener != null) { + addVideoDebugListener(listener); + } + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + public void addVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.add(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. + */ + @Deprecated + public void removeVideoDebugListener(VideoRendererEventListener listener) { + videoDebugListeners.remove(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + @SuppressWarnings("deprecation") + public void setAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.retainAll(Collections.singleton(analyticsCollector)); + if (listener != null) { + addAudioDebugListener(listener); + } + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} to get more detailed debug + * information. + */ + @Deprecated + public void addAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.add(listener); + } + + /** + * @deprecated Use {@link #addAnalyticsListener(AnalyticsListener)} and {@link + * #removeAnalyticsListener(AnalyticsListener)} to get more detailed debug information. + */ + @Deprecated + public void removeAudioDebugListener(AudioRendererEventListener listener) { + audioDebugListeners.remove(listener); + } + + // ExoPlayer implementation + + @Override + public Looper getPlaybackLooper() { + return player.getPlaybackLooper(); + } + + @Override + public Looper getApplicationLooper() { + return player.getApplicationLooper(); + } + + @Override + public void addListener(Player.EventListener listener) { + verifyApplicationThread(); + player.addListener(listener); + } + + @Override + public void removeListener(Player.EventListener listener) { + verifyApplicationThread(); + player.removeListener(listener); + } + + @Override + @State + public int getPlaybackState() { + verifyApplicationThread(); + return player.getPlaybackState(); + } + + @Override + @PlaybackSuppressionReason + public int getPlaybackSuppressionReason() { + verifyApplicationThread(); + return player.getPlaybackSuppressionReason(); + } + + @Override + @Nullable + public ExoPlaybackException getPlaybackError() { + verifyApplicationThread(); + return player.getPlaybackError(); + } + + @Override + public void retry() { + verifyApplicationThread(); + if (mediaSource != null + && (getPlaybackError() != null || getPlaybackState() == Player.STATE_IDLE)) { + prepare(mediaSource, /* resetPosition= */ false, /* resetState= */ false); + } + } + + @Override + public void prepare(MediaSource mediaSource) { + prepare(mediaSource, /* resetPosition= */ true, /* resetState= */ true); + } + + @Override + public void prepare(MediaSource mediaSource, boolean resetPosition, boolean resetState) { + verifyApplicationThread(); + if (this.mediaSource != null) { + this.mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + } + this.mediaSource = mediaSource; + mediaSource.addEventListener(eventHandler, analyticsCollector); + boolean playWhenReady = getPlayWhenReady(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, Player.STATE_BUFFERING); + updatePlayWhenReady(playWhenReady, playerCommand); + player.prepare(mediaSource, resetPosition, resetState); + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + verifyApplicationThread(); + @AudioFocusManager.PlayerCommand + int playerCommand = audioFocusManager.updateAudioFocus(playWhenReady, getPlaybackState()); + updatePlayWhenReady(playWhenReady, playerCommand); + } + + @Override + public boolean getPlayWhenReady() { + verifyApplicationThread(); + return player.getPlayWhenReady(); + } + + @Override + public @RepeatMode int getRepeatMode() { + verifyApplicationThread(); + return player.getRepeatMode(); + } + + @Override + public void setRepeatMode(@RepeatMode int repeatMode) { + verifyApplicationThread(); + player.setRepeatMode(repeatMode); + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + verifyApplicationThread(); + player.setShuffleModeEnabled(shuffleModeEnabled); + } + + @Override + public boolean getShuffleModeEnabled() { + verifyApplicationThread(); + return player.getShuffleModeEnabled(); + } + + @Override + public boolean isLoading() { + verifyApplicationThread(); + return player.isLoading(); + } + + @Override + public void seekTo(int windowIndex, long positionMs) { + verifyApplicationThread(); + analyticsCollector.notifySeekStarted(); + player.seekTo(windowIndex, positionMs); + } + + @Override + public void setPlaybackParameters(@Nullable PlaybackParameters playbackParameters) { + verifyApplicationThread(); + player.setPlaybackParameters(playbackParameters); + } + + @Override + public PlaybackParameters getPlaybackParameters() { + verifyApplicationThread(); + return player.getPlaybackParameters(); + } + + @Override + public void setSeekParameters(@Nullable SeekParameters seekParameters) { + verifyApplicationThread(); + player.setSeekParameters(seekParameters); + } + + @Override + public SeekParameters getSeekParameters() { + verifyApplicationThread(); + return player.getSeekParameters(); + } + + @Override + public void setForegroundMode(boolean foregroundMode) { + player.setForegroundMode(foregroundMode); + } + + @Override + public void stop(boolean reset) { + verifyApplicationThread(); + audioFocusManager.updateAudioFocus(getPlayWhenReady(), Player.STATE_IDLE); + player.stop(reset); + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + analyticsCollector.resetForNewMediaSource(); + if (reset) { + mediaSource = null; + } + } + currentCues = Collections.emptyList(); + } + + @Override + public void release() { + verifyApplicationThread(); + audioBecomingNoisyManager.setEnabled(false); + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + audioFocusManager.release(); + player.release(); + removeSurfaceCallbacks(); + if (surface != null) { + if (ownsSurface) { + surface.release(); + } + surface = null; + } + if (mediaSource != null) { + mediaSource.removeEventListener(analyticsCollector); + mediaSource = null; + } + if (isPriorityTaskManagerRegistered) { + Assertions.checkNotNull(priorityTaskManager).remove(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = false; + } + bandwidthMeter.removeEventListener(analyticsCollector); + currentCues = Collections.emptyList(); + playerReleased = true; + } + + @Override + public PlayerMessage createMessage(PlayerMessage.Target target) { + verifyApplicationThread(); + return player.createMessage(target); + } + + @Override + public int getRendererCount() { + verifyApplicationThread(); + return player.getRendererCount(); + } + + @Override + public int getRendererType(int index) { + verifyApplicationThread(); + return player.getRendererType(index); + } + + @Override + public TrackGroupArray getCurrentTrackGroups() { + verifyApplicationThread(); + return player.getCurrentTrackGroups(); + } + + @Override + public TrackSelectionArray getCurrentTrackSelections() { + verifyApplicationThread(); + return player.getCurrentTrackSelections(); + } + + @Override + public Timeline getCurrentTimeline() { + verifyApplicationThread(); + return player.getCurrentTimeline(); + } + + @Override + public int getCurrentPeriodIndex() { + verifyApplicationThread(); + return player.getCurrentPeriodIndex(); + } + + @Override + public int getCurrentWindowIndex() { + verifyApplicationThread(); + return player.getCurrentWindowIndex(); + } + + @Override + public long getDuration() { + verifyApplicationThread(); + return player.getDuration(); + } + + @Override + public long getCurrentPosition() { + verifyApplicationThread(); + return player.getCurrentPosition(); + } + + @Override + public long getBufferedPosition() { + verifyApplicationThread(); + return player.getBufferedPosition(); + } + + @Override + public long getTotalBufferedDuration() { + verifyApplicationThread(); + return player.getTotalBufferedDuration(); + } + + @Override + public boolean isPlayingAd() { + verifyApplicationThread(); + return player.isPlayingAd(); + } + + @Override + public int getCurrentAdGroupIndex() { + verifyApplicationThread(); + return player.getCurrentAdGroupIndex(); + } + + @Override + public int getCurrentAdIndexInAdGroup() { + verifyApplicationThread(); + return player.getCurrentAdIndexInAdGroup(); + } + + @Override + public long getContentPosition() { + verifyApplicationThread(); + return player.getContentPosition(); + } + + @Override + public long getContentBufferedPosition() { + verifyApplicationThread(); + return player.getContentBufferedPosition(); + } + + /** + * Sets whether the player should use a {@link android.os.PowerManager.WakeLock} to ensure the + * device stays awake for playback, even when the screen is off. + * + * <p>Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback can occur when the screen is off (e.g. background audio playback). It is not useful if + * the screen will always be on during playback (e.g. foreground video playback). + * + * <p>This feature is not enabled by default. If enabled, a WakeLock is held whenever the player + * is in the {@link #STATE_READY READY} or {@link #STATE_BUFFERING BUFFERING} states with {@code + * playWhenReady = true}. + * + * @param handleWakeLock Whether the player should use a {@link android.os.PowerManager.WakeLock} + * to ensure the device stays awake for playback, even when the screen is off. + * @deprecated Use {@link #setWakeMode(int)} instead. + */ + @Deprecated + public void setHandleWakeLock(boolean handleWakeLock) { + setWakeMode(handleWakeLock ? C.WAKE_MODE_LOCAL : C.WAKE_MODE_NONE); + } + + /** + * Sets how the player should keep the device awake for playback when the screen is off. + * + * <p>Enabling this feature requires the {@link android.Manifest.permission#WAKE_LOCK} permission. + * It should be used together with a foreground {@link android.app.Service} for use cases where + * playback occurs and the screen is off (e.g. background audio playback). It is not useful when + * the screen will be kept on during playback (e.g. foreground video playback). + * + * <p>When enabled, the locks ({@link android.os.PowerManager.WakeLock} / {@link + * android.net.wifi.WifiManager.WifiLock}) will be held whenever the player is in the {@link + * #STATE_READY} or {@link #STATE_BUFFERING} states with {@code playWhenReady = true}. The locks + * held depends on the specified {@link C.WakeMode}. + * + * @param wakeMode The {@link C.WakeMode} option to keep the device awake during playback. + */ + public void setWakeMode(@C.WakeMode int wakeMode) { + 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; + } + } + + // Internal methods. + + private void removeSurfaceCallbacks() { + 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 setVideoSurfaceInternal(@Nullable Surface surface, boolean ownsSurface) { + // Note: We don't turn this method into a no-op if the surface 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() == C.TRACK_TYPE_VIDEO) { + messages.add( + player.createMessage(renderer).setType(C.MSG_SET_SURFACE).setPayload(surface).send()); + } + } + if (this.surface != null && this.surface != surface) { + // We're replacing a surface. Block to ensure that it's not accessed after the method returns. + try { + for (PlayerMessage message : messages) { + message.blockUntilDelivered(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + // If we created the previous surface, we are responsible for releasing it. + if (this.ownsSurface) { + this.surface.release(); + } + } + this.surface = surface; + this.ownsSurface = ownsSurface; + } + + private void setVideoDecoderOutputBufferRendererInternal( + @Nullable VideoDecoderOutputBufferRenderer videoDecoderOutputBufferRenderer) { + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_VIDEO) { + player + .createMessage(renderer) + .setType(C.MSG_SET_VIDEO_DECODER_OUTPUT_BUFFER_RENDERER) + .setPayload(videoDecoderOutputBufferRenderer) + .send(); + } + } + this.videoDecoderOutputBufferRenderer = videoDecoderOutputBufferRenderer; + } + + private void maybeNotifySurfaceSizeChanged(int width, int height) { + if (width != surfaceWidth || height != surfaceHeight) { + surfaceWidth = width; + surfaceHeight = height; + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + videoListener.onSurfaceSizeChanged(width, height); + } + } + } + + private void sendVolumeToRenderers() { + float scaledVolume = audioVolume * audioFocusManager.getVolumeMultiplier(); + for (Renderer renderer : renderers) { + if (renderer.getTrackType() == C.TRACK_TYPE_AUDIO) { + player.createMessage(renderer).setType(C.MSG_SET_VOLUME).setPayload(scaledVolume).send(); + } + } + } + + private void updatePlayWhenReady( + boolean playWhenReady, @AudioFocusManager.PlayerCommand int playerCommand) { + playWhenReady = playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_DO_NOT_PLAY; + @PlaybackSuppressionReason + int playbackSuppressionReason = + playWhenReady && playerCommand != AudioFocusManager.PLAYER_COMMAND_PLAY_WHEN_READY + ? Player.PLAYBACK_SUPPRESSION_REASON_TRANSIENT_AUDIO_FOCUS_LOSS + : Player.PLAYBACK_SUPPRESSION_REASON_NONE; + player.setPlayWhenReady(playWhenReady, playbackSuppressionReason); + } + + private void verifyApplicationThread() { + if (Looper.myLooper() != getApplicationLooper()) { + Log.w( + TAG, + "Player is accessed on the wrong thread. See " + + "https://exoplayer.dev/issues/player-accessed-on-wrong-thread", + hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); + hasNotifiedFullWrongThreadWarning = true; + } + } + + private void updateWakeAndWifiLock() { + @State int playbackState = getPlaybackState(); + switch (playbackState) { + case Player.STATE_READY: + case Player.STATE_BUFFERING: + wakeLockManager.setStayAwake(getPlayWhenReady()); + wifiLockManager.setStayAwake(getPlayWhenReady()); + break; + case Player.STATE_ENDED: + case Player.STATE_IDLE: + wakeLockManager.setStayAwake(false); + wifiLockManager.setStayAwake(false); + break; + default: + throw new IllegalStateException(); + } + } + + private final class ComponentListener + implements VideoRendererEventListener, + AudioRendererEventListener, + TextOutput, + MetadataOutput, + SurfaceHolder.Callback, + TextureView.SurfaceTextureListener, + AudioFocusManager.PlayerControl, + AudioBecomingNoisyManager.EventListener, + Player.EventListener { + + // VideoRendererEventListener implementation + + @Override + public void onVideoEnabled(DecoderCounters counters) { + videoDecoderCounters = counters; + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoEnabled(counters); + } + } + + @Override + public void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); + } + } + + @Override + public void onVideoInputFormatChanged(Format format) { + videoFormat = format; + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoInputFormatChanged(format); + } + } + + @Override + public void onDroppedFrames(int count, long elapsed) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onDroppedFrames(count, elapsed); + } + } + + @Override + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + // Prevent duplicate notification if a listener is both a VideoRendererEventListener and + // a VideoListener, as they have the same method signature. + if (!videoDebugListeners.contains(videoListener)) { + videoListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoSizeChanged( + width, height, unappliedRotationDegrees, pixelWidthHeightRatio); + } + } + + @Override + public void onRenderedFirstFrame(Surface surface) { + if (SimpleExoPlayer.this.surface == surface) { + for (org.mozilla.thirdparty.com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { + videoListener.onRenderedFirstFrame(); + } + } + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onRenderedFirstFrame(surface); + } + } + + @Override + public void onVideoDisabled(DecoderCounters counters) { + for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { + videoDebugListener.onVideoDisabled(counters); + } + videoFormat = null; + videoDecoderCounters = null; + } + + // AudioRendererEventListener implementation + + @Override + public void onAudioEnabled(DecoderCounters counters) { + audioDecoderCounters = counters; + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioEnabled(counters); + } + } + + @Override + public void onAudioSessionId(int sessionId) { + if (audioSessionId == sessionId) { + return; + } + audioSessionId = sessionId; + for (AudioListener audioListener : audioListeners) { + // Prevent duplicate notification if a listener is both a AudioRendererEventListener and + // a AudioListener, as they have the same method signature. + if (!audioDebugListeners.contains(audioListener)) { + audioListener.onAudioSessionId(sessionId); + } + } + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioSessionId(sessionId); + } + } + + @Override + public void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioDecoderInitialized( + decoderName, initializedTimestampMs, initializationDurationMs); + } + } + + @Override + public void onAudioInputFormatChanged(Format format) { + audioFormat = format; + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioInputFormatChanged(format); + } + } + + @Override + public void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioSinkUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs); + } + } + + @Override + public void onAudioDisabled(DecoderCounters counters) { + for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { + audioDebugListener.onAudioDisabled(counters); + } + audioFormat = null; + audioDecoderCounters = null; + audioSessionId = C.AUDIO_SESSION_ID_UNSET; + } + + // TextOutput implementation + + @Override + public void onCues(List<Cue> cues) { + currentCues = cues; + for (TextOutput textOutput : textOutputs) { + textOutput.onCues(cues); + } + } + + // MetadataOutput implementation + + @Override + public void onMetadata(Metadata metadata) { + for (MetadataOutput metadataOutput : metadataOutputs) { + metadataOutput.onMetadata(metadata); + } + } + + // SurfaceHolder.Callback implementation + + @Override + public void surfaceCreated(SurfaceHolder holder) { + setVideoSurfaceInternal(holder.getSurface(), false); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + maybeNotifySurfaceSizeChanged(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ false); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + } + + // TextureView.SurfaceTextureListener implementation + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { + setVideoSurfaceInternal(new Surface(surfaceTexture), /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { + maybeNotifySurfaceSizeChanged(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + setVideoSurfaceInternal(/* surface= */ null, /* ownsSurface= */ true); + maybeNotifySurfaceSizeChanged(/* width= */ 0, /* height= */ 0); + return true; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { + // Do nothing. + } + + // AudioFocusManager.PlayerControl implementation + + @Override + public void setVolumeMultiplier(float volumeMultiplier) { + sendVolumeToRenderers(); + } + + @Override + public void executePlayerCommand(@AudioFocusManager.PlayerCommand int playerCommand) { + updatePlayWhenReady(getPlayWhenReady(), playerCommand); + } + + // AudioBecomingNoisyManager.EventListener implementation. + + @Override + public void onAudioBecomingNoisy() { + setPlayWhenReady(false); + } + + // Player.EventListener implementation. + + @Override + public void onLoadingChanged(boolean isLoading) { + if (priorityTaskManager != null) { + if (isLoading && !isPriorityTaskManagerRegistered) { + priorityTaskManager.add(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = true; + } else if (!isLoading && isPriorityTaskManagerRegistered) { + priorityTaskManager.remove(C.PRIORITY_PLAYBACK); + isPriorityTaskManagerRegistered = false; + } + } + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, @State int playbackState) { + updateWakeAndWifiLock(); + } + } +} |