From c577d9d35191b11b46ec215be4812b96accd06a0 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 18 Jan 2018 06:59:30 -0800 Subject: [PATCH] Let SimpleExoPlayerView/LeanbackPlayerAdapter bind with any Player Also sanitize naming (PlayerView/PlayerControlView). ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=182364487 --- RELEASENOTES.md | 8 + .../exoplayer2/castdemo/MainActivity.java | 21 +- .../exoplayer2/castdemo/PlayerManager.java | 52 +- .../src/main/res/layout/main_activity.xml | 4 +- .../exoplayer2/imademo/MainActivity.java | 4 +- .../exoplayer2/imademo/PlayerManager.java | 8 +- .../ima/src/main/res/layout/main_activity.xml | 2 +- .../exoplayer2/demo/PlayerActivity.java | 28 +- .../src/main/res/layout/player_activity.xml | 2 +- extensions/cast/README.md | 5 +- .../exoplayer2/ext/cast/CastPlayer.java | 10 + .../ext/leanback/LeanbackPlayerAdapter.java | 39 +- .../android/exoplayer2/ExoPlayerImpl.java | 10 + .../com/google/android/exoplayer2/Player.java | 138 +++ .../android/exoplayer2/SimpleExoPlayer.java | 177 +-- .../exoplayer2/video/VideoListener.java | 45 + .../exoplayer2/ui/PlaybackControlView.java | 1078 +--------------- .../exoplayer2/ui/PlayerControlView.java | 1101 +++++++++++++++++ .../android/exoplayer2/ui/PlayerView.java | 1057 ++++++++++++++++ .../exoplayer2/ui/SimpleExoPlayerView.java | 1011 +-------------- .../res/layout/exo_player_control_view.xml | 18 + .../src/main/res/layout/exo_player_view.xml | 18 + library/ui/src/main/res/values/attrs.xml | 6 +- .../exoplayer2/testutil/StubExoPlayer.java | 10 + 24 files changed, 2575 insertions(+), 2277 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java create mode 100644 library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java create mode 100644 library/ui/src/main/res/layout/exo_player_control_view.xml create mode 100644 library/ui/src/main/res/layout/exo_player_view.xml diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed6dedd0c3..1a54a44058 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -6,6 +6,10 @@ `SimpleExoPlayerView` is configured to use `TextureView` ([#91](https://github.com/google/ExoPlayer/issues/91)). * Player interface: + * Add `Player.VideoComponent`, `Player.TextComponent` and + `Player.MetadataComponent` interfaces that define optional video, text and + metadata output functionality. New `getVideoComponent`, `getTextComponent` + and `getMetadataComponent` methods provide access to this functionality. * Add optional parameter to `stop` to reset the player when stopping. * Add a reason to `EventListener.onTimelineChanged` to distinguish between initial preparation, reset and dynamic updates. @@ -17,6 +21,10 @@ more customization of the message. Now supports setting a message delivery playback position and/or a delivery handler. ([#2189](https://github.com/google/ExoPlayer/issues/2189)). +* UI components: + * Generalized player and control views to allow them to bind with any + `Player`, and renamed them to `PlayerView` and `PlayerControlView` + respectively. * Buffering: * Allow a back-buffer of media to be retained behind the current playback position, for fast backward seeking. The back-buffer can be configured by diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index d34888352f..07781c091e 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -39,8 +39,8 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.castdemo.DemoUtil.Sample; import com.google.android.exoplayer2.ext.cast.CastPlayer; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.gms.cast.framework.CastButtonFactory; import com.google.android.gms.cast.framework.CastContext; @@ -50,8 +50,8 @@ import com.google.android.gms.cast.framework.CastContext; public class MainActivity extends AppCompatActivity implements OnClickListener, PlayerManager.QueuePositionListener { - private SimpleExoPlayerView simpleExoPlayerView; - private PlaybackControlView castControlView; + private PlayerView localPlayerView; + private PlayerControlView castControlView; private PlayerManager playerManager; private MediaQueueAdapter listAdapter; private CastContext castContext; @@ -66,8 +66,8 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, setContentView(R.layout.main_activity); - simpleExoPlayerView = findViewById(R.id.player_view); - simpleExoPlayerView.requestFocus(); + localPlayerView = findViewById(R.id.local_player_view); + localPlayerView.requestFocus(); castControlView = findViewById(R.id.cast_control_view); @@ -93,8 +93,13 @@ public class MainActivity extends AppCompatActivity implements OnClickListener, @Override public void onResume() { super.onResume(); - playerManager = PlayerManager.createPlayerManager(this, simpleExoPlayerView, castControlView, - this, castContext); + playerManager = + PlayerManager.createPlayerManager( + /* queuePositionListener= */ this, + localPlayerView, + castControlView, + /* context= */ this, + castContext); } @Override diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 548482f61f..ac488ff3fd 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -40,8 +40,8 @@ import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.PlaybackControlView; -import com.google.android.exoplayer2.ui.SimpleExoPlayerView; +import com.google.android.exoplayer2.ui.PlayerControlView; +import com.google.android.exoplayer2.ui.PlayerView; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.gms.cast.MediaInfo; @@ -73,12 +73,12 @@ import java.util.ArrayList; private static final DefaultHttpDataSourceFactory DATA_SOURCE_FACTORY = new DefaultHttpDataSourceFactory(USER_AGENT, BANDWIDTH_METER); - private final SimpleExoPlayerView exoPlayerView; - private final PlaybackControlView castControlView; + private final PlayerView localPlayerView; + private final PlayerControlView castControlView; private final SimpleExoPlayer exoPlayer; private final CastPlayer castPlayer; private final ArrayList mediaQueue; - private final QueuePositionListener listener; + private final QueuePositionListener queuePositionListener; private DynamicConcatenatingMediaSource dynamicConcatenatingMediaSource; private boolean castMediaQueueCreationPending; @@ -86,25 +86,33 @@ import java.util.ArrayList; private Player currentPlayer; /** - * @param listener A {@link QueuePositionListener} for queue position changes. - * @param exoPlayerView The {@link SimpleExoPlayerView} for local playback. - * @param castControlView The {@link PlaybackControlView} to control remote playback. + * @param queuePositionListener A {@link QueuePositionListener} for queue position changes. + * @param localPlayerView The {@link PlayerView} for local playback. + * @param castControlView The {@link PlayerControlView} to control remote playback. * @param context A {@link Context}. * @param castContext The {@link CastContext}. */ - public static PlayerManager createPlayerManager(QueuePositionListener listener, - SimpleExoPlayerView exoPlayerView, PlaybackControlView castControlView, Context context, + public static PlayerManager createPlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, CastContext castContext) { - PlayerManager playerManager = new PlayerManager(listener, exoPlayerView, castControlView, - context, castContext); + PlayerManager playerManager = + new PlayerManager( + queuePositionListener, localPlayerView, castControlView, context, castContext); playerManager.init(); return playerManager; } - private PlayerManager(QueuePositionListener listener, SimpleExoPlayerView exoPlayerView, - PlaybackControlView castControlView, Context context, CastContext castContext) { - this.listener = listener; - this.exoPlayerView = exoPlayerView; + private PlayerManager( + QueuePositionListener queuePositionListener, + PlayerView localPlayerView, + PlayerControlView castControlView, + Context context, + CastContext castContext) { + this.queuePositionListener = queuePositionListener; + this.localPlayerView = localPlayerView; this.castControlView = castControlView; mediaQueue = new ArrayList<>(); currentItemIndex = C.INDEX_UNSET; @@ -113,7 +121,7 @@ import java.util.ArrayList; RenderersFactory renderersFactory = new DefaultRenderersFactory(context, null); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector); exoPlayer.addListener(this); - exoPlayerView.setPlayer(exoPlayer); + localPlayerView.setPlayer(exoPlayer); castPlayer = new CastPlayer(castContext); castPlayer.addListener(this); @@ -242,7 +250,7 @@ import java.util.ArrayList; */ public boolean dispatchKeyEvent(KeyEvent event) { if (currentPlayer == exoPlayer) { - return exoPlayerView.dispatchKeyEvent(event); + return localPlayerView.dispatchKeyEvent(event); } else /* currentPlayer == castPlayer */ { return castControlView.dispatchKeyEvent(event); } @@ -256,7 +264,7 @@ import java.util.ArrayList; mediaQueue.clear(); castPlayer.setSessionAvailabilityListener(null); castPlayer.release(); - exoPlayerView.setPlayer(null); + localPlayerView.setPlayer(null); exoPlayer.release(); } @@ -309,10 +317,10 @@ import java.util.ArrayList; // View management. if (currentPlayer == exoPlayer) { - exoPlayerView.setVisibility(View.VISIBLE); + localPlayerView.setVisibility(View.VISIBLE); castControlView.hide(); } else /* currentPlayer == castPlayer */ { - exoPlayerView.setVisibility(View.GONE); + localPlayerView.setVisibility(View.GONE); castControlView.show(); } @@ -380,7 +388,7 @@ import java.util.ArrayList; if (this.currentItemIndex != currentItemIndex) { int oldIndex = this.currentItemIndex; this.currentItemIndex = currentItemIndex; - listener.onQueuePositionChanged(oldIndex, currentItemIndex); + queuePositionListener.onQueuePositionChanged(oldIndex, currentItemIndex); } } diff --git a/demos/cast/src/main/res/layout/main_activity.xml b/demos/cast/src/main/res/layout/main_activity.xml index 1cce287b28..01e48cdea7 100644 --- a/demos/cast/src/main/res/layout/main_activity.xml +++ b/demos/cast/src/main/res/layout/main_activity.xml @@ -19,7 +19,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="true"> - - - - diff --git a/extensions/cast/README.md b/extensions/cast/README.md index 73f7041729..8666690661 100644 --- a/extensions/cast/README.md +++ b/extensions/cast/README.md @@ -27,7 +27,4 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## Create a `CastPlayer` and use it to integrate Cast into your app using -ExoPlayer's common Player interface. You can try the Cast Extension to see how a -[PlaybackControlView][] can be used to control playback in a remote receiver app. - -[PlaybackControlView]: https://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer2/ui/PlaybackControlView.html +ExoPlayer's common `Player` interface. diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 1f39fe0023..e545dfd352 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -280,6 +280,16 @@ public final class CastPlayer implements Player { // Player implementation. + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public void addListener(EventListener listener) { listeners.add(listener); diff --git a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java index c9ed54398e..cbb950093c 100644 --- a/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java +++ b/extensions/leanback/src/main/java/com/google/android/exoplayer2/ext/leanback/LeanbackPlayerAdapter.java @@ -33,13 +33,11 @@ import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; -import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.util.ErrorMessageProvider; +import com.google.android.exoplayer2.video.VideoListener; -/** - * Leanback {@code PlayerAdapter} implementation for {@link SimpleExoPlayer}. - */ +/** Leanback {@code PlayerAdapter} implementation for {@link Player}. */ public final class LeanbackPlayerAdapter extends PlayerAdapter { static { @@ -47,7 +45,7 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } private final Context context; - private final SimpleExoPlayer player; + private final Player player; private final Handler handler; private final ComponentListener componentListener; private final Runnable updateProgressRunnable; @@ -60,14 +58,14 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /** * Builds an instance. Note that the {@code PlayerAdapter} does not manage the lifecycle of the - * {@link SimpleExoPlayer} instance. The caller remains responsible for releasing the player when - * it's no longer required. + * {@link Player} instance. The caller remains responsible for releasing the player when it's no + * longer required. * * @param context The current context (activity). * @param player Instance of your exoplayer that needs to be configured. * @param updatePeriodMs The delay between player control updates, in milliseconds. */ - public LeanbackPlayerAdapter(Context context, SimpleExoPlayer player, final int updatePeriodMs) { + public LeanbackPlayerAdapter(Context context, Player player, final int updatePeriodMs) { this.context = context; this.player = player; handler = new Handler(); @@ -115,13 +113,19 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } notifyStateChanged(); player.addListener(componentListener); - player.addVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.addVideoListener(componentListener); + } } @Override public void onDetachedFromHost() { player.removeListener(componentListener); - player.removeVideoListener(componentListener); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.removeVideoListener(componentListener); + } if (surfaceHolderGlueHost != null) { surfaceHolderGlueHost.setSurfaceHolderCallback(null); surfaceHolderGlueHost = null; @@ -196,7 +200,10 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { /* package */ void setVideoSurface(Surface surface) { hasSurface = surface != null; - player.setVideoSurface(surface); + Player.VideoComponent videoComponent = player.getVideoComponent(); + if (videoComponent != null) { + videoComponent.setVideoSurface(surface); + } maybeNotifyPreparedStateChanged(getCallback()); } @@ -219,8 +226,8 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { } } - private final class ComponentListener extends Player.DefaultEventListener implements - SimpleExoPlayer.VideoListener, SurfaceHolder.Callback { + private final class ComponentListener extends Player.DefaultEventListener + implements SurfaceHolder.Callback, VideoListener { // SurfaceHolder.Callback implementation. @@ -274,11 +281,11 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter { callback.onBufferedPositionChanged(LeanbackPlayerAdapter.this); } - // SimpleExoplayerView.Callback implementation. + // VideoListener implementation. @Override - public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio) { + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, width, height); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b5f6e623eb..83bbdd1157 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -124,6 +124,16 @@ import java.util.concurrent.CopyOnWriteArraySet; internalPlayerHandler = new Handler(internalPlayer.getPlaybackLooper()); } + @Override + public VideoComponent getVideoComponent() { + return null; + } + + @Override + public TextComponent getTextComponent() { + return null; + } + @Override public Looper getPlaybackLooper() { return internalPlayer.getPlaybackLooper(); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 97cd9449d3..443ff8a2ea 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -18,8 +18,14 @@ package com.google.android.exoplayer2; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.Nullable; +import android.view.Surface; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.TextOutput; import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.video.VideoListener; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -44,6 +50,130 @@ import java.lang.annotation.RetentionPolicy; */ public interface Player { + /** The video component of a {@link Player}. */ + interface VideoComponent { + + /** + * Sets the video scaling mode. + * + * @param videoScalingMode The video scaling mode. + */ + void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode); + + /** Returns the video scaling mode. */ + @C.VideoScalingMode + int getVideoScalingMode(); + + /** + * Adds a listener to receive video events. + * + * @param listener The listener to register. + */ + void addVideoListener(VideoListener listener); + + /** + * Removes a listener of video events. + * + * @param listener The listener to unregister. + */ + void removeVideoListener(VideoListener listener); + + /** + * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} + * currently set on the player. + */ + void clearVideoSurface(); + + /** + * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for + * tracking the lifecycle of the surface, and must clear the surface by calling {@code + * setVideoSurface(null)} if the surface is destroyed. + * + *

If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link + * SurfaceHolder} then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, {@link + * #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} rather + * than this method, since passing the holder allows the player to track the lifecycle of the + * surface automatically. + * + * @param surface The {@link Surface}. + */ + void setVideoSurface(Surface surface); + + /** + * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. + * Else does nothing. + * + * @param surface The surface to clear. + */ + void clearVideoSurface(Surface surface); + + /** + * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be + * rendered. The player will track the lifecycle of the surface automatically. + * + * @param surfaceHolder The surface holder. + */ + void setVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being + * rendered if it matches the one passed. Else does nothing. + * + * @param surfaceHolder The surface holder to clear. + */ + void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder); + + /** + * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param surfaceView The surface view. + */ + void setVideoSurfaceView(SurfaceView surfaceView); + + /** + * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param surfaceView The texture view to clear. + */ + void clearVideoSurfaceView(SurfaceView surfaceView); + + /** + * Sets the {@link TextureView} onto which video will be rendered. The player will track the + * lifecycle of the surface automatically. + * + * @param textureView The texture view. + */ + void setVideoTextureView(TextureView textureView); + + /** + * Clears the {@link TextureView} onto which video is being rendered if it matches the one + * passed. Else does nothing. + * + * @param textureView The texture view to clear. + */ + void clearVideoTextureView(TextureView textureView); + } + + /** The text component of a {@link Player}. */ + interface TextComponent { + + /** + * Registers an output to receive text events. + * + * @param listener The output to register. + */ + void addTextOutput(TextOutput listener); + + /** + * Removes a text output. + * + * @param listener The output to remove. + */ + void removeTextOutput(TextOutput listener); + } + /** * Listener of changes in player state. */ @@ -298,6 +428,14 @@ public interface Player { */ int TIMELINE_CHANGE_REASON_DYNAMIC = 2; + /** Returns the component of this player for video output, or null if video is not supported. */ + @Nullable + VideoComponent getVideoComponent(); + + /** Returns the component of this player for text output, or null if text is not supported. */ + @Nullable + TextComponent getTextComponent(); + /** * Register a listener to receive events from the player. The listener's methods will be called on * the thread that was used to construct the player. However, if the thread used to construct the diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ec53e5a964..98ef35d62c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -50,39 +50,11 @@ import java.util.concurrent.CopyOnWriteArraySet; * be obtained from {@link ExoPlayerFactory}. */ @TargetApi(16) -public class SimpleExoPlayer implements ExoPlayer { +public class SimpleExoPlayer implements ExoPlayer, Player.VideoComponent, Player.TextComponent { - /** - * A listener for video rendering information from a {@link SimpleExoPlayer}. - */ - public interface VideoListener { - - /** - * Called each time there's a change in the size of the video being rendered. - * - * @param width The video width in pixels. - * @param height The video height in pixels. - * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise - * rotation in degrees that the application should apply for the video for it to be rendered - * in the correct orientation. This value will always be zero on API levels 21 and above, - * since the renderer will apply all necessary rotations internally. On earlier API levels - * this is not possible. Applications that use {@link android.view.TextureView} can apply - * the rotation by calling {@link android.view.TextureView#setTransform}. Applications that - * do not expect to encounter rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic - * content. - */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); - - /** - * Called when a frame is rendered for the first time since setting the surface, and when a - * frame is rendered for the first time since a video track was selected. - */ - void onRenderedFirstFrame(); - - } + /** @deprecated Use {@link com.google.android.exoplayer2.video.VideoListener}. */ + @Deprecated + public interface VideoListener extends com.google.android.exoplayer2.video.VideoListener {} private static final String TAG = "SimpleExoPlayer"; @@ -90,7 +62,8 @@ public class SimpleExoPlayer implements ExoPlayer { private final ExoPlayer player; private final ComponentListener componentListener; - private final CopyOnWriteArraySet videoListeners; + private final CopyOnWriteArraySet + videoListeners; private final CopyOnWriteArraySet textOutputs; private final CopyOnWriteArraySet metadataOutputs; private final CopyOnWriteArraySet videoDebugListeners; @@ -154,14 +127,25 @@ public class SimpleExoPlayer implements ExoPlayer { player = createExoPlayerImpl(renderers, trackSelector, loadControl, clock); } + @Override + public VideoComponent getVideoComponent() { + return this; + } + + @Override + public TextComponent getTextComponent() { + return this; + } + /** * Sets the video scaling mode. - *

- * 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}. + * + *

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) { this.videoScalingMode = videoScalingMode; for (Renderer renderer : renderers) { @@ -175,57 +159,30 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Returns the video scaling mode. - */ + @Override public @C.VideoScalingMode int getVideoScalingMode() { return videoScalingMode; } - /** - * Clears any {@link Surface}, {@link SurfaceHolder}, {@link SurfaceView} or {@link TextureView} - * currently set on the player. - */ + @Override public void clearVideoSurface() { setVideoSurface(null); } - /** - * Sets the {@link Surface} onto which video will be rendered. The caller is responsible for - * tracking the lifecycle of the surface, and must clear the surface by calling - * {@code setVideoSurface(null)} if the surface is destroyed. - *

- * If the surface is held by a {@link SurfaceView}, {@link TextureView} or {@link SurfaceHolder} - * then it's recommended to use {@link #setVideoSurfaceView(SurfaceView)}, - * {@link #setVideoTextureView(TextureView)} or {@link #setVideoSurfaceHolder(SurfaceHolder)} - * rather than this method, since passing the holder allows the player to track the lifecycle of - * the surface automatically. - * - * @param surface The {@link Surface}. - */ + @Override public void setVideoSurface(Surface surface) { removeSurfaceCallbacks(); setVideoSurfaceInternal(surface, false); } - /** - * Clears the {@link Surface} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surface The surface to clear. - */ + @Override public void clearVideoSurface(Surface surface) { if (surface != null && surface == this.surface) { setVideoSurface(null); } } - /** - * Sets the {@link SurfaceHolder} that holds the {@link Surface} onto which video will be - * rendered. The player will track the lifecycle of the surface automatically. - * - * @param surfaceHolder The surface holder. - */ + @Override public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { removeSurfaceCallbacks(); this.surfaceHolder = surfaceHolder; @@ -238,44 +195,24 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Clears the {@link SurfaceHolder} that holds the {@link Surface} onto which video is being - * rendered if it matches the one passed. Else does nothing. - * - * @param surfaceHolder The surface holder to clear. - */ + @Override public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { if (surfaceHolder != null && surfaceHolder == this.surfaceHolder) { setVideoSurfaceHolder(null); } } - /** - * Sets the {@link SurfaceView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param surfaceView The surface view. - */ + @Override public void setVideoSurfaceView(SurfaceView surfaceView) { setVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Clears the {@link SurfaceView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param surfaceView The texture view to clear. - */ + @Override public void clearVideoSurfaceView(SurfaceView surfaceView) { clearVideoSurfaceHolder(surfaceView == null ? null : surfaceView.getHolder()); } - /** - * Sets the {@link TextureView} onto which video will be rendered. The player will track the - * lifecycle of the surface automatically. - * - * @param textureView The texture view. - */ + @Override public void setVideoTextureView(TextureView textureView) { removeSurfaceCallbacks(); this.textureView = textureView; @@ -292,12 +229,7 @@ public class SimpleExoPlayer implements ExoPlayer { } } - /** - * Clears the {@link TextureView} onto which video is being rendered if it matches the one passed. - * Else does nothing. - * - * @param textureView The texture view to clear. - */ + @Override public void clearVideoTextureView(TextureView textureView) { if (textureView != null && textureView == this.textureView) { setVideoTextureView(null); @@ -446,21 +378,13 @@ public class SimpleExoPlayer implements ExoPlayer { return audioDecoderCounters; } - /** - * Adds a listener to receive video events. - * - * @param listener The listener to register. - */ - public void addVideoListener(VideoListener listener) { + @Override + public void addVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.add(listener); } - /** - * Removes a listener of video events. - * - * @param listener The listener to unregister. - */ - public void removeVideoListener(VideoListener listener) { + @Override + public void removeVideoListener(com.google.android.exoplayer2.video.VideoListener listener) { videoListeners.remove(listener); } @@ -468,7 +392,7 @@ public class SimpleExoPlayer implements ExoPlayer { * Sets a listener to receive video events, removing all existing listeners. * * @param listener The listener. - * @deprecated Use {@link #addVideoListener(VideoListener)}. + * @deprecated Use {@link #addVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void setVideoListener(VideoListener listener) { @@ -479,30 +403,23 @@ public class SimpleExoPlayer implements ExoPlayer { } /** - * Equivalent to {@link #removeVideoListener(VideoListener)}. + * Equivalent to {@link #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. * * @param listener The listener to clear. - * @deprecated Use {@link #removeVideoListener(VideoListener)}. + * @deprecated Use {@link + * #removeVideoListener(com.google.android.exoplayer2.video.VideoListener)}. */ @Deprecated public void clearVideoListener(VideoListener listener) { removeVideoListener(listener); } - /** - * Registers an output to receive text events. - * - * @param listener The output to register. - */ + @Override public void addTextOutput(TextOutput listener) { textOutputs.add(listener); } - /** - * Removes a text output. - * - * @param listener The output to remove. - */ + @Override public void removeTextOutput(TextOutput listener) { textOutputs.remove(listener); } @@ -532,20 +449,10 @@ public class SimpleExoPlayer implements ExoPlayer { removeTextOutput(output); } - /** - * Registers an output to receive metadata events. - * - * @param listener The output to register. - */ public void addMetadataOutput(MetadataOutput listener) { metadataOutputs.add(listener); } - /** - * Removes a metadata output. - * - * @param listener The output to remove. - */ public void removeMetadataOutput(MetadataOutput listener) { metadataOutputs.remove(listener); } @@ -978,7 +885,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onVideoSizeChanged(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); } @@ -991,7 +898,7 @@ public class SimpleExoPlayer implements ExoPlayer { @Override public void onRenderedFirstFrame(Surface surface) { if (SimpleExoPlayer.this.surface == surface) { - for (VideoListener videoListener : videoListeners) { + for (com.google.android.exoplayer2.video.VideoListener videoListener : videoListeners) { videoListener.onRenderedFirstFrame(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java new file mode 100644 index 0000000000..ab09e0bbc2 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoListener.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.video; + +/** A listener for metadata corresponding to video being rendered. */ +public interface VideoListener { + + /** + * Called each time there's a change in the size of the video being rendered. + * + * @param width The video width in pixels. + * @param height The video height in pixels. + * @param unappliedRotationDegrees For videos that require a rotation, this is the clockwise + * rotation in degrees that the application should apply for the video for it to be rendered + * in the correct orientation. This value will always be zero on API levels 21 and above, + * since the renderer will apply all necessary rotations internally. On earlier API levels + * this is not possible. Applications that use {@link android.view.TextureView} can apply the + * rotation by calling {@link android.view.TextureView#setTransform}. Applications that do not + * expect to encounter rotated videos can safely ignore this parameter. + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * content. + */ + void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio); + + /** + * Called when a frame is rendered for the first time since setting the surface, and when a frame + * is rendered for the first time since a video track was selected. + */ + void onRenderedFirstFrame(); +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java index fefbb0797a..da03d28cba 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlaybackControlView.java @@ -15,171 +15,24 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; -import android.os.SystemClock; -import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.FrameLayout; -import android.widget.ImageView; -import android.widget.TextView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.Arrays; -import java.util.Formatter; -import java.util.Locale; -/** - * A view for controlling {@link Player} instances. - * - *

A PlaybackControlView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

Attributes

- * - * The following attributes can be set on a PlaybackControlView when used in a layout XML file: - * - *

- * - *

    - *
  • {@code show_timeout} - The time between the last user interaction and the controls - * being automatically hidden, in milliseconds. Use zero if the controls should not - * automatically timeout. - *
      - *
    • Corresponding method: {@link #setShowTimeoutMs(int)} - *
    • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} - *
    - *
  • {@code rewind_increment} - The duration of the rewind applied when the user taps the - * rewind button, in milliseconds. Use zero to disable the rewind button. - *
      - *
    • Corresponding method: {@link #setRewindIncrementMs(int)} - *
    • Default: {@link #DEFAULT_REWIND_MS} - *
    - *
  • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. - *
      - *
    • Corresponding method: {@link #setFastForwardIncrementMs(int)} - *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS} - *
    - *
  • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat - * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, - * or {@code one|all}. - *
      - *
    • Corresponding method: {@link #setRepeatToggleModes(int)} - *
    • Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES} - *
    - *
  • {@code show_shuffle_button} - Whether the shuffle button is shown. - *
      - *
    • Corresponding method: {@link #setShowShuffleButton(boolean)} - *
    • Default: false - *
    - *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See - * below for more details. - *
      - *
    • Corresponding method: None - *
    • Default: {@code R.id.exo_playback_control_view} - *
    - *
- * - *

Overriding the layout file

- * - * To customize the layout of PlaybackControlView throughout your app, or just for certain - * configurations, you can define {@code exo_playback_control_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by PlaybackControlView. The view identifies and - * binds its children by looking for the following ids: - * - *

- * - *

    - *
  • {@code exo_play} - The play button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_pause} - The pause button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_ffwd} - The fast forward button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_rew} - The rewind button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_prev} - The previous track button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_next} - The next track button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_repeat_toggle} - The repeat toggle button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_shuffle} - The shuffle button. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_position} - Text view displaying the current playback position. - *
      - *
    • Type: {@link TextView} - *
    - *
  • {@code exo_duration} - Text view displaying the current media duration. - *
      - *
    • Type: {@link TextView} - *
    - *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. - *
      - *
    • Type: {@link TimeBar} - *
    - *
- * - *

All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

Specifying a custom layout file

- * - * Defining your own {@code exo_playback_control_view.xml} is useful to customize the layout of - * PlaybackControlView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} - * attribute on a PlaybackControlView. This will cause the specified layout to be inflated instead - * of {@code exo_playback_control_view.xml} for only the instance on which the attribute is set. - */ -public class PlaybackControlView extends FrameLayout { - - static { - ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); - } +/** @deprecated Use {@link PlayerControlView}. */ +@Deprecated +public class PlaybackControlView extends PlayerControlView { /** @deprecated Use {@link com.google.android.exoplayer2.ControlDispatcher}. */ @Deprecated public interface ControlDispatcher extends com.google.android.exoplayer2.ControlDispatcher {} - /** Listener to be notified about changes of the visibility of the UI control. */ - public interface VisibilityListener { - - /** - * Called when the visibility changes. - * - * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. - */ - void onVisibilityChange(int visibility); - } + /** + * @deprecated Use {@link com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener}. + */ + @Deprecated + public interface VisibilityListener + extends com.google.android.exoplayer2.ui.PlayerControlView.VisibilityListener {} private static final class DefaultControlDispatcher extends com.google.android.exoplayer2.DefaultControlDispatcher implements ControlDispatcher {} @@ -188,927 +41,34 @@ public class PlaybackControlView extends FrameLayout { public static final ControlDispatcher DEFAULT_CONTROL_DISPATCHER = new DefaultControlDispatcher(); /** The default fast forward increment, in milliseconds. */ - public static final int DEFAULT_FAST_FORWARD_MS = 15000; + public static final int DEFAULT_FAST_FORWARD_MS = PlayerControlView.DEFAULT_FAST_FORWARD_MS; /** The default rewind increment, in milliseconds. */ - public static final int DEFAULT_REWIND_MS = 5000; + public static final int DEFAULT_REWIND_MS = PlayerControlView.DEFAULT_REWIND_MS; /** The default show timeout, in milliseconds. */ - public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + public static final int DEFAULT_SHOW_TIMEOUT_MS = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; /** The default repeat toggle modes. */ public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = - RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + PlayerControlView.DEFAULT_REPEAT_TOGGLE_MODES; /** The maximum number of windows that can be shown in a multi-window time bar. */ - public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; - - private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; - - private final ComponentListener componentListener; - private final View previousButton; - private final View nextButton; - private final View playButton; - private final View pauseButton; - private final View fastForwardButton; - private final View rewindButton; - private final ImageView repeatToggleButton; - private final View shuffleButton; - private final TextView durationView; - private final TextView positionView; - private final TimeBar timeBar; - private final StringBuilder formatBuilder; - private final Formatter formatter; - private final Timeline.Period period; - private final Timeline.Window window; - - private final Drawable repeatOffButtonDrawable; - private final Drawable repeatOneButtonDrawable; - private final Drawable repeatAllButtonDrawable; - private final String repeatOffButtonContentDescription; - private final String repeatOneButtonContentDescription; - private final String repeatAllButtonContentDescription; - - private Player player; - private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; - private VisibilityListener visibilityListener; - - private boolean isAttachedToWindow; - private boolean showMultiWindowTimeBar; - private boolean multiWindowTimeBar; - private boolean scrubbing; - private int rewindMs; - private int fastForwardMs; - private int showTimeoutMs; - private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; - private boolean showShuffleButton; - private long hideAtMs; - private long[] adGroupTimesMs; - private boolean[] playedAdGroups; - private long[] extraAdGroupTimesMs; - private boolean[] extraPlayedAdGroups; - - private final Runnable updateProgressAction = - new Runnable() { - @Override - public void run() { - updateProgress(); - } - }; - - private final Runnable hideAction = - new Runnable() { - @Override - public void run() { - hide(); - } - }; + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = + PlayerControlView.MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR; public PlaybackControlView(Context context) { - this(context, null); + super(context); } public PlaybackControlView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } public PlaybackControlView(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, attrs); + super(context, attrs, defStyleAttr); } public PlaybackControlView( Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { - super(context, attrs, defStyleAttr); - int controllerLayoutId = R.layout.exo_playback_control_view; - rewindMs = DEFAULT_REWIND_MS; - fastForwardMs = DEFAULT_FAST_FORWARD_MS; - showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; - repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; - showShuffleButton = false; - if (playbackAttrs != null) { - TypedArray a = - context - .getTheme() - .obtainStyledAttributes(playbackAttrs, R.styleable.PlaybackControlView, 0, 0); - try { - rewindMs = a.getInt(R.styleable.PlaybackControlView_rewind_increment, rewindMs); - fastForwardMs = - a.getInt(R.styleable.PlaybackControlView_fastforward_increment, fastForwardMs); - showTimeoutMs = a.getInt(R.styleable.PlaybackControlView_show_timeout, showTimeoutMs); - controllerLayoutId = - a.getResourceId( - R.styleable.PlaybackControlView_controller_layout_id, controllerLayoutId); - repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); - showShuffleButton = - a.getBoolean(R.styleable.PlaybackControlView_show_shuffle_button, showShuffleButton); - } finally { - a.recycle(); - } - } - period = new Timeline.Period(); - window = new Timeline.Window(); - formatBuilder = new StringBuilder(); - formatter = new Formatter(formatBuilder, Locale.getDefault()); - adGroupTimesMs = new long[0]; - playedAdGroups = new boolean[0]; - extraAdGroupTimesMs = new long[0]; - extraPlayedAdGroups = new boolean[0]; - componentListener = new ComponentListener(); - controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); - - LayoutInflater.from(context).inflate(controllerLayoutId, this); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - durationView = findViewById(R.id.exo_duration); - positionView = findViewById(R.id.exo_position); - timeBar = findViewById(R.id.exo_progress); - if (timeBar != null) { - timeBar.addListener(componentListener); - } - playButton = findViewById(R.id.exo_play); - if (playButton != null) { - playButton.setOnClickListener(componentListener); - } - pauseButton = findViewById(R.id.exo_pause); - if (pauseButton != null) { - pauseButton.setOnClickListener(componentListener); - } - previousButton = findViewById(R.id.exo_prev); - if (previousButton != null) { - previousButton.setOnClickListener(componentListener); - } - nextButton = findViewById(R.id.exo_next); - if (nextButton != null) { - nextButton.setOnClickListener(componentListener); - } - rewindButton = findViewById(R.id.exo_rew); - if (rewindButton != null) { - rewindButton.setOnClickListener(componentListener); - } - fastForwardButton = findViewById(R.id.exo_ffwd); - if (fastForwardButton != null) { - fastForwardButton.setOnClickListener(componentListener); - } - repeatToggleButton = findViewById(R.id.exo_repeat_toggle); - if (repeatToggleButton != null) { - repeatToggleButton.setOnClickListener(componentListener); - } - shuffleButton = findViewById(R.id.exo_shuffle); - if (shuffleButton != null) { - shuffleButton.setOnClickListener(componentListener); - } - Resources resources = context.getResources(); - repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); - repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); - repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); - repeatOffButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_off_description); - repeatOneButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_one_description); - repeatAllButtonContentDescription = - resources.getString(R.string.exo_controls_repeat_all_description); + super(context, attrs, defStyleAttr, playbackAttrs); } - @SuppressWarnings("ResourceType") - private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( - TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - return a.getInt(R.styleable.PlaybackControlView_repeat_toggle_modes, repeatToggleModes); - } - - /** - * Returns the {@link Player} currently being controlled by this view, or null if no player is - * set. - */ - public Player getPlayer() { - return player; - } - - /** - * Sets the {@link Player} to control. - * - * @param player The {@link Player} to control. - */ - public void setPlayer(Player player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - } - this.player = player; - if (player != null) { - player.addListener(componentListener); - } - updateAll(); - } - - /** - * Sets whether the time bar should show all windows, as opposed to just the current one. If the - * timeline has a period with unknown duration or more than {@link - * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single - * window. - * - * @param showMultiWindowTimeBar Whether the time bar should show all windows. - */ - public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - this.showMultiWindowTimeBar = showMultiWindowTimeBar; - updateTimeBarMode(); - } - - /** - * Sets the millisecond positions of extra ad markers relative to the start of the window (or - * timeline, if in multi-window mode) and whether each extra ad has been played or not. The - * markers are shown in addition to any ad markers for ads in the player's timeline. - * - * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or - * {@code null} to show no extra ad markers. - * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad - * markers. - */ - public void setExtraAdGroupMarkers( - @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { - if (extraAdGroupTimesMs == null) { - this.extraAdGroupTimesMs = new long[0]; - this.extraPlayedAdGroups = new boolean[0]; - } else { - Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); - this.extraAdGroupTimesMs = extraAdGroupTimesMs; - this.extraPlayedAdGroups = extraPlayedAdGroups; - } - updateProgress(); - } - - /** - * Sets the {@link VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setVisibilityListener(VisibilityListener listener) { - this.visibilityListener = listener; - } - - /** - * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. - * - * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null - * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. - */ - public void setControlDispatcher( - @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { - this.controlDispatcher = - controlDispatcher == null - ? new com.google.android.exoplayer2.DefaultControlDispatcher() - : controlDispatcher; - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - this.rewindMs = rewindMs; - updateNavigation(); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - this.fastForwardMs = fastForwardMs; - updateNavigation(); - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input. - * - * @return The duration in milliseconds. A non-positive value indicates that the controls will - * remain visible indefinitely. - */ - public int getShowTimeoutMs() { - return showTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input. - * - * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls - * to remain visible indefinitely. - */ - public void setShowTimeoutMs(int showTimeoutMs) { - this.showTimeoutMs = showTimeoutMs; - // showTimeoutMs is changed, so call hideAfterTimeout to reset the timeout. - if (isVisible()) { - hideAfterTimeout(); - } - } - - /** - * Returns which repeat toggle modes are enabled. - * - * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. - */ - public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { - return repeatToggleModes; - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - this.repeatToggleModes = repeatToggleModes; - if (player != null) { - @Player.RepeatMode int currentMode = player.getRepeatMode(); - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE - && currentMode != Player.REPEAT_MODE_OFF) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE - && currentMode == Player.REPEAT_MODE_ALL) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); - } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL - && currentMode == Player.REPEAT_MODE_ONE) { - controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); - } - } - } - - /** Returns whether the shuffle button is shown. */ - public boolean getShowShuffleButton() { - return showShuffleButton; - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - this.showShuffleButton = showShuffleButton; - updateShuffleButton(); - } - - /** - * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will - * be automatically hidden after this duration of time has elapsed without user input. - */ - public void show() { - if (!isVisible()) { - setVisibility(VISIBLE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - updateAll(); - requestPlayPauseFocus(); - } - // Call hideAfterTimeout even if already visible to reset the timeout. - hideAfterTimeout(); - } - - /** Hides the controller. */ - public void hide() { - if (isVisible()) { - setVisibility(GONE); - if (visibilityListener != null) { - visibilityListener.onVisibilityChange(getVisibility()); - } - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - hideAtMs = C.TIME_UNSET; - } - } - - /** Returns whether the controller is currently visible. */ - public boolean isVisible() { - return getVisibility() == VISIBLE; - } - - private void hideAfterTimeout() { - removeCallbacks(hideAction); - if (showTimeoutMs > 0) { - hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; - if (isAttachedToWindow) { - postDelayed(hideAction, showTimeoutMs); - } - } else { - hideAtMs = C.TIME_UNSET; - } - } - - private void updateAll() { - updatePlayPauseButton(); - updateNavigation(); - updateRepeatModeButton(); - updateShuffleButton(); - updateProgress(); - } - - private void updatePlayPauseButton() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - boolean requestPlayPauseFocus = false; - boolean playing = player != null && player.getPlayWhenReady(); - if (playButton != null) { - requestPlayPauseFocus |= playing && playButton.isFocused(); - playButton.setVisibility(playing ? View.GONE : View.VISIBLE); - } - if (pauseButton != null) { - requestPlayPauseFocus |= !playing && pauseButton.isFocused(); - pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); - } - if (requestPlayPauseFocus) { - requestPlayPauseFocus(); - } - } - - private void updateNavigation() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - Timeline timeline = player != null ? player.getCurrentTimeline() : null; - boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); - boolean isSeekable = false; - boolean enablePrevious = false; - boolean enableNext = false; - if (haveNonEmptyTimeline && !player.isPlayingAd()) { - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - isSeekable = window.isSeekable; - enablePrevious = - isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; - enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; - } - setButtonEnabled(enablePrevious, previousButton); - setButtonEnabled(enableNext, nextButton); - setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); - setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); - if (timeBar != null) { - timeBar.setEnabled(isSeekable); - } - } - - private void updateRepeatModeButton() { - if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { - return; - } - if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { - repeatToggleButton.setVisibility(View.GONE); - return; - } - if (player == null) { - setButtonEnabled(false, repeatToggleButton); - return; - } - setButtonEnabled(true, repeatToggleButton); - switch (player.getRepeatMode()) { - case Player.REPEAT_MODE_OFF: - repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); - repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); - break; - case Player.REPEAT_MODE_ONE: - repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); - repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); - break; - case Player.REPEAT_MODE_ALL: - repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); - repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); - break; - default: - // Never happens. - } - repeatToggleButton.setVisibility(View.VISIBLE); - } - - private void updateShuffleButton() { - if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { - return; - } - if (!showShuffleButton) { - shuffleButton.setVisibility(View.GONE); - } else if (player == null) { - setButtonEnabled(false, shuffleButton); - } else { - shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); - shuffleButton.setEnabled(true); - shuffleButton.setVisibility(View.VISIBLE); - } - } - - private void updateTimeBarMode() { - if (player == null) { - return; - } - multiWindowTimeBar = - showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); - } - - private void updateProgress() { - if (!isVisible() || !isAttachedToWindow) { - return; - } - - long position = 0; - long bufferedPosition = 0; - long duration = 0; - if (player != null) { - long currentWindowTimeBarOffsetUs = 0; - long durationUs = 0; - int adGroupCount = 0; - Timeline timeline = player.getCurrentTimeline(); - if (!timeline.isEmpty()) { - int currentWindowIndex = player.getCurrentWindowIndex(); - int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; - int lastWindowIndex = - multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; - for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { - if (i == currentWindowIndex) { - currentWindowTimeBarOffsetUs = durationUs; - } - timeline.getWindow(i, window); - if (window.durationUs == C.TIME_UNSET) { - Assertions.checkState(!multiWindowTimeBar); - break; - } - for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { - timeline.getPeriod(j, period); - int periodAdGroupCount = period.getAdGroupCount(); - for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { - long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); - if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { - if (period.durationUs == C.TIME_UNSET) { - // Don't show ad markers for postrolls in periods with unknown duration. - continue; - } - adGroupTimeInPeriodUs = period.durationUs; - } - long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); - if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { - if (adGroupCount == adGroupTimesMs.length) { - int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); - playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); - } - adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); - playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); - adGroupCount++; - } - } - } - durationUs += window.durationUs; - } - } - duration = C.usToMs(durationUs); - position = C.usToMs(currentWindowTimeBarOffsetUs); - bufferedPosition = position; - if (player.isPlayingAd()) { - position += player.getContentPosition(); - bufferedPosition = position; - } else { - position += player.getCurrentPosition(); - bufferedPosition += player.getBufferedPosition(); - } - if (timeBar != null) { - int extraAdGroupCount = extraAdGroupTimesMs.length; - int totalAdGroupCount = adGroupCount + extraAdGroupCount; - if (totalAdGroupCount > adGroupTimesMs.length) { - adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); - playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); - } - System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); - System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); - timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); - } - } - if (durationView != null) { - durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); - } - if (positionView != null && !scrubbing) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - if (timeBar != null) { - timeBar.setPosition(position); - timeBar.setBufferedPosition(bufferedPosition); - timeBar.setDuration(duration); - } - - // Cancel any pending updates and schedule a new one if necessary. - removeCallbacks(updateProgressAction); - int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); - if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { - long delayMs; - if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { - float playbackSpeed = player.getPlaybackParameters().speed; - if (playbackSpeed <= 0.1f) { - delayMs = 1000; - } else if (playbackSpeed <= 5f) { - long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); - long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); - if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { - mediaTimeDelayMs += mediaTimeUpdatePeriodMs; - } - delayMs = - playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); - } else { - delayMs = 200; - } - } else { - delayMs = 1000; - } - postDelayed(updateProgressAction, delayMs); - } - } - - private void requestPlayPauseFocus() { - boolean playing = player != null && player.getPlayWhenReady(); - if (!playing && playButton != null) { - playButton.requestFocus(); - } else if (playing && pauseButton != null) { - pauseButton.requestFocus(); - } - } - - private void setButtonEnabled(boolean enabled, View view) { - if (view == null) { - return; - } - view.setEnabled(enabled); - view.setAlpha(enabled ? 1f : 0.3f); - view.setVisibility(VISIBLE); - } - - private void previous() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - timeline.getWindow(windowIndex, window); - int previousWindowIndex = player.getPreviousWindowIndex(); - if (previousWindowIndex != C.INDEX_UNSET - && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS - || (window.isDynamic && !window.isSeekable))) { - seekTo(previousWindowIndex, C.TIME_UNSET); - } else { - seekTo(0); - } - } - - private void next() { - Timeline timeline = player.getCurrentTimeline(); - if (timeline.isEmpty()) { - return; - } - int windowIndex = player.getCurrentWindowIndex(); - int nextWindowIndex = player.getNextWindowIndex(); - if (nextWindowIndex != C.INDEX_UNSET) { - seekTo(nextWindowIndex, C.TIME_UNSET); - } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { - seekTo(windowIndex, C.TIME_UNSET); - } - } - - private void rewind() { - if (rewindMs <= 0) { - return; - } - seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); - } - - private void fastForward() { - if (fastForwardMs <= 0) { - return; - } - long durationMs = player.getDuration(); - long seekPositionMs = player.getCurrentPosition() + fastForwardMs; - if (durationMs != C.TIME_UNSET) { - seekPositionMs = Math.min(seekPositionMs, durationMs); - } - seekTo(seekPositionMs); - } - - private void seekTo(long positionMs) { - seekTo(player.getCurrentWindowIndex(), positionMs); - } - - private void seekTo(int windowIndex, long positionMs) { - boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); - if (!dispatched) { - // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the - // seek then it'll now be in the wrong position. Trigger a progress update to snap it back. - updateProgress(); - } - } - - private void seekToTimeBarPosition(long positionMs) { - int windowIndex; - Timeline timeline = player.getCurrentTimeline(); - if (multiWindowTimeBar && !timeline.isEmpty()) { - int windowCount = timeline.getWindowCount(); - windowIndex = 0; - while (true) { - long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); - if (positionMs < windowDurationMs) { - break; - } else if (windowIndex == windowCount - 1) { - // Seeking past the end of the last window should seek to the end of the timeline. - positionMs = windowDurationMs; - break; - } - positionMs -= windowDurationMs; - windowIndex++; - } - } else { - windowIndex = player.getCurrentWindowIndex(); - } - seekTo(windowIndex, positionMs); - } - - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - isAttachedToWindow = true; - if (hideAtMs != C.TIME_UNSET) { - long delayMs = hideAtMs - SystemClock.uptimeMillis(); - if (delayMs <= 0) { - hide(); - } else { - postDelayed(hideAction, delayMs); - } - } - updateAll(); - } - - @Override - public void onDetachedFromWindow() { - super.onDetachedFromWindow(); - isAttachedToWindow = false; - removeCallbacks(updateProgressAction); - removeCallbacks(hideAction); - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - if (player == null || !isHandledMediaKey(keyCode)) { - return false; - } - if (event.getAction() == KeyEvent.ACTION_DOWN) { - if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { - fastForward(); - } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { - rewind(); - } else if (event.getRepeatCount() == 0) { - switch (keyCode) { - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - controlDispatcher.dispatchSetPlayWhenReady(player, true); - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - controlDispatcher.dispatchSetPlayWhenReady(player, false); - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - next(); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - previous(); - break; - default: - break; - } - } - } - return true; - } - - @SuppressLint("InlinedApi") - private static boolean isHandledMediaKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD - || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY - || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE - || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT - || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; - } - - /** - * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. - * - * @param timeline The {@link Timeline} to check. - * @param window A scratch {@link Timeline.Window} instance. - * @return Whether the specified timeline can be shown on a multi-window time bar. - */ - private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { - if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { - return false; - } - int windowCount = timeline.getWindowCount(); - for (int i = 0; i < windowCount; i++) { - if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { - return false; - } - } - return true; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TimeBar.OnScrubListener, OnClickListener { - - @Override - public void onScrubStart(TimeBar timeBar, long position) { - removeCallbacks(hideAction); - scrubbing = true; - } - - @Override - public void onScrubMove(TimeBar timeBar, long position) { - if (positionView != null) { - positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); - } - } - - @Override - public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { - scrubbing = false; - if (!canceled && player != null) { - seekToTimeBarPosition(position); - } - hideAfterTimeout(); - } - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - updatePlayPauseButton(); - updateProgress(); - } - - @Override - public void onRepeatModeChanged(int repeatMode) { - updateRepeatModeButton(); - updateNavigation(); - } - - @Override - public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { - updateShuffleButton(); - updateNavigation(); - } - - @Override - public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { - updateNavigation(); - updateProgress(); - } - - @Override - public void onTimelineChanged( - Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { - updateNavigation(); - updateTimeBarMode(); - updateProgress(); - } - - @Override - public void onClick(View view) { - if (player != null) { - if (nextButton == view) { - next(); - } else if (previousButton == view) { - previous(); - } else if (fastForwardButton == view) { - fastForward(); - } else if (rewindButton == view) { - rewind(); - } else if (playButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, true); - } else if (pauseButton == view) { - controlDispatcher.dispatchSetPlayWhenReady(player, false); - } else if (repeatToggleButton == view) { - controlDispatcher.dispatchSetRepeatMode( - player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); - } else if (shuffleButton == view) { - controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); - } - } - hideAfterTimeout(); - } - } } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java new file mode 100644 index 0000000000..20c3ef02dc --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerControlView.java @@ -0,0 +1,1101 @@ +/* + * 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 com.google.android.exoplayer2.ui; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Timeline; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import java.util.Arrays; +import java.util.Formatter; +import java.util.Locale; + +/** + * A view for controlling {@link Player} instances. + * + *

A PlayerControlView can be customized by setting attributes (or calling corresponding + * methods), overriding the view's layout file or by specifying a custom view layout file, as + * outlined below. + * + *

Attributes

+ * + * The following attributes can be set on a PlayerControlView when used in a layout XML file: + * + *
    + *
  • {@code show_timeout} - The time between the last user interaction and the controls + * being automatically hidden, in milliseconds. Use zero if the controls should not + * automatically timeout. + *
      + *
    • Corresponding method: {@link #setShowTimeoutMs(int)} + *
    • Default: {@link #DEFAULT_SHOW_TIMEOUT_MS} + *
    + *
  • {@code rewind_increment} - The duration of the rewind applied when the user taps the + * rewind button, in milliseconds. Use zero to disable the rewind button. + *
      + *
    • Corresponding method: {@link #setRewindIncrementMs(int)} + *
    • Default: {@link #DEFAULT_REWIND_MS} + *
    + *
  • {@code fastforward_increment} - Like {@code rewind_increment}, but for fast forward. + *
      + *
    • Corresponding method: {@link #setFastForwardIncrementMs(int)} + *
    • Default: {@link #DEFAULT_FAST_FORWARD_MS} + *
    + *
  • {@code repeat_toggle_modes} - A flagged enumeration value specifying which repeat + * mode toggle options are enabled. Valid values are: {@code none}, {@code one}, {@code all}, + * or {@code one|all}. + *
      + *
    • Corresponding method: {@link #setRepeatToggleModes(int)} + *
    • Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES} + *
    + *
  • {@code show_shuffle_button} - Whether the shuffle button is shown. + *
      + *
    • Corresponding method: {@link #setShowShuffleButton(boolean)} + *
    • Default: false + *
    + *
  • {@code controller_layout_id} - Specifies the id of the layout to be inflated. See + * below for more details. + *
      + *
    • Corresponding method: None + *
    • Default: {@code R.id.exo_player_control_view} + *
    + *
+ * + *

Overriding the layout file

+ * + * To customize the layout of PlayerControlView throughout your app, or just for certain + * configurations, you can define {@code exo_player_control_view.xml} layout files in your + * application {@code res/layout*} directories. These layouts will override the one provided by the + * ExoPlayer library, and will be inflated for use by PlayerControlView. The view identifies and + * binds its children by looking for the following ids: + * + *

+ * + *

    + *
  • {@code exo_play} - The play button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_pause} - The pause button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_ffwd} - The fast forward button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_rew} - The rewind button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_prev} - The previous track button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_next} - The next track button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_repeat_toggle} - The repeat toggle button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_shuffle} - The shuffle button. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_position} - Text view displaying the current playback position. + *
      + *
    • Type: {@link TextView} + *
    + *
  • {@code exo_duration} - Text view displaying the current media duration. + *
      + *
    • Type: {@link TextView} + *
    + *
  • {@code exo_progress} - Time bar that's updated during playback and allows seeking. + *
      + *
    • Type: {@link TimeBar} + *
    + *
+ * + *

All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

Specifying a custom layout file

+ * + * Defining your own {@code exo_player_control_view.xml} is useful to customize the layout of + * PlayerControlView throughout your application. It's also possible to customize the layout for a + * single instance in a layout file. This is achieved by setting the {@code controller_layout_id} + * attribute on a PlayerControlView. This will cause the specified layout to be inflated instead of + * {@code exo_player_control_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerControlView extends FrameLayout { + + static { + ExoPlayerLibraryInfo.registerModule("goog.exo.ui"); + } + + /** Listener to be notified about changes of the visibility of the UI control. */ + public interface VisibilityListener { + + /** + * Called when the visibility changes. + * + * @param visibility The new visibility. Either {@link View#VISIBLE} or {@link View#GONE}. + */ + void onVisibilityChange(int visibility); + } + + /** The default fast forward increment, in milliseconds. */ + public static final int DEFAULT_FAST_FORWARD_MS = 15000; + /** The default rewind increment, in milliseconds. */ + public static final int DEFAULT_REWIND_MS = 5000; + /** The default show timeout, in milliseconds. */ + public static final int DEFAULT_SHOW_TIMEOUT_MS = 5000; + /** The default repeat toggle modes. */ + public static final @RepeatModeUtil.RepeatToggleModes int DEFAULT_REPEAT_TOGGLE_MODES = + RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE; + + /** The maximum number of windows that can be shown in a multi-window time bar. */ + public static final int MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR = 100; + + private static final long MAX_POSITION_FOR_SEEK_TO_PREVIOUS = 3000; + + private final ComponentListener componentListener; + private final View previousButton; + private final View nextButton; + private final View playButton; + private final View pauseButton; + private final View fastForwardButton; + private final View rewindButton; + private final ImageView repeatToggleButton; + private final View shuffleButton; + private final TextView durationView; + private final TextView positionView; + private final TimeBar timeBar; + private final StringBuilder formatBuilder; + private final Formatter formatter; + private final Timeline.Period period; + private final Timeline.Window window; + + private final Drawable repeatOffButtonDrawable; + private final Drawable repeatOneButtonDrawable; + private final Drawable repeatAllButtonDrawable; + private final String repeatOffButtonContentDescription; + private final String repeatOneButtonContentDescription; + private final String repeatAllButtonContentDescription; + + private Player player; + private com.google.android.exoplayer2.ControlDispatcher controlDispatcher; + private VisibilityListener visibilityListener; + + private boolean isAttachedToWindow; + private boolean showMultiWindowTimeBar; + private boolean multiWindowTimeBar; + private boolean scrubbing; + private int rewindMs; + private int fastForwardMs; + private int showTimeoutMs; + private @RepeatModeUtil.RepeatToggleModes int repeatToggleModes; + private boolean showShuffleButton; + private long hideAtMs; + private long[] adGroupTimesMs; + private boolean[] playedAdGroups; + private long[] extraAdGroupTimesMs; + private boolean[] extraPlayedAdGroups; + + private final Runnable updateProgressAction = + new Runnable() { + @Override + public void run() { + updateProgress(); + } + }; + + private final Runnable hideAction = + new Runnable() { + @Override + public void run() { + hide(); + } + }; + + public PlayerControlView(Context context) { + this(context, null); + } + + public PlayerControlView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerControlView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, attrs); + } + + public PlayerControlView( + Context context, AttributeSet attrs, int defStyleAttr, AttributeSet playbackAttrs) { + super(context, attrs, defStyleAttr); + int controllerLayoutId = R.layout.exo_player_control_view; + rewindMs = DEFAULT_REWIND_MS; + fastForwardMs = DEFAULT_FAST_FORWARD_MS; + showTimeoutMs = DEFAULT_SHOW_TIMEOUT_MS; + repeatToggleModes = DEFAULT_REPEAT_TOGGLE_MODES; + showShuffleButton = false; + if (playbackAttrs != null) { + TypedArray a = + context + .getTheme() + .obtainStyledAttributes(playbackAttrs, R.styleable.PlayerControlView, 0, 0); + try { + rewindMs = a.getInt(R.styleable.PlayerControlView_rewind_increment, rewindMs); + fastForwardMs = + a.getInt(R.styleable.PlayerControlView_fastforward_increment, fastForwardMs); + showTimeoutMs = a.getInt(R.styleable.PlayerControlView_show_timeout, showTimeoutMs); + controllerLayoutId = + a.getResourceId(R.styleable.PlayerControlView_controller_layout_id, controllerLayoutId); + repeatToggleModes = getRepeatToggleModes(a, repeatToggleModes); + showShuffleButton = + a.getBoolean(R.styleable.PlayerControlView_show_shuffle_button, showShuffleButton); + } finally { + a.recycle(); + } + } + period = new Timeline.Period(); + window = new Timeline.Window(); + formatBuilder = new StringBuilder(); + formatter = new Formatter(formatBuilder, Locale.getDefault()); + adGroupTimesMs = new long[0]; + playedAdGroups = new boolean[0]; + extraAdGroupTimesMs = new long[0]; + extraPlayedAdGroups = new boolean[0]; + componentListener = new ComponentListener(); + controlDispatcher = new com.google.android.exoplayer2.DefaultControlDispatcher(); + + LayoutInflater.from(context).inflate(controllerLayoutId, this); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + durationView = findViewById(R.id.exo_duration); + positionView = findViewById(R.id.exo_position); + timeBar = findViewById(R.id.exo_progress); + if (timeBar != null) { + timeBar.addListener(componentListener); + } + playButton = findViewById(R.id.exo_play); + if (playButton != null) { + playButton.setOnClickListener(componentListener); + } + pauseButton = findViewById(R.id.exo_pause); + if (pauseButton != null) { + pauseButton.setOnClickListener(componentListener); + } + previousButton = findViewById(R.id.exo_prev); + if (previousButton != null) { + previousButton.setOnClickListener(componentListener); + } + nextButton = findViewById(R.id.exo_next); + if (nextButton != null) { + nextButton.setOnClickListener(componentListener); + } + rewindButton = findViewById(R.id.exo_rew); + if (rewindButton != null) { + rewindButton.setOnClickListener(componentListener); + } + fastForwardButton = findViewById(R.id.exo_ffwd); + if (fastForwardButton != null) { + fastForwardButton.setOnClickListener(componentListener); + } + repeatToggleButton = findViewById(R.id.exo_repeat_toggle); + if (repeatToggleButton != null) { + repeatToggleButton.setOnClickListener(componentListener); + } + shuffleButton = findViewById(R.id.exo_shuffle); + if (shuffleButton != null) { + shuffleButton.setOnClickListener(componentListener); + } + Resources resources = context.getResources(); + repeatOffButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_off); + repeatOneButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_one); + repeatAllButtonDrawable = resources.getDrawable(R.drawable.exo_controls_repeat_all); + repeatOffButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_off_description); + repeatOneButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_one_description); + repeatAllButtonContentDescription = + resources.getString(R.string.exo_controls_repeat_all_description); + } + + @SuppressWarnings("ResourceType") + private static @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes( + TypedArray a, @RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + return a.getInt(R.styleable.PlayerControlView_repeat_toggle_modes, repeatToggleModes); + } + + /** + * Returns the {@link Player} currently being controlled by this view, or null if no player is + * set. + */ + public Player getPlayer() { + return player; + } + + /** + * Sets the {@link Player} to control. + * + * @param player The {@link Player} to control. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + } + this.player = player; + if (player != null) { + player.addListener(componentListener); + } + updateAll(); + } + + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. If the + * timeline has a period with unknown duration or more than {@link + * #MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR} windows the time bar will fall back to showing a single + * window. + * + * @param showMultiWindowTimeBar Whether the time bar should show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + this.showMultiWindowTimeBar = showMultiWindowTimeBar; + updateTimeBarMode(); + } + + /** + * Sets the millisecond positions of extra ad markers relative to the start of the window (or + * timeline, if in multi-window mode) and whether each extra ad has been played or not. The + * markers are shown in addition to any ad markers for ads in the player's timeline. + * + * @param extraAdGroupTimesMs The millisecond timestamps of the extra ad markers to show, or + * {@code null} to show no extra ad markers. + * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad + * markers. + */ + public void setExtraAdGroupMarkers( + @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { + if (extraAdGroupTimesMs == null) { + this.extraAdGroupTimesMs = new long[0]; + this.extraPlayedAdGroups = new boolean[0]; + } else { + Assertions.checkArgument(extraAdGroupTimesMs.length == extraPlayedAdGroups.length); + this.extraAdGroupTimesMs = extraAdGroupTimesMs; + this.extraPlayedAdGroups = extraPlayedAdGroups; + } + updateProgress(); + } + + /** + * Sets the {@link VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setVisibilityListener(VisibilityListener listener) { + this.visibilityListener = listener; + } + + /** + * Sets the {@link com.google.android.exoplayer2.ControlDispatcher}. + * + * @param controlDispatcher The {@link com.google.android.exoplayer2.ControlDispatcher}, or null + * to use {@link com.google.android.exoplayer2.DefaultControlDispatcher}. + */ + public void setControlDispatcher( + @Nullable com.google.android.exoplayer2.ControlDispatcher controlDispatcher) { + this.controlDispatcher = + controlDispatcher == null + ? new com.google.android.exoplayer2.DefaultControlDispatcher() + : controlDispatcher; + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + this.rewindMs = rewindMs; + updateNavigation(); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + this.fastForwardMs = fastForwardMs; + updateNavigation(); + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input. + * + * @return The duration in milliseconds. A non-positive value indicates that the controls will + * remain visible indefinitely. + */ + public int getShowTimeoutMs() { + return showTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input. + * + * @param showTimeoutMs The duration in milliseconds. A non-positive value will cause the controls + * to remain visible indefinitely. + */ + public void setShowTimeoutMs(int showTimeoutMs) { + this.showTimeoutMs = showTimeoutMs; + if (isVisible()) { + // Reset the timeout. + hideAfterTimeout(); + } + } + + /** + * Returns which repeat toggle modes are enabled. + * + * @return The currently enabled {@link RepeatModeUtil.RepeatToggleModes}. + */ + public @RepeatModeUtil.RepeatToggleModes int getRepeatToggleModes() { + return repeatToggleModes; + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + this.repeatToggleModes = repeatToggleModes; + if (player != null) { + @Player.RepeatMode int currentMode = player.getRepeatMode(); + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE + && currentMode != Player.REPEAT_MODE_OFF) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_OFF); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ONE + && currentMode == Player.REPEAT_MODE_ALL) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ONE); + } else if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL + && currentMode == Player.REPEAT_MODE_ONE) { + controlDispatcher.dispatchSetRepeatMode(player, Player.REPEAT_MODE_ALL); + } + } + } + + /** Returns whether the shuffle button is shown. */ + public boolean getShowShuffleButton() { + return showShuffleButton; + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + this.showShuffleButton = showShuffleButton; + updateShuffleButton(); + } + + /** + * Shows the playback controls. If {@link #getShowTimeoutMs()} is positive then the controls will + * be automatically hidden after this duration of time has elapsed without user input. + */ + public void show() { + if (!isVisible()) { + setVisibility(VISIBLE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + updateAll(); + requestPlayPauseFocus(); + } + // Call hideAfterTimeout even if already visible to reset the timeout. + hideAfterTimeout(); + } + + /** Hides the controller. */ + public void hide() { + if (isVisible()) { + setVisibility(GONE); + if (visibilityListener != null) { + visibilityListener.onVisibilityChange(getVisibility()); + } + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + hideAtMs = C.TIME_UNSET; + } + } + + /** Returns whether the controller is currently visible. */ + public boolean isVisible() { + return getVisibility() == VISIBLE; + } + + private void hideAfterTimeout() { + removeCallbacks(hideAction); + if (showTimeoutMs > 0) { + hideAtMs = SystemClock.uptimeMillis() + showTimeoutMs; + if (isAttachedToWindow) { + postDelayed(hideAction, showTimeoutMs); + } + } else { + hideAtMs = C.TIME_UNSET; + } + } + + private void updateAll() { + updatePlayPauseButton(); + updateNavigation(); + updateRepeatModeButton(); + updateShuffleButton(); + updateProgress(); + } + + private void updatePlayPauseButton() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + boolean requestPlayPauseFocus = false; + boolean playing = player != null && player.getPlayWhenReady(); + if (playButton != null) { + requestPlayPauseFocus |= playing && playButton.isFocused(); + playButton.setVisibility(playing ? View.GONE : View.VISIBLE); + } + if (pauseButton != null) { + requestPlayPauseFocus |= !playing && pauseButton.isFocused(); + pauseButton.setVisibility(!playing ? View.GONE : View.VISIBLE); + } + if (requestPlayPauseFocus) { + requestPlayPauseFocus(); + } + } + + private void updateNavigation() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + Timeline timeline = player != null ? player.getCurrentTimeline() : null; + boolean haveNonEmptyTimeline = timeline != null && !timeline.isEmpty(); + boolean isSeekable = false; + boolean enablePrevious = false; + boolean enableNext = false; + if (haveNonEmptyTimeline && !player.isPlayingAd()) { + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + isSeekable = window.isSeekable; + enablePrevious = + isSeekable || !window.isDynamic || player.getPreviousWindowIndex() != C.INDEX_UNSET; + enableNext = window.isDynamic || player.getNextWindowIndex() != C.INDEX_UNSET; + } + setButtonEnabled(enablePrevious, previousButton); + setButtonEnabled(enableNext, nextButton); + setButtonEnabled(fastForwardMs > 0 && isSeekable, fastForwardButton); + setButtonEnabled(rewindMs > 0 && isSeekable, rewindButton); + if (timeBar != null) { + timeBar.setEnabled(isSeekable); + } + } + + private void updateRepeatModeButton() { + if (!isVisible() || !isAttachedToWindow || repeatToggleButton == null) { + return; + } + if (repeatToggleModes == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) { + repeatToggleButton.setVisibility(View.GONE); + return; + } + if (player == null) { + setButtonEnabled(false, repeatToggleButton); + return; + } + setButtonEnabled(true, repeatToggleButton); + switch (player.getRepeatMode()) { + case Player.REPEAT_MODE_OFF: + repeatToggleButton.setImageDrawable(repeatOffButtonDrawable); + repeatToggleButton.setContentDescription(repeatOffButtonContentDescription); + break; + case Player.REPEAT_MODE_ONE: + repeatToggleButton.setImageDrawable(repeatOneButtonDrawable); + repeatToggleButton.setContentDescription(repeatOneButtonContentDescription); + break; + case Player.REPEAT_MODE_ALL: + repeatToggleButton.setImageDrawable(repeatAllButtonDrawable); + repeatToggleButton.setContentDescription(repeatAllButtonContentDescription); + break; + default: + // Never happens. + } + repeatToggleButton.setVisibility(View.VISIBLE); + } + + private void updateShuffleButton() { + if (!isVisible() || !isAttachedToWindow || shuffleButton == null) { + return; + } + if (!showShuffleButton) { + shuffleButton.setVisibility(View.GONE); + } else if (player == null) { + setButtonEnabled(false, shuffleButton); + } else { + shuffleButton.setAlpha(player.getShuffleModeEnabled() ? 1f : 0.3f); + shuffleButton.setEnabled(true); + shuffleButton.setVisibility(View.VISIBLE); + } + } + + private void updateTimeBarMode() { + if (player == null) { + return; + } + multiWindowTimeBar = + showMultiWindowTimeBar && canShowMultiWindowTimeBar(player.getCurrentTimeline(), window); + } + + private void updateProgress() { + if (!isVisible() || !isAttachedToWindow) { + return; + } + + long position = 0; + long bufferedPosition = 0; + long duration = 0; + if (player != null) { + long currentWindowTimeBarOffsetUs = 0; + long durationUs = 0; + int adGroupCount = 0; + Timeline timeline = player.getCurrentTimeline(); + if (!timeline.isEmpty()) { + int currentWindowIndex = player.getCurrentWindowIndex(); + int firstWindowIndex = multiWindowTimeBar ? 0 : currentWindowIndex; + int lastWindowIndex = + multiWindowTimeBar ? timeline.getWindowCount() - 1 : currentWindowIndex; + for (int i = firstWindowIndex; i <= lastWindowIndex; i++) { + if (i == currentWindowIndex) { + currentWindowTimeBarOffsetUs = durationUs; + } + timeline.getWindow(i, window); + if (window.durationUs == C.TIME_UNSET) { + Assertions.checkState(!multiWindowTimeBar); + break; + } + for (int j = window.firstPeriodIndex; j <= window.lastPeriodIndex; j++) { + timeline.getPeriod(j, period); + int periodAdGroupCount = period.getAdGroupCount(); + for (int adGroupIndex = 0; adGroupIndex < periodAdGroupCount; adGroupIndex++) { + long adGroupTimeInPeriodUs = period.getAdGroupTimeUs(adGroupIndex); + if (adGroupTimeInPeriodUs == C.TIME_END_OF_SOURCE) { + if (period.durationUs == C.TIME_UNSET) { + // Don't show ad markers for postrolls in periods with unknown duration. + continue; + } + adGroupTimeInPeriodUs = period.durationUs; + } + long adGroupTimeInWindowUs = adGroupTimeInPeriodUs + period.getPositionInWindowUs(); + if (adGroupTimeInWindowUs >= 0 && adGroupTimeInWindowUs <= window.durationUs) { + if (adGroupCount == adGroupTimesMs.length) { + int newLength = adGroupTimesMs.length == 0 ? 1 : adGroupTimesMs.length * 2; + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, newLength); + playedAdGroups = Arrays.copyOf(playedAdGroups, newLength); + } + adGroupTimesMs[adGroupCount] = C.usToMs(durationUs + adGroupTimeInWindowUs); + playedAdGroups[adGroupCount] = period.hasPlayedAdGroup(adGroupIndex); + adGroupCount++; + } + } + } + durationUs += window.durationUs; + } + } + duration = C.usToMs(durationUs); + position = C.usToMs(currentWindowTimeBarOffsetUs); + bufferedPosition = position; + if (player.isPlayingAd()) { + position += player.getContentPosition(); + bufferedPosition = position; + } else { + position += player.getCurrentPosition(); + bufferedPosition += player.getBufferedPosition(); + } + if (timeBar != null) { + int extraAdGroupCount = extraAdGroupTimesMs.length; + int totalAdGroupCount = adGroupCount + extraAdGroupCount; + if (totalAdGroupCount > adGroupTimesMs.length) { + adGroupTimesMs = Arrays.copyOf(adGroupTimesMs, totalAdGroupCount); + playedAdGroups = Arrays.copyOf(playedAdGroups, totalAdGroupCount); + } + System.arraycopy(extraAdGroupTimesMs, 0, adGroupTimesMs, adGroupCount, extraAdGroupCount); + System.arraycopy(extraPlayedAdGroups, 0, playedAdGroups, adGroupCount, extraAdGroupCount); + timeBar.setAdGroupTimesMs(adGroupTimesMs, playedAdGroups, totalAdGroupCount); + } + } + if (durationView != null) { + durationView.setText(Util.getStringForTime(formatBuilder, formatter, duration)); + } + if (positionView != null && !scrubbing) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + if (timeBar != null) { + timeBar.setPosition(position); + timeBar.setBufferedPosition(bufferedPosition); + timeBar.setDuration(duration); + } + + // Cancel any pending updates and schedule a new one if necessary. + removeCallbacks(updateProgressAction); + int playbackState = player == null ? Player.STATE_IDLE : player.getPlaybackState(); + if (playbackState != Player.STATE_IDLE && playbackState != Player.STATE_ENDED) { + long delayMs; + if (player.getPlayWhenReady() && playbackState == Player.STATE_READY) { + float playbackSpeed = player.getPlaybackParameters().speed; + if (playbackSpeed <= 0.1f) { + delayMs = 1000; + } else if (playbackSpeed <= 5f) { + long mediaTimeUpdatePeriodMs = 1000 / Math.max(1, Math.round(1 / playbackSpeed)); + long mediaTimeDelayMs = mediaTimeUpdatePeriodMs - (position % mediaTimeUpdatePeriodMs); + if (mediaTimeDelayMs < (mediaTimeUpdatePeriodMs / 5)) { + mediaTimeDelayMs += mediaTimeUpdatePeriodMs; + } + delayMs = + playbackSpeed == 1 ? mediaTimeDelayMs : (long) (mediaTimeDelayMs / playbackSpeed); + } else { + delayMs = 200; + } + } else { + delayMs = 1000; + } + postDelayed(updateProgressAction, delayMs); + } + } + + private void requestPlayPauseFocus() { + boolean playing = player != null && player.getPlayWhenReady(); + if (!playing && playButton != null) { + playButton.requestFocus(); + } else if (playing && pauseButton != null) { + pauseButton.requestFocus(); + } + } + + private void setButtonEnabled(boolean enabled, View view) { + if (view == null) { + return; + } + view.setEnabled(enabled); + view.setAlpha(enabled ? 1f : 0.3f); + view.setVisibility(VISIBLE); + } + + private void previous() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + timeline.getWindow(windowIndex, window); + int previousWindowIndex = player.getPreviousWindowIndex(); + if (previousWindowIndex != C.INDEX_UNSET + && (player.getCurrentPosition() <= MAX_POSITION_FOR_SEEK_TO_PREVIOUS + || (window.isDynamic && !window.isSeekable))) { + seekTo(previousWindowIndex, C.TIME_UNSET); + } else { + seekTo(0); + } + } + + private void next() { + Timeline timeline = player.getCurrentTimeline(); + if (timeline.isEmpty()) { + return; + } + int windowIndex = player.getCurrentWindowIndex(); + int nextWindowIndex = player.getNextWindowIndex(); + if (nextWindowIndex != C.INDEX_UNSET) { + seekTo(nextWindowIndex, C.TIME_UNSET); + } else if (timeline.getWindow(windowIndex, window, false).isDynamic) { + seekTo(windowIndex, C.TIME_UNSET); + } + } + + private void rewind() { + if (rewindMs <= 0) { + return; + } + seekTo(Math.max(player.getCurrentPosition() - rewindMs, 0)); + } + + private void fastForward() { + if (fastForwardMs <= 0) { + return; + } + long durationMs = player.getDuration(); + long seekPositionMs = player.getCurrentPosition() + fastForwardMs; + if (durationMs != C.TIME_UNSET) { + seekPositionMs = Math.min(seekPositionMs, durationMs); + } + seekTo(seekPositionMs); + } + + private void seekTo(long positionMs) { + seekTo(player.getCurrentWindowIndex(), positionMs); + } + + private void seekTo(int windowIndex, long positionMs) { + boolean dispatched = controlDispatcher.dispatchSeekTo(player, windowIndex, positionMs); + if (!dispatched) { + // The seek wasn't dispatched. If the progress bar was dragged by the user to perform the + // seek then it'll now be in the wrong position. Trigger a progress update to snap it back. + updateProgress(); + } + } + + private void seekToTimeBarPosition(long positionMs) { + int windowIndex; + Timeline timeline = player.getCurrentTimeline(); + if (multiWindowTimeBar && !timeline.isEmpty()) { + int windowCount = timeline.getWindowCount(); + windowIndex = 0; + while (true) { + long windowDurationMs = timeline.getWindow(windowIndex, window).getDurationMs(); + if (positionMs < windowDurationMs) { + break; + } else if (windowIndex == windowCount - 1) { + // Seeking past the end of the last window should seek to the end of the timeline. + positionMs = windowDurationMs; + break; + } + positionMs -= windowDurationMs; + windowIndex++; + } + } else { + windowIndex = player.getCurrentWindowIndex(); + } + seekTo(windowIndex, positionMs); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + isAttachedToWindow = true; + if (hideAtMs != C.TIME_UNSET) { + long delayMs = hideAtMs - SystemClock.uptimeMillis(); + if (delayMs <= 0) { + hide(); + } else { + postDelayed(hideAction, delayMs); + } + } + updateAll(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + isAttachedToWindow = false; + removeCallbacks(updateProgressAction); + removeCallbacks(hideAction); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + return dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + if (player == null || !isHandledMediaKey(keyCode)) { + return false; + } + if (event.getAction() == KeyEvent.ACTION_DOWN) { + if (keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD) { + fastForward(); + } else if (keyCode == KeyEvent.KEYCODE_MEDIA_REWIND) { + rewind(); + } else if (event.getRepeatCount() == 0) { + switch (keyCode) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, !player.getPlayWhenReady()); + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + controlDispatcher.dispatchSetPlayWhenReady(player, true); + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + controlDispatcher.dispatchSetPlayWhenReady(player, false); + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + next(); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + previous(); + break; + default: + break; + } + } + } + return true; + } + + @SuppressLint("InlinedApi") + private static boolean isHandledMediaKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_MEDIA_FAST_FORWARD + || keyCode == KeyEvent.KEYCODE_MEDIA_REWIND + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY + || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE + || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT + || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS; + } + + /** + * Returns whether the specified {@code timeline} can be shown on a multi-window time bar. + * + * @param timeline The {@link Timeline} to check. + * @param window A scratch {@link Timeline.Window} instance. + * @return Whether the specified timeline can be shown on a multi-window time bar. + */ + private static boolean canShowMultiWindowTimeBar(Timeline timeline, Timeline.Window window) { + if (timeline.getWindowCount() > MAX_WINDOWS_FOR_MULTI_WINDOW_TIME_BAR) { + return false; + } + int windowCount = timeline.getWindowCount(); + for (int i = 0; i < windowCount; i++) { + if (timeline.getWindow(i, window).durationUs == C.TIME_UNSET) { + return false; + } + } + return true; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TimeBar.OnScrubListener, OnClickListener { + + @Override + public void onScrubStart(TimeBar timeBar, long position) { + removeCallbacks(hideAction); + scrubbing = true; + } + + @Override + public void onScrubMove(TimeBar timeBar, long position) { + if (positionView != null) { + positionView.setText(Util.getStringForTime(formatBuilder, formatter, position)); + } + } + + @Override + public void onScrubStop(TimeBar timeBar, long position, boolean canceled) { + scrubbing = false; + if (!canceled && player != null) { + seekToTimeBarPosition(position); + } + hideAfterTimeout(); + } + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + updatePlayPauseButton(); + updateProgress(); + } + + @Override + public void onRepeatModeChanged(int repeatMode) { + updateRepeatModeButton(); + updateNavigation(); + } + + @Override + public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) { + updateShuffleButton(); + updateNavigation(); + } + + @Override + public void onPositionDiscontinuity(@Player.DiscontinuityReason int reason) { + updateNavigation(); + updateProgress(); + } + + @Override + public void onTimelineChanged( + Timeline timeline, Object manifest, @Player.TimelineChangeReason int reason) { + updateNavigation(); + updateTimeBarMode(); + updateProgress(); + } + + @Override + public void onClick(View view) { + if (player != null) { + if (nextButton == view) { + next(); + } else if (previousButton == view) { + previous(); + } else if (fastForwardButton == view) { + fastForward(); + } else if (rewindButton == view) { + rewind(); + } else if (playButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, true); + } else if (pauseButton == view) { + controlDispatcher.dispatchSetPlayWhenReady(player, false); + } else if (repeatToggleButton == view) { + controlDispatcher.dispatchSetRepeatMode( + player, RepeatModeUtil.getNextRepeatMode(player.getRepeatMode(), repeatToggleModes)); + } else if (shuffleButton == view) { + controlDispatcher.dispatchSetShuffleModeEnabled(player, !player.getShuffleModeEnabled()); + } + } + hideAfterTimeout(); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java new file mode 100644 index 0000000000..66c197ecb7 --- /dev/null +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerView.java @@ -0,0 +1,1057 @@ +/* + * 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 com.google.android.exoplayer2.ui; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.SurfaceView; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ControlDispatcher; +import com.google.android.exoplayer2.DefaultControlDispatcher; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Player.DiscontinuityReason; +import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.metadata.id3.ApicFrame; +import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.text.Cue; +import com.google.android.exoplayer2.text.TextOutput; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.trackselection.TrackSelectionArray; +import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; +import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.RepeatModeUtil; +import com.google.android.exoplayer2.util.Util; +import com.google.android.exoplayer2.video.VideoListener; +import java.util.List; + +/** + * A high level view for {@link Player} media playbacks. It displays video, subtitles and album art + * during playback, and displays playback controls using a {@link PlayerControlView}. + * + *

A PlayerView can be customized by setting attributes (or calling corresponding methods), + * overriding the view's layout file or by specifying a custom view layout file, as outlined below. + * + *

Attributes

+ * + * The following attributes can be set on a PlayerView when used in a layout XML file: + * + *
    + *
  • {@code use_artwork} - Whether artwork is used if available in audio streams. + *
      + *
    • Corresponding method: {@link #setUseArtwork(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio + * streams. + *
      + *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)} + *
    • Default: {@code null} + *
    + *
  • {@code use_controller} - Whether the playback controls can be shown. + *
      + *
    • Corresponding method: {@link #setUseController(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. + *
      + *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code auto_show} - Whether the playback controls are automatically shown when + * playback starts, pauses, ends, or fails. If set to false, the playback controls can be + * manually operated with {@link #showController()} and {@link #hideController()}. + *
      + *
    • Corresponding method: {@link #setControllerAutoShow(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code hide_during_ads} - Whether the playback controls are hidden during ads. + * Controls are always shown during ads if they are enabled and the player is paused. + *
      + *
    • Corresponding method: {@link #setControllerHideDuringAds(boolean)} + *
    • Default: {@code true} + *
    + *
  • {@code resize_mode} - Controls how video and album art is resized within the view. + * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. + *
      + *
    • Corresponding method: {@link #setResizeMode(int)} + *
    • Default: {@code fit} + *
    + *
  • {@code surface_type} - The type of surface view used for video playbacks. Valid + * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} + * is recommended for audio only applications, since creating the surface can be expensive. + * Using {@code surface_view} is recommended for video applications. + *
      + *
    • Corresponding method: None + *
    • Default: {@code surface_view} + *
    + *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} + * view. + *
      + *
    • Corresponding method: {@link #setShutterBackgroundColor(int)} + *
    • Default: {@code unset} + *
    + *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below + * for more details. + *
      + *
    • Corresponding method: None + *
    • Default: {@code R.id.exo_player_view} + *
    + *
  • {@code controller_layout_id} - Specifies the id of the layout resource to be + * inflated by the child {@link PlayerControlView}. See below for more details. + *
      + *
    • Corresponding method: None + *
    • Default: {@code R.id.exo_player_control_view} + *
    + *
  • All attributes that can be set on a {@link PlayerControlView} can also be set on a + * PlayerView, and will be propagated to the inflated {@link PlayerControlView} unless the + * layout is overridden to specify a custom {@code exo_controller} (see below). + *
+ * + *

Overriding the layout file

+ * + * To customize the layout of PlayerView throughout your app, or just for certain configurations, + * you can define {@code exo_player_view.xml} layout files in your application {@code res/layout*} + * directories. These layouts will override the one provided by the ExoPlayer library, and will be + * inflated for use by PlayerView. The view identifies and binds its children by looking for the + * following ids: + * + *

+ * + *

    + *
  • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video + * or album art of the media being played, and the configured {@code resize_mode}. The video + * surface view is inflated into this frame as its first child. + *
      + *
    • Type: {@link AspectRatioFrameLayout} + *
    + *
  • {@code exo_shutter} - A view that's made visible when video should be hidden. This + * view is typically an opaque view that covers the video surface view, thereby obscuring it + * when visible. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_subtitles} - Displays subtitles. + *
      + *
    • Type: {@link SubtitleView} + *
    + *
  • {@code exo_artwork} - Displays album art. + *
      + *
    • Type: {@link ImageView} + *
    + *
  • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated + * {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists. + *
      + *
    • Type: {@link View} + *
    + *
  • {@code exo_controller} - An already inflated {@link PlayerControlView}. Allows use + * of a custom extension of {@link PlayerControlView}. Note that attributes such as {@code + * rewind_increment} will not be automatically propagated through to this instance. If a view + * exists with this id, any {@code exo_controller_placeholder} view will be ignored. + *
      + *
    • Type: {@link PlayerControlView} + *
    + *
  • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which + * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. + *
      + *
    • Type: {@link FrameLayout} + *
    + *
+ * + *

All child views are optional and so can be omitted if not required, however where defined they + * must be of the expected type. + * + *

Specifying a custom layout file

+ * + * Defining your own {@code exo_player_view.xml} is useful to customize the layout of PlayerView + * throughout your application. It's also possible to customize the layout for a single instance in + * a layout file. This is achieved by setting the {@code player_layout_id} attribute on a + * PlayerView. This will cause the specified layout to be inflated instead of {@code + * exo_player_view.xml} for only the instance on which the attribute is set. + */ +public class PlayerView extends FrameLayout { + + private static final int SURFACE_TYPE_NONE = 0; + private static final int SURFACE_TYPE_SURFACE_VIEW = 1; + private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; + + private final AspectRatioFrameLayout contentFrame; + private final View shutterView; + private final View surfaceView; + private final ImageView artworkView; + private final SubtitleView subtitleView; + private final PlayerControlView controller; + private final ComponentListener componentListener; + private final FrameLayout overlayFrameLayout; + + private Player player; + private boolean useController; + private boolean useArtwork; + private Bitmap defaultArtwork; + private int controllerShowTimeoutMs; + private boolean controllerAutoShow; + private boolean controllerHideDuringAds; + private boolean controllerHideOnTouch; + private int textureViewRotation; + + public PlayerView(Context context) { + this(context, null); + } + + public PlayerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public PlayerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + if (isInEditMode()) { + contentFrame = null; + shutterView = null; + surfaceView = null; + artworkView = null; + subtitleView = null; + controller = null; + componentListener = null; + overlayFrameLayout = null; + ImageView logo = new ImageView(context); + if (Util.SDK_INT >= 23) { + configureEditModeLogoV23(getResources(), logo); + } else { + configureEditModeLogo(getResources(), logo); + } + addView(logo); + return; + } + + boolean shutterColorSet = false; + int shutterColor = 0; + int playerLayoutId = R.layout.exo_player_view; + boolean useArtwork = true; + int defaultArtworkId = 0; + boolean useController = true; + int surfaceType = SURFACE_TYPE_SURFACE_VIEW; + int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; + int controllerShowTimeoutMs = PlayerControlView.DEFAULT_SHOW_TIMEOUT_MS; + boolean controllerHideOnTouch = true; + boolean controllerAutoShow = true; + boolean controllerHideDuringAds = true; + if (attrs != null) { + TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.PlayerView, 0, 0); + try { + shutterColorSet = a.hasValue(R.styleable.PlayerView_shutter_background_color); + shutterColor = a.getColor(R.styleable.PlayerView_shutter_background_color, shutterColor); + playerLayoutId = a.getResourceId(R.styleable.PlayerView_player_layout_id, playerLayoutId); + useArtwork = a.getBoolean(R.styleable.PlayerView_use_artwork, useArtwork); + defaultArtworkId = + a.getResourceId(R.styleable.PlayerView_default_artwork, defaultArtworkId); + useController = a.getBoolean(R.styleable.PlayerView_use_controller, useController); + surfaceType = a.getInt(R.styleable.PlayerView_surface_type, surfaceType); + resizeMode = a.getInt(R.styleable.PlayerView_resize_mode, resizeMode); + controllerShowTimeoutMs = + a.getInt(R.styleable.PlayerView_show_timeout, controllerShowTimeoutMs); + controllerHideOnTouch = + a.getBoolean(R.styleable.PlayerView_hide_on_touch, controllerHideOnTouch); + controllerAutoShow = a.getBoolean(R.styleable.PlayerView_auto_show, controllerAutoShow); + controllerHideDuringAds = + a.getBoolean(R.styleable.PlayerView_hide_during_ads, controllerHideDuringAds); + } finally { + a.recycle(); + } + } + + LayoutInflater.from(context).inflate(playerLayoutId, this); + componentListener = new ComponentListener(); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + + // Content frame. + contentFrame = findViewById(R.id.exo_content_frame); + if (contentFrame != null) { + setResizeModeRaw(contentFrame, resizeMode); + } + + // Shutter view. + shutterView = findViewById(R.id.exo_shutter); + if (shutterView != null && shutterColorSet) { + shutterView.setBackgroundColor(shutterColor); + } + + // Create a surface view and insert it into the content frame, if there is one. + if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + surfaceView = + surfaceType == SURFACE_TYPE_TEXTURE_VIEW + ? new TextureView(context) + : new SurfaceView(context); + surfaceView.setLayoutParams(params); + contentFrame.addView(surfaceView, 0); + } else { + surfaceView = null; + } + + // Overlay frame layout. + overlayFrameLayout = findViewById(R.id.exo_overlay); + + // Artwork view. + artworkView = findViewById(R.id.exo_artwork); + this.useArtwork = useArtwork && artworkView != null; + if (defaultArtworkId != 0) { + defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); + } + + // Subtitle view. + subtitleView = findViewById(R.id.exo_subtitles); + if (subtitleView != null) { + subtitleView.setUserDefaultStyle(); + subtitleView.setUserDefaultTextSize(); + } + + // Playback control view. + PlayerControlView customController = findViewById(R.id.exo_controller); + View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); + if (customController != null) { + this.controller = customController; + } else if (controllerPlaceholder != null) { + // Propagate attrs as playbackAttrs so that PlayerControlView's custom attributes are + // transferred, but standard FrameLayout attributes (e.g. background) are not. + this.controller = new PlayerControlView(context, null, 0, attrs); + controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); + ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); + int controllerIndex = parent.indexOfChild(controllerPlaceholder); + parent.removeView(controllerPlaceholder); + parent.addView(controller, controllerIndex); + } else { + this.controller = null; + } + this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; + this.controllerHideOnTouch = controllerHideOnTouch; + this.controllerAutoShow = controllerAutoShow; + this.controllerHideDuringAds = controllerHideDuringAds; + this.useController = useController && controller != null; + hideController(); + } + + /** + * Switches the view targeted by a given {@link Player}. + * + * @param player The player whose target view is being switched. + * @param oldPlayerView The old view to detach from the player. + * @param newPlayerView The new view to attach to the player. + */ + public static void switchTargetView( + @NonNull Player player, + @Nullable PlayerView oldPlayerView, + @Nullable PlayerView newPlayerView) { + if (oldPlayerView == newPlayerView) { + return; + } + // We attach the new view before detaching the old one because this ordering allows the player + // to swap directly from one surface to another, without transitioning through a state where no + // surface is attached. This is significantly more efficient and achieves a more seamless + // transition when using platform provided video decoders. + if (newPlayerView != null) { + newPlayerView.setPlayer(player); + } + if (oldPlayerView != null) { + oldPlayerView.setPlayer(null); + } + } + + /** Returns the player currently set on this view, or null if no player is set. */ + public Player getPlayer() { + return player; + } + + /** + * Set the {@link Player} to use. + * + *

To transition a {@link Player} from targeting one view to another, it's recommended to use + * {@link #switchTargetView(Player, PlayerView, PlayerView)} rather than this method. If you do + * wish to use this method directly, be sure to attach the player to the new view before + * calling {@code setPlayer(null)} to detach it from the old one. This ordering is significantly + * more efficient and may allow for more seamless transitions. + * + * @param player The {@link Player} to use. + */ + public void setPlayer(Player player) { + if (this.player == player) { + return; + } + if (this.player != null) { + this.player.removeListener(componentListener); + Player.VideoComponent oldVideoComponent = this.player.getVideoComponent(); + if (oldVideoComponent != null) { + oldVideoComponent.removeVideoListener(componentListener); + if (surfaceView instanceof TextureView) { + oldVideoComponent.clearVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + oldVideoComponent.clearVideoSurfaceView((SurfaceView) surfaceView); + } + } + Player.TextComponent oldTextComponent = this.player.getTextComponent(); + if (oldTextComponent != null) { + oldTextComponent.removeTextOutput(componentListener); + } + } + this.player = player; + if (useController) { + controller.setPlayer(player); + } + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + if (subtitleView != null) { + subtitleView.setCues(null); + } + if (player != null) { + Player.VideoComponent newVideoComponent = player.getVideoComponent(); + if (newVideoComponent != null) { + if (surfaceView instanceof TextureView) { + newVideoComponent.setVideoTextureView((TextureView) surfaceView); + } else if (surfaceView instanceof SurfaceView) { + newVideoComponent.setVideoSurfaceView((SurfaceView) surfaceView); + } + newVideoComponent.addVideoListener(componentListener); + } + Player.TextComponent newTextComponent = player.getTextComponent(); + if (newTextComponent != null) { + newTextComponent.addTextOutput(componentListener); + } + player.addListener(componentListener); + maybeShowController(false); + updateForCurrentTrackSelections(); + } else { + hideController(); + hideArtwork(); + } + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + if (surfaceView instanceof SurfaceView) { + // Work around https://github.com/google/ExoPlayer/issues/3160. + surfaceView.setVisibility(visibility); + } + } + + /** + * Sets the resize mode. + * + * @param resizeMode The resize mode. + */ + public void setResizeMode(@ResizeMode int resizeMode) { + Assertions.checkState(contentFrame != null); + contentFrame.setResizeMode(resizeMode); + } + + /** Returns whether artwork is displayed if present in the media. */ + public boolean getUseArtwork() { + return useArtwork; + } + + /** + * Sets whether artwork is displayed if present in the media. + * + * @param useArtwork Whether artwork is displayed. + */ + public void setUseArtwork(boolean useArtwork) { + Assertions.checkState(!useArtwork || artworkView != null); + if (this.useArtwork != useArtwork) { + this.useArtwork = useArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns the default artwork to display. */ + public Bitmap getDefaultArtwork() { + return defaultArtwork; + } + + /** + * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is + * present in the media. + * + * @param defaultArtwork the default artwork to display. + */ + public void setDefaultArtwork(Bitmap defaultArtwork) { + if (this.defaultArtwork != defaultArtwork) { + this.defaultArtwork = defaultArtwork; + updateForCurrentTrackSelections(); + } + } + + /** Returns whether the playback controls can be shown. */ + public boolean getUseController() { + return useController; + } + + /** + * Sets whether the playback controls can be shown. If set to {@code false} the playback controls + * are never visible and are disconnected from the player. + * + * @param useController Whether the playback controls can be shown. + */ + public void setUseController(boolean useController) { + Assertions.checkState(!useController || controller != null); + if (this.useController == useController) { + return; + } + this.useController = useController; + if (useController) { + controller.setPlayer(player); + } else if (controller != null) { + controller.hide(); + controller.setPlayer(null); + } + } + + /** + * Sets the background color of the {@code exo_shutter} view. + * + * @param color The background color. + */ + public void setShutterBackgroundColor(int color) { + if (shutterView != null) { + shutterView.setBackgroundColor(color); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (player != null && player.isPlayingAd()) { + // Focus any overlay UI now, in case it's provided by a WebView whose contents may update + // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using + // IMA [Internal: b/62371030]. + overlayFrameLayout.requestFocus(); + return super.dispatchKeyEvent(event); + } + boolean isDpadWhenControlHidden = + isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); + maybeShowController(true); + return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); + } + + /** + * Called to process media key events. Any {@link KeyEvent} can be passed but only media key + * events will be handled. Does nothing if playback controls are disabled. + * + * @param event A key event. + * @return Whether the key event was handled. + */ + public boolean dispatchMediaKeyEvent(KeyEvent event) { + return useController && controller.dispatchMediaKeyEvent(event); + } + + /** + * Shows the playback controls. Does nothing if playback controls are disabled. + * + *

The playback controls are automatically hidden during playback after {{@link + * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, + * is paused, has ended or failed. + */ + public void showController() { + showController(shouldShowControllerIndefinitely()); + } + + /** Hides the playback controls. Does nothing if playback controls are disabled. */ + public void hideController() { + if (controller != null) { + controller.hide(); + } + } + + /** + * Returns the playback controls timeout. The playback controls are automatically hidden after + * this duration of time has elapsed without user input and with playback or buffering in + * progress. + * + * @return The timeout in milliseconds. A non-positive value will cause the controller to remain + * visible indefinitely. + */ + public int getControllerShowTimeoutMs() { + return controllerShowTimeoutMs; + } + + /** + * Sets the playback controls timeout. The playback controls are automatically hidden after this + * duration of time has elapsed without user input and with playback or buffering in progress. + * + * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the + * controller to remain visible indefinitely. + */ + public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { + Assertions.checkState(controller != null); + this.controllerShowTimeoutMs = controllerShowTimeoutMs; + if (controller.isVisible()) { + // Update the controller's timeout if necessary. + showController(); + } + } + + /** Returns whether the playback controls are hidden by touch events. */ + public boolean getControllerHideOnTouch() { + return controllerHideOnTouch; + } + + /** + * Sets whether the playback controls are hidden by touch events. + * + * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. + */ + public void setControllerHideOnTouch(boolean controllerHideOnTouch) { + Assertions.checkState(controller != null); + this.controllerHideOnTouch = controllerHideOnTouch; + } + + /** + * Returns whether the playback controls are automatically shown when playback starts, pauses, + * ends, or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + */ + public boolean getControllerAutoShow() { + return controllerAutoShow; + } + + /** + * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, + * or fails. If set to false, the playback controls can be manually operated with {@link + * #showController()} and {@link #hideController()}. + * + * @param controllerAutoShow Whether the playback controls are allowed to show automatically. + */ + public void setControllerAutoShow(boolean controllerAutoShow) { + this.controllerAutoShow = controllerAutoShow; + } + + /** + * Sets whether the playback controls are hidden when ads are playing. Controls are always shown + * during ads if they are enabled and the player is paused. + * + * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. + */ + public void setControllerHideDuringAds(boolean controllerHideDuringAds) { + this.controllerHideDuringAds = controllerHideDuringAds; + } + + /** + * Set the {@link PlayerControlView.VisibilityListener}. + * + * @param listener The listener to be notified about visibility changes. + */ + public void setControllerVisibilityListener(PlayerControlView.VisibilityListener listener) { + Assertions.checkState(controller != null); + controller.setVisibilityListener(listener); + } + + /** + * Sets the {@link ControlDispatcher}. + * + * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link + * DefaultControlDispatcher}. + */ + public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { + Assertions.checkState(controller != null); + controller.setControlDispatcher(controlDispatcher); + } + + /** + * Sets the rewind increment in milliseconds. + * + * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the + * rewind button to be disabled. + */ + public void setRewindIncrementMs(int rewindMs) { + Assertions.checkState(controller != null); + controller.setRewindIncrementMs(rewindMs); + } + + /** + * Sets the fast forward increment in milliseconds. + * + * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will + * cause the fast forward button to be disabled. + */ + public void setFastForwardIncrementMs(int fastForwardMs) { + Assertions.checkState(controller != null); + controller.setFastForwardIncrementMs(fastForwardMs); + } + + /** + * Sets which repeat toggle modes are enabled. + * + * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. + */ + public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { + Assertions.checkState(controller != null); + controller.setRepeatToggleModes(repeatToggleModes); + } + + /** + * Sets whether the shuffle button is shown. + * + * @param showShuffleButton Whether the shuffle button is shown. + */ + public void setShowShuffleButton(boolean showShuffleButton) { + Assertions.checkState(controller != null); + controller.setShowShuffleButton(showShuffleButton); + } + + /** + * Sets whether the time bar should show all windows, as opposed to just the current one. + * + * @param showMultiWindowTimeBar Whether to show all windows. + */ + public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { + Assertions.checkState(controller != null); + controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); + } + + /** + * Gets the view onto which video is rendered. This is a: + * + *

    + *
  • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code + * surface_view}. + *
  • {@link TextureView} if {@code surface_type} is {@code texture_view}. + *
  • {@code null} if {@code surface_type} is {@code none}. + *
+ * + * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. + */ + public View getVideoSurfaceView() { + return surfaceView; + } + + /** + * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of + * the player. + * + * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and + * the overlay is not present. + */ + public FrameLayout getOverlayFrameLayout() { + return overlayFrameLayout; + } + + /** + * Gets the {@link SubtitleView}. + * + * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the + * subtitle view is not present. + */ + public SubtitleView getSubtitleView() { + return subtitleView; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { + return false; + } + if (!controller.isVisible()) { + maybeShowController(true); + } else if (controllerHideOnTouch) { + controller.hide(); + } + return true; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + if (!useController || player == null) { + return false; + } + maybeShowController(true); + return true; + } + + /** Shows the playback controls, but only if forced or shown indefinitely. */ + private void maybeShowController(boolean isForced) { + if (isPlayingAd() && controllerHideDuringAds) { + return; + } + if (useController) { + boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; + boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); + if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { + showController(shouldShowIndefinitely); + } + } + } + + private boolean shouldShowControllerIndefinitely() { + if (player == null) { + return true; + } + int playbackState = player.getPlaybackState(); + return controllerAutoShow + && (playbackState == Player.STATE_IDLE + || playbackState == Player.STATE_ENDED + || !player.getPlayWhenReady()); + } + + private void showController(boolean showIndefinitely) { + if (!useController) { + return; + } + controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); + controller.show(); + } + + private boolean isPlayingAd() { + return player != null && player.isPlayingAd() && player.getPlayWhenReady(); + } + + private void updateForCurrentTrackSelections() { + if (player == null) { + return; + } + TrackSelectionArray selections = player.getCurrentTrackSelections(); + for (int i = 0; i < selections.length; i++) { + if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { + // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in + // onRenderedFirstFrame(). + hideArtwork(); + return; + } + } + // Video disabled so the shutter must be closed. + if (shutterView != null) { + shutterView.setVisibility(VISIBLE); + } + // Display artwork if enabled and available, else hide it. + if (useArtwork) { + for (int i = 0; i < selections.length; i++) { + TrackSelection selection = selections.get(i); + if (selection != null) { + for (int j = 0; j < selection.length(); j++) { + Metadata metadata = selection.getFormat(j).metadata; + if (metadata != null && setArtworkFromMetadata(metadata)) { + return; + } + } + } + } + if (setArtworkFromBitmap(defaultArtwork)) { + return; + } + } + // Artwork disabled or unavailable. + hideArtwork(); + } + + private boolean setArtworkFromMetadata(Metadata metadata) { + for (int i = 0; i < metadata.length(); i++) { + Metadata.Entry metadataEntry = metadata.get(i); + if (metadataEntry instanceof ApicFrame) { + byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; + Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); + return setArtworkFromBitmap(bitmap); + } + } + return false; + } + + private boolean setArtworkFromBitmap(Bitmap bitmap) { + if (bitmap != null) { + int bitmapWidth = bitmap.getWidth(); + int bitmapHeight = bitmap.getHeight(); + if (bitmapWidth > 0 && bitmapHeight > 0) { + if (contentFrame != null) { + contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); + } + artworkView.setImageBitmap(bitmap); + artworkView.setVisibility(VISIBLE); + return true; + } + } + return false; + } + + private void hideArtwork() { + if (artworkView != null) { + artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. + artworkView.setVisibility(INVISIBLE); + } + } + + @TargetApi(23) + private static void configureEditModeLogoV23(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); + } + + @SuppressWarnings("deprecation") + private static void configureEditModeLogo(Resources resources, ImageView logo) { + logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); + logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); + } + + @SuppressWarnings("ResourceType") + private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { + aspectRatioFrame.setResizeMode(resizeMode); + } + + /** Applies a texture rotation to a {@link TextureView}. */ + private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { + float textureViewWidth = textureView.getWidth(); + float textureViewHeight = textureView.getHeight(); + if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { + textureView.setTransform(null); + } else { + Matrix transformMatrix = new Matrix(); + float pivotX = textureViewWidth / 2; + float pivotY = textureViewHeight / 2; + transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); + + // After rotation, scale the rotated texture to fit the TextureView size. + RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); + RectF rotatedTextureRect = new RectF(); + transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); + transformMatrix.postScale( + textureViewWidth / rotatedTextureRect.width(), + textureViewHeight / rotatedTextureRect.height(), + pivotX, + pivotY); + textureView.setTransform(transformMatrix); + } + } + + @SuppressLint("InlinedApi") + private boolean isDpadKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; + } + + private final class ComponentListener extends Player.DefaultEventListener + implements TextOutput, VideoListener, OnLayoutChangeListener { + + // TextOutput implementation + + @Override + public void onCues(List cues) { + if (subtitleView != null) { + subtitleView.onCues(cues); + } + } + + // VideoListener implementation + + @Override + public void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { + if (contentFrame == null) { + return; + } + float videoAspectRatio = + (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; + + if (surfaceView instanceof TextureView) { + // Try to apply rotation transformation when our surface is a TextureView. + if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { + // We will apply a rotation 90/270 degree to the output texture of the TextureView. + // In this case, the output video's width and height will be swapped. + videoAspectRatio = 1 / videoAspectRatio; + } + if (textureViewRotation != 0) { + surfaceView.removeOnLayoutChangeListener(this); + } + textureViewRotation = unappliedRotationDegrees; + if (textureViewRotation != 0) { + // The texture view's dimensions might be changed after layout step. + // So add an OnLayoutChangeListener to apply rotation after layout step. + surfaceView.addOnLayoutChangeListener(this); + } + applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); + } + + contentFrame.setAspectRatio(videoAspectRatio); + } + + @Override + public void onRenderedFirstFrame() { + if (shutterView != null) { + shutterView.setVisibility(INVISIBLE); + } + } + + @Override + public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { + updateForCurrentTrackSelections(); + } + + // Player.EventListener implementation + + @Override + public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } else { + maybeShowController(false); + } + } + + @Override + public void onPositionDiscontinuity(@DiscontinuityReason int reason) { + if (isPlayingAd() && controllerHideDuringAds) { + hideController(); + } + } + + // OnLayoutChangeListener implementation + + @Override + public void onLayoutChange( + View view, + int left, + int top, + int right, + int bottom, + int oldLeft, + int oldTop, + int oldRight, + int oldBottom) { + applyTextureViewRotation((TextureView) view, textureViewRotation); + } + } +} diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java index 6e69a31fd9..b8098b6fa7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SimpleExoPlayerView.java @@ -15,360 +15,28 @@ */ package com.google.android.exoplayer2.ui; -import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; -import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Matrix; -import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.AttributeSet; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.SurfaceView; -import android.view.TextureView; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.ImageView; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ControlDispatcher; -import com.google.android.exoplayer2.DefaultControlDispatcher; -import com.google.android.exoplayer2.Player; -import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.metadata.Metadata; -import com.google.android.exoplayer2.metadata.id3.ApicFrame; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.text.Cue; -import com.google.android.exoplayer2.text.TextOutput; -import com.google.android.exoplayer2.trackselection.TrackSelection; -import com.google.android.exoplayer2.trackselection.TrackSelectionArray; -import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.ResizeMode; -import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.RepeatModeUtil; -import com.google.android.exoplayer2.util.Util; -import java.util.List; -/** - * A high level view for {@link SimpleExoPlayer} media playbacks. It displays video, subtitles and - * album art during playback, and displays playback controls using a {@link PlaybackControlView}. - * - *

A SimpleExoPlayerView can be customized by setting attributes (or calling corresponding - * methods), overriding the view's layout file or by specifying a custom view layout file, as - * outlined below. - * - *

Attributes

- * - * The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file: - * - *

- * - *

    - *
  • {@code use_artwork} - Whether artwork is used if available in audio streams. - *
      - *
    • Corresponding method: {@link #setUseArtwork(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code default_artwork} - Default artwork to use if no artwork available in audio - * streams. - *
      - *
    • Corresponding method: {@link #setDefaultArtwork(Bitmap)} - *
    • Default: {@code null} - *
    - *
  • {@code use_controller} - Whether the playback controls can be shown. - *
      - *
    • Corresponding method: {@link #setUseController(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code hide_on_touch} - Whether the playback controls are hidden by touch events. - *
      - *
    • Corresponding method: {@link #setControllerHideOnTouch(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code auto_show} - Whether the playback controls are automatically shown when - * playback starts, pauses, ends, or fails. If set to false, the playback controls can be - * manually operated with {@link #showController()} and {@link #hideController()}. - *
      - *
    • Corresponding method: {@link #setControllerAutoShow(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code hide_during_ads} - Whether the playback controls are hidden during ads. - * Controls are always shown during ads if they are enabled and the player is paused. - *
      - *
    • Corresponding method: {@link #setControllerHideDuringAds(boolean)} - *
    • Default: {@code true} - *
    - *
  • {@code resize_mode} - Controls how video and album art is resized within the view. - * Valid values are {@code fit}, {@code fixed_width}, {@code fixed_height} and {@code fill}. - *
      - *
    • Corresponding method: {@link #setResizeMode(int)} - *
    • Default: {@code fit} - *
    - *
  • {@code surface_type} - The type of surface view used for video playbacks. Valid - * values are {@code surface_view}, {@code texture_view} and {@code none}. Using {@code none} - * is recommended for audio only applications, since creating the surface can be expensive. - * Using {@code surface_view} is recommended for video applications. - *
      - *
    • Corresponding method: None - *
    • Default: {@code surface_view} - *
    - *
  • {@code shutter_background_color} - The background color of the {@code exo_shutter} - * view. - *
      - *
    • Corresponding method: {@link #setShutterBackgroundColor(int)} - *
    • Default: {@code unset} - *
    - *
  • {@code player_layout_id} - Specifies the id of the layout to be inflated. See below - * for more details. - *
      - *
    • Corresponding method: None - *
    • Default: {@code R.id.exo_simple_player_view} - *
    - *
  • {@code controller_layout_id} - Specifies the id of the layout resource to be - * inflated by the child {@link PlaybackControlView}. See below for more details. - *
      - *
    • Corresponding method: None - *
    • Default: {@code R.id.exo_playback_control_view} - *
    - *
  • All attributes that can be set on a {@link PlaybackControlView} can also be set on a - * SimpleExoPlayerView, and will be propagated to the inflated {@link PlaybackControlView} - * unless the layout is overridden to specify a custom {@code exo_controller} (see below). - *
- * - *

Overriding the layout file

- * - * To customize the layout of SimpleExoPlayerView throughout your app, or just for certain - * configurations, you can define {@code exo_simple_player_view.xml} layout files in your - * application {@code res/layout*} directories. These layouts will override the one provided by the - * ExoPlayer library, and will be inflated for use by SimpleExoPlayerView. The view identifies and - * binds its children by looking for the following ids: - * - *

- * - *

    - *
  • {@code exo_content_frame} - A frame whose aspect ratio is resized based on the video - * or album art of the media being played, and the configured {@code resize_mode}. The video - * surface view is inflated into this frame as its first child. - *
      - *
    • Type: {@link AspectRatioFrameLayout} - *
    - *
  • {@code exo_shutter} - A view that's made visible when video should be hidden. This - * view is typically an opaque view that covers the video surface view, thereby obscuring it - * when visible. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_subtitles} - Displays subtitles. - *
      - *
    • Type: {@link SubtitleView} - *
    - *
  • {@code exo_artwork} - Displays album art. - *
      - *
    • Type: {@link ImageView} - *
    - *
  • {@code exo_controller_placeholder} - A placeholder that's replaced with the inflated - * {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists. - *
      - *
    • Type: {@link View} - *
    - *
  • {@code exo_controller} - An already inflated {@link PlaybackControlView}. Allows use - * of a custom extension of {@link PlaybackControlView}. Note that attributes such as {@code - * rewind_increment} will not be automatically propagated through to this instance. If a view - * exists with this id, any {@code exo_controller_placeholder} view will be ignored. - *
      - *
    • Type: {@link PlaybackControlView} - *
    - *
  • {@code exo_overlay} - A {@link FrameLayout} positioned on top of the player which - * the app can access via {@link #getOverlayFrameLayout()}, provided for convenience. - *
      - *
    • Type: {@link FrameLayout} - *
    - *
- * - *

All child views are optional and so can be omitted if not required, however where defined they - * must be of the expected type. - * - *

Specifying a custom layout file

- * - * Defining your own {@code exo_simple_player_view.xml} is useful to customize the layout of - * SimpleExoPlayerView throughout your application. It's also possible to customize the layout for a - * single instance in a layout file. This is achieved by setting the {@code player_layout_id} - * attribute on a SimpleExoPlayerView. This will cause the specified layout to be inflated instead - * of {@code exo_simple_player_view.xml} for only the instance on which the attribute is set. - */ +/** @deprecated Use {@link PlayerView}. */ +@Deprecated @TargetApi(16) -public final class SimpleExoPlayerView extends FrameLayout { - - private static final int SURFACE_TYPE_NONE = 0; - private static final int SURFACE_TYPE_SURFACE_VIEW = 1; - private static final int SURFACE_TYPE_TEXTURE_VIEW = 2; - - private final AspectRatioFrameLayout contentFrame; - private final View shutterView; - private final View surfaceView; - private final ImageView artworkView; - private final SubtitleView subtitleView; - private final PlaybackControlView controller; - private final ComponentListener componentListener; - private final FrameLayout overlayFrameLayout; - - private SimpleExoPlayer player; - private boolean useController; - private boolean useArtwork; - private Bitmap defaultArtwork; - private int controllerShowTimeoutMs; - private boolean controllerAutoShow; - private boolean controllerHideDuringAds; - private boolean controllerHideOnTouch; - private int textureViewRotation; +public final class SimpleExoPlayerView extends PlayerView { public SimpleExoPlayerView(Context context) { - this(context, null); + super(context); } public SimpleExoPlayerView(Context context, AttributeSet attrs) { - this(context, attrs, 0); + super(context, attrs); } public SimpleExoPlayerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - - if (isInEditMode()) { - contentFrame = null; - shutterView = null; - surfaceView = null; - artworkView = null; - subtitleView = null; - controller = null; - componentListener = null; - overlayFrameLayout = null; - ImageView logo = new ImageView(context); - if (Util.SDK_INT >= 23) { - configureEditModeLogoV23(getResources(), logo); - } else { - configureEditModeLogo(getResources(), logo); - } - addView(logo); - return; - } - - boolean shutterColorSet = false; - int shutterColor = 0; - int playerLayoutId = R.layout.exo_simple_player_view; - boolean useArtwork = true; - int defaultArtworkId = 0; - boolean useController = true; - int surfaceType = SURFACE_TYPE_SURFACE_VIEW; - int resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT; - int controllerShowTimeoutMs = PlaybackControlView.DEFAULT_SHOW_TIMEOUT_MS; - boolean controllerHideOnTouch = true; - boolean controllerAutoShow = true; - boolean controllerHideDuringAds = true; - if (attrs != null) { - TypedArray a = - context.getTheme().obtainStyledAttributes(attrs, R.styleable.SimpleExoPlayerView, 0, 0); - try { - shutterColorSet = a.hasValue(R.styleable.SimpleExoPlayerView_shutter_background_color); - shutterColor = - a.getColor(R.styleable.SimpleExoPlayerView_shutter_background_color, shutterColor); - playerLayoutId = - a.getResourceId(R.styleable.SimpleExoPlayerView_player_layout_id, playerLayoutId); - useArtwork = a.getBoolean(R.styleable.SimpleExoPlayerView_use_artwork, useArtwork); - defaultArtworkId = - a.getResourceId(R.styleable.SimpleExoPlayerView_default_artwork, defaultArtworkId); - useController = a.getBoolean(R.styleable.SimpleExoPlayerView_use_controller, useController); - surfaceType = a.getInt(R.styleable.SimpleExoPlayerView_surface_type, surfaceType); - resizeMode = a.getInt(R.styleable.SimpleExoPlayerView_resize_mode, resizeMode); - controllerShowTimeoutMs = - a.getInt(R.styleable.SimpleExoPlayerView_show_timeout, controllerShowTimeoutMs); - controllerHideOnTouch = - a.getBoolean(R.styleable.SimpleExoPlayerView_hide_on_touch, controllerHideOnTouch); - controllerAutoShow = - a.getBoolean(R.styleable.SimpleExoPlayerView_auto_show, controllerAutoShow); - controllerHideDuringAds = - a.getBoolean(R.styleable.SimpleExoPlayerView_hide_during_ads, controllerHideDuringAds); - } finally { - a.recycle(); - } - } - - LayoutInflater.from(context).inflate(playerLayoutId, this); - componentListener = new ComponentListener(); - setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); - - // Content frame. - contentFrame = findViewById(R.id.exo_content_frame); - if (contentFrame != null) { - setResizeModeRaw(contentFrame, resizeMode); - } - - // Shutter view. - shutterView = findViewById(R.id.exo_shutter); - if (shutterView != null && shutterColorSet) { - shutterView.setBackgroundColor(shutterColor); - } - - // Create a surface view and insert it into the content frame, if there is one. - if (contentFrame != null && surfaceType != SURFACE_TYPE_NONE) { - ViewGroup.LayoutParams params = - new ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - surfaceView = - surfaceType == SURFACE_TYPE_TEXTURE_VIEW - ? new TextureView(context) - : new SurfaceView(context); - surfaceView.setLayoutParams(params); - contentFrame.addView(surfaceView, 0); - } else { - surfaceView = null; - } - - // Overlay frame layout. - overlayFrameLayout = findViewById(R.id.exo_overlay); - - // Artwork view. - artworkView = findViewById(R.id.exo_artwork); - this.useArtwork = useArtwork && artworkView != null; - if (defaultArtworkId != 0) { - defaultArtwork = BitmapFactory.decodeResource(context.getResources(), defaultArtworkId); - } - - // Subtitle view. - subtitleView = findViewById(R.id.exo_subtitles); - if (subtitleView != null) { - subtitleView.setUserDefaultStyle(); - subtitleView.setUserDefaultTextSize(); - } - - // Playback control view. - PlaybackControlView customController = findViewById(R.id.exo_controller); - View controllerPlaceholder = findViewById(R.id.exo_controller_placeholder); - if (customController != null) { - this.controller = customController; - } else if (controllerPlaceholder != null) { - // Propagate attrs as playbackAttrs so that PlaybackControlView's custom attributes are - // transferred, but standard FrameLayout attributes (e.g. background) are not. - this.controller = new PlaybackControlView(context, null, 0, attrs); - controller.setLayoutParams(controllerPlaceholder.getLayoutParams()); - ViewGroup parent = ((ViewGroup) controllerPlaceholder.getParent()); - int controllerIndex = parent.indexOfChild(controllerPlaceholder); - parent.removeView(controllerPlaceholder); - parent.addView(controller, controllerIndex); - } else { - this.controller = null; - } - this.controllerShowTimeoutMs = controller != null ? controllerShowTimeoutMs : 0; - this.controllerHideOnTouch = controllerHideOnTouch; - this.controllerAutoShow = controllerAutoShow; - this.controllerHideDuringAds = controllerHideDuringAds; - this.useController = useController && controller != null; - hideController(); } /** @@ -382,674 +50,7 @@ public final class SimpleExoPlayerView extends FrameLayout { @NonNull SimpleExoPlayer player, @Nullable SimpleExoPlayerView oldPlayerView, @Nullable SimpleExoPlayerView newPlayerView) { - if (oldPlayerView == newPlayerView) { - return; - } - // We attach the new view before detaching the old one because this ordering allows the player - // to swap directly from one surface to another, without transitioning through a state where no - // surface is attached. This is significantly more efficient and achieves a more seamless - // transition when using platform provided video decoders. - if (newPlayerView != null) { - newPlayerView.setPlayer(player); - } - if (oldPlayerView != null) { - oldPlayerView.setPlayer(null); - } + PlayerView.switchTargetView(player, oldPlayerView, newPlayerView); } - /** Returns the player currently set on this view, or null if no player is set. */ - public SimpleExoPlayer getPlayer() { - return player; - } - - /** - * Set the {@link SimpleExoPlayer} to use. - * - *

To transition a {@link SimpleExoPlayer} from targeting one view to another, it's recommended - * to use {@link #switchTargetView(SimpleExoPlayer, SimpleExoPlayerView, SimpleExoPlayerView)} - * rather than this method. If you do wish to use this method directly, be sure to attach the - * player to the new view before calling {@code setPlayer(null)} to detach it from the - * old one. This ordering is significantly more efficient and may allow for more seamless - * transitions. - * - * @param player The {@link SimpleExoPlayer} to use. - */ - public void setPlayer(SimpleExoPlayer player) { - if (this.player == player) { - return; - } - if (this.player != null) { - this.player.removeListener(componentListener); - this.player.removeTextOutput(componentListener); - this.player.removeVideoListener(componentListener); - if (surfaceView instanceof TextureView) { - this.player.clearVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - this.player.clearVideoSurfaceView((SurfaceView) surfaceView); - } - } - this.player = player; - if (useController) { - controller.setPlayer(player); - } - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - if (subtitleView != null) { - subtitleView.setCues(null); - } - if (player != null) { - if (surfaceView instanceof TextureView) { - player.setVideoTextureView((TextureView) surfaceView); - } else if (surfaceView instanceof SurfaceView) { - player.setVideoSurfaceView((SurfaceView) surfaceView); - } - player.addVideoListener(componentListener); - player.addTextOutput(componentListener); - player.addListener(componentListener); - maybeShowController(false); - updateForCurrentTrackSelections(); - } else { - hideController(); - hideArtwork(); - } - } - - @Override - public void setVisibility(int visibility) { - super.setVisibility(visibility); - if (surfaceView instanceof SurfaceView) { - // Work around https://github.com/google/ExoPlayer/issues/3160. - surfaceView.setVisibility(visibility); - } - } - - /** - * Sets the resize mode. - * - * @param resizeMode The resize mode. - */ - public void setResizeMode(@ResizeMode int resizeMode) { - Assertions.checkState(contentFrame != null); - contentFrame.setResizeMode(resizeMode); - } - - /** Returns whether artwork is displayed if present in the media. */ - public boolean getUseArtwork() { - return useArtwork; - } - - /** - * Sets whether artwork is displayed if present in the media. - * - * @param useArtwork Whether artwork is displayed. - */ - public void setUseArtwork(boolean useArtwork) { - Assertions.checkState(!useArtwork || artworkView != null); - if (this.useArtwork != useArtwork) { - this.useArtwork = useArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns the default artwork to display. */ - public Bitmap getDefaultArtwork() { - return defaultArtwork; - } - - /** - * Sets the default artwork to display if {@code useArtwork} is {@code true} and no artwork is - * present in the media. - * - * @param defaultArtwork the default artwork to display. - */ - public void setDefaultArtwork(Bitmap defaultArtwork) { - if (this.defaultArtwork != defaultArtwork) { - this.defaultArtwork = defaultArtwork; - updateForCurrentTrackSelections(); - } - } - - /** Returns whether the playback controls can be shown. */ - public boolean getUseController() { - return useController; - } - - /** - * Sets whether the playback controls can be shown. If set to {@code false} the playback controls - * are never visible and are disconnected from the player. - * - * @param useController Whether the playback controls can be shown. - */ - public void setUseController(boolean useController) { - Assertions.checkState(!useController || controller != null); - if (this.useController == useController) { - return; - } - this.useController = useController; - if (useController) { - controller.setPlayer(player); - } else if (controller != null) { - controller.hide(); - controller.setPlayer(null); - } - } - - /** - * Sets the background color of the {@code exo_shutter} view. - * - * @param color The background color. - */ - public void setShutterBackgroundColor(int color) { - if (shutterView != null) { - shutterView.setBackgroundColor(color); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - if (player != null && player.isPlayingAd()) { - // Focus any overlay UI now, in case it's provided by a WebView whose contents may update - // dynamically. This is needed to make the "Skip ad" button focused on Android TV when using - // IMA [Internal: b/62371030]. - overlayFrameLayout.requestFocus(); - return super.dispatchKeyEvent(event); - } - boolean isDpadWhenControlHidden = - isDpadKey(event.getKeyCode()) && useController && !controller.isVisible(); - maybeShowController(true); - return isDpadWhenControlHidden || dispatchMediaKeyEvent(event) || super.dispatchKeyEvent(event); - } - - /** - * Called to process media key events. Any {@link KeyEvent} can be passed but only media key - * events will be handled. Does nothing if playback controls are disabled. - * - * @param event A key event. - * @return Whether the key event was handled. - */ - public boolean dispatchMediaKeyEvent(KeyEvent event) { - return useController && controller.dispatchMediaKeyEvent(event); - } - - /** - * Shows the playback controls. Does nothing if playback controls are disabled. - * - *

The playback controls are automatically hidden during playback after {{@link - * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, - * is paused, has ended or failed. - */ - public void showController() { - showController(shouldShowControllerIndefinitely()); - } - - /** Hides the playback controls. Does nothing if playback controls are disabled. */ - public void hideController() { - if (controller != null) { - controller.hide(); - } - } - - /** - * Returns the playback controls timeout. The playback controls are automatically hidden after - * this duration of time has elapsed without user input and with playback or buffering in - * progress. - * - * @return The timeout in milliseconds. A non-positive value will cause the controller to remain - * visible indefinitely. - */ - public int getControllerShowTimeoutMs() { - return controllerShowTimeoutMs; - } - - /** - * Sets the playback controls timeout. The playback controls are automatically hidden after this - * duration of time has elapsed without user input and with playback or buffering in progress. - * - * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the - * controller to remain visible indefinitely. - */ - public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { - Assertions.checkState(controller != null); - this.controllerShowTimeoutMs = controllerShowTimeoutMs; - // If controller is already visible, call showController to update the controller's timeout - // if necessary. - if (controller.isVisible()) { - showController(); - } - } - - /** Returns whether the playback controls are hidden by touch events. */ - public boolean getControllerHideOnTouch() { - return controllerHideOnTouch; - } - - /** - * Sets whether the playback controls are hidden by touch events. - * - * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. - */ - public void setControllerHideOnTouch(boolean controllerHideOnTouch) { - Assertions.checkState(controller != null); - this.controllerHideOnTouch = controllerHideOnTouch; - } - - /** - * Returns whether the playback controls are automatically shown when playback starts, pauses, - * ends, or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - */ - public boolean getControllerAutoShow() { - return controllerAutoShow; - } - - /** - * Sets whether the playback controls are automatically shown when playback starts, pauses, ends, - * or fails. If set to false, the playback controls can be manually operated with {@link - * #showController()} and {@link #hideController()}. - * - * @param controllerAutoShow Whether the playback controls are allowed to show automatically. - */ - public void setControllerAutoShow(boolean controllerAutoShow) { - this.controllerAutoShow = controllerAutoShow; - } - - /** - * Sets whether the playback controls are hidden when ads are playing. Controls are always shown - * during ads if they are enabled and the player is paused. - * - * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. - */ - public void setControllerHideDuringAds(boolean controllerHideDuringAds) { - this.controllerHideDuringAds = controllerHideDuringAds; - } - - /** - * Set the {@link PlaybackControlView.VisibilityListener}. - * - * @param listener The listener to be notified about visibility changes. - */ - public void setControllerVisibilityListener(PlaybackControlView.VisibilityListener listener) { - Assertions.checkState(controller != null); - controller.setVisibilityListener(listener); - } - - /** - * Sets the {@link ControlDispatcher}. - * - * @param controlDispatcher The {@link ControlDispatcher}, or null to use {@link - * DefaultControlDispatcher}. - */ - public void setControlDispatcher(@Nullable ControlDispatcher controlDispatcher) { - Assertions.checkState(controller != null); - controller.setControlDispatcher(controlDispatcher); - } - - /** - * Sets the rewind increment in milliseconds. - * - * @param rewindMs The rewind increment in milliseconds. A non-positive value will cause the - * rewind button to be disabled. - */ - public void setRewindIncrementMs(int rewindMs) { - Assertions.checkState(controller != null); - controller.setRewindIncrementMs(rewindMs); - } - - /** - * Sets the fast forward increment in milliseconds. - * - * @param fastForwardMs The fast forward increment in milliseconds. A non-positive value will - * cause the fast forward button to be disabled. - */ - public void setFastForwardIncrementMs(int fastForwardMs) { - Assertions.checkState(controller != null); - controller.setFastForwardIncrementMs(fastForwardMs); - } - - /** - * Sets which repeat toggle modes are enabled. - * - * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. - */ - public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { - Assertions.checkState(controller != null); - controller.setRepeatToggleModes(repeatToggleModes); - } - - /** - * Sets whether the shuffle button is shown. - * - * @param showShuffleButton Whether the shuffle button is shown. - */ - public void setShowShuffleButton(boolean showShuffleButton) { - Assertions.checkState(controller != null); - controller.setShowShuffleButton(showShuffleButton); - } - - /** - * Sets whether the time bar should show all windows, as opposed to just the current one. - * - * @param showMultiWindowTimeBar Whether to show all windows. - */ - public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { - Assertions.checkState(controller != null); - controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); - } - - /** - * Gets the view onto which video is rendered. This is a: - * - *

    - *
  • {@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code - * surface_view}. - *
  • {@link TextureView} if {@code surface_type} is {@code texture_view}. - *
  • {@code null} if {@code surface_type} is {@code none}. - *
- * - * @return The {@link SurfaceView}, {@link TextureView} or {@code null}. - */ - public View getVideoSurfaceView() { - return surfaceView; - } - - /** - * Gets the overlay {@link FrameLayout}, which can be populated with UI elements to show on top of - * the player. - * - * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and - * the overlay is not present. - */ - public FrameLayout getOverlayFrameLayout() { - return overlayFrameLayout; - } - - /** - * Gets the {@link SubtitleView}. - * - * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the - * subtitle view is not present. - */ - public SubtitleView getSubtitleView() { - return subtitleView; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (!useController || player == null || ev.getActionMasked() != MotionEvent.ACTION_DOWN) { - return false; - } - if (!controller.isVisible()) { - maybeShowController(true); - } else if (controllerHideOnTouch) { - controller.hide(); - } - return true; - } - - @Override - public boolean onTrackballEvent(MotionEvent ev) { - if (!useController || player == null) { - return false; - } - maybeShowController(true); - return true; - } - - /** Shows the playback controls, but only if forced or shown indefinitely. */ - private void maybeShowController(boolean isForced) { - if (isPlayingAd() && controllerHideDuringAds) { - return; - } - if (useController) { - boolean wasShowingIndefinitely = controller.isVisible() && controller.getShowTimeoutMs() <= 0; - boolean shouldShowIndefinitely = shouldShowControllerIndefinitely(); - if (isForced || wasShowingIndefinitely || shouldShowIndefinitely) { - showController(shouldShowIndefinitely); - } - } - } - - private boolean shouldShowControllerIndefinitely() { - if (player == null) { - return true; - } - int playbackState = player.getPlaybackState(); - return controllerAutoShow - && (playbackState == Player.STATE_IDLE - || playbackState == Player.STATE_ENDED - || !player.getPlayWhenReady()); - } - - private void showController(boolean showIndefinitely) { - if (!useController) { - return; - } - controller.setShowTimeoutMs(showIndefinitely ? 0 : controllerShowTimeoutMs); - controller.show(); - } - - private boolean isPlayingAd() { - return player != null && player.isPlayingAd() && player.getPlayWhenReady(); - } - - private void updateForCurrentTrackSelections() { - if (player == null) { - return; - } - TrackSelectionArray selections = player.getCurrentTrackSelections(); - for (int i = 0; i < selections.length; i++) { - if (player.getRendererType(i) == C.TRACK_TYPE_VIDEO && selections.get(i) != null) { - // Video enabled so artwork must be hidden. If the shutter is closed, it will be opened in - // onRenderedFirstFrame(). - hideArtwork(); - return; - } - } - // Video disabled so the shutter must be closed. - if (shutterView != null) { - shutterView.setVisibility(VISIBLE); - } - // Display artwork if enabled and available, else hide it. - if (useArtwork) { - for (int i = 0; i < selections.length; i++) { - TrackSelection selection = selections.get(i); - if (selection != null) { - for (int j = 0; j < selection.length(); j++) { - Metadata metadata = selection.getFormat(j).metadata; - if (metadata != null && setArtworkFromMetadata(metadata)) { - return; - } - } - } - } - if (setArtworkFromBitmap(defaultArtwork)) { - return; - } - } - // Artwork disabled or unavailable. - hideArtwork(); - } - - private boolean setArtworkFromMetadata(Metadata metadata) { - for (int i = 0; i < metadata.length(); i++) { - Metadata.Entry metadataEntry = metadata.get(i); - if (metadataEntry instanceof ApicFrame) { - byte[] bitmapData = ((ApicFrame) metadataEntry).pictureData; - Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length); - return setArtworkFromBitmap(bitmap); - } - } - return false; - } - - private boolean setArtworkFromBitmap(Bitmap bitmap) { - if (bitmap != null) { - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - if (bitmapWidth > 0 && bitmapHeight > 0) { - if (contentFrame != null) { - contentFrame.setAspectRatio((float) bitmapWidth / bitmapHeight); - } - artworkView.setImageBitmap(bitmap); - artworkView.setVisibility(VISIBLE); - return true; - } - } - return false; - } - - private void hideArtwork() { - if (artworkView != null) { - artworkView.setImageResource(android.R.color.transparent); // Clears any bitmap reference. - artworkView.setVisibility(INVISIBLE); - } - } - - @TargetApi(23) - private static void configureEditModeLogoV23(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo, null)); - logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color, null)); - } - - @SuppressWarnings("deprecation") - private static void configureEditModeLogo(Resources resources, ImageView logo) { - logo.setImageDrawable(resources.getDrawable(R.drawable.exo_edit_mode_logo)); - logo.setBackgroundColor(resources.getColor(R.color.exo_edit_mode_background_color)); - } - - @SuppressWarnings("ResourceType") - private static void setResizeModeRaw(AspectRatioFrameLayout aspectRatioFrame, int resizeMode) { - aspectRatioFrame.setResizeMode(resizeMode); - } - - /** Applies a texture rotation to a {@link TextureView}. */ - private static void applyTextureViewRotation(TextureView textureView, int textureViewRotation) { - float textureViewWidth = textureView.getWidth(); - float textureViewHeight = textureView.getHeight(); - if (textureViewWidth == 0 || textureViewHeight == 0 || textureViewRotation == 0) { - textureView.setTransform(null); - } else { - Matrix transformMatrix = new Matrix(); - float pivotX = textureViewWidth / 2; - float pivotY = textureViewHeight / 2; - transformMatrix.postRotate(textureViewRotation, pivotX, pivotY); - - // After rotation, scale the rotated texture to fit the TextureView size. - RectF originalTextureRect = new RectF(0, 0, textureViewWidth, textureViewHeight); - RectF rotatedTextureRect = new RectF(); - transformMatrix.mapRect(rotatedTextureRect, originalTextureRect); - transformMatrix.postScale( - textureViewWidth / rotatedTextureRect.width(), - textureViewHeight / rotatedTextureRect.height(), - pivotX, - pivotY); - textureView.setTransform(transformMatrix); - } - } - - @SuppressLint("InlinedApi") - private boolean isDpadKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_UP - || keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_CENTER; - } - - private final class ComponentListener extends Player.DefaultEventListener - implements TextOutput, SimpleExoPlayer.VideoListener, OnLayoutChangeListener { - - // TextOutput implementation - - @Override - public void onCues(List cues) { - if (subtitleView != null) { - subtitleView.onCues(cues); - } - } - - // SimpleExoPlayer.VideoInfoListener implementation - - @Override - public void onVideoSizeChanged( - int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) { - if (contentFrame == null) { - return; - } - float videoAspectRatio = - (height == 0 || width == 0) ? 1 : (width * pixelWidthHeightRatio) / height; - - if (surfaceView instanceof TextureView) { - // Try to apply rotation transformation when our surface is a TextureView. - if (unappliedRotationDegrees == 90 || unappliedRotationDegrees == 270) { - // We will apply a rotation 90/270 degree to the output texture of the TextureView. - // In this case, the output video's width and height will be swapped. - videoAspectRatio = 1 / videoAspectRatio; - } - if (textureViewRotation != 0) { - surfaceView.removeOnLayoutChangeListener(this); - } - textureViewRotation = unappliedRotationDegrees; - if (textureViewRotation != 0) { - // The texture view's dimensions might be changed after layout step. - // So add an OnLayoutChangeListener to apply rotation after layout step. - surfaceView.addOnLayoutChangeListener(this); - } - applyTextureViewRotation((TextureView) surfaceView, textureViewRotation); - } - - contentFrame.setAspectRatio(videoAspectRatio); - } - - @Override - public void onRenderedFirstFrame() { - if (shutterView != null) { - shutterView.setVisibility(INVISIBLE); - } - } - - @Override - public void onTracksChanged(TrackGroupArray tracks, TrackSelectionArray selections) { - updateForCurrentTrackSelections(); - } - - // Player.EventListener implementation - - @Override - public void onPlayerStateChanged(boolean playWhenReady, int playbackState) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } else { - maybeShowController(false); - } - } - - @Override - public void onPositionDiscontinuity(@DiscontinuityReason int reason) { - if (isPlayingAd() && controllerHideDuringAds) { - hideController(); - } - } - - // OnLayoutChangeListener implementation - - @Override - public void onLayoutChange( - View view, - int left, - int top, - int right, - int bottom, - int oldLeft, - int oldTop, - int oldRight, - int oldBottom) { - applyTextureViewRotation((TextureView) view, textureViewRotation); - } - } } diff --git a/library/ui/src/main/res/layout/exo_player_control_view.xml b/library/ui/src/main/res/layout/exo_player_control_view.xml new file mode 100644 index 0000000000..fd221e5d84 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_control_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/layout/exo_player_view.xml b/library/ui/src/main/res/layout/exo_player_view.xml new file mode 100644 index 0000000000..dc6dda1667 --- /dev/null +++ b/library/ui/src/main/res/layout/exo_player_view.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/library/ui/src/main/res/values/attrs.xml b/library/ui/src/main/res/values/attrs.xml index b6ed4b17af..24fa8a2091 100644 --- a/library/ui/src/main/res/values/attrs.xml +++ b/library/ui/src/main/res/values/attrs.xml @@ -41,7 +41,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java index 7164fa13ab..40d5b6c3f9 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/StubExoPlayer.java @@ -32,6 +32,16 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; */ public abstract class StubExoPlayer implements ExoPlayer { + @Override + public VideoComponent getVideoComponent() { + throw new UnsupportedOperationException(); + } + + @Override + public TextComponent getTextComponent() { + throw new UnsupportedOperationException(); + } + @Override public Looper getPlaybackLooper() { throw new UnsupportedOperationException();