Commit c577d9d3 by olly Committed by Oliver Woodman

Let SimpleExoPlayerView/LeanbackPlayerAdapter bind with any Player

Also sanitize naming (PlayerView/PlayerControlView).

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182364487
parent 605aeb3a
Showing with 2575 additions and 2277 deletions
......@@ -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
......
......@@ -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
......
......@@ -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<DemoUtil.Sample> 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);
}
}
......
......@@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/local_player_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="12"
......@@ -42,7 +42,7 @@
android:layout_alignParentBottom="true"
android:padding="30dp"/>
</RelativeLayout>
<com.google.android.exoplayer2.ui.PlaybackControlView android:id="@+id/cast_control_view"
<com.google.android.exoplayer2.ui.PlayerControlView android:id="@+id/cast_control_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
......
......@@ -18,7 +18,7 @@ package com.google.android.exoplayer2.imademo;
import android.app.Activity;
import android.os.Bundle;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.ui.PlayerView;
/**
* Main Activity for the IMA plugin demo. {@link ExoPlayer} objects are created by
......@@ -26,7 +26,7 @@ import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
*/
public final class MainActivity extends Activity {
private SimpleExoPlayerView playerView;
private PlayerView playerView;
private PlayerManager player;
@Override
......
......@@ -38,7 +38,7 @@ import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
......@@ -68,7 +68,7 @@ import com.google.android.exoplayer2.util.Util;
new DefaultBandwidthMeter());
}
public void init(Context context, SimpleExoPlayerView simpleExoPlayerView) {
public void init(Context context, PlayerView playerView) {
// Create a default track selector.
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
......@@ -79,7 +79,7 @@ import com.google.android.exoplayer2.util.Util;
player = ExoPlayerFactory.newSimpleInstance(context, trackSelector);
// Bind the player to the view.
simpleExoPlayerView.setPlayer(player);
playerView.setPlayer(player);
// This is the MediaSource representing the content media (i.e. not the ad).
String contentUrl = context.getString(R.string.content_url);
......@@ -92,7 +92,7 @@ import com.google.android.exoplayer2.util.Util;
contentMediaSource,
/* adMediaSourceFactory= */ this,
adsLoader,
simpleExoPlayerView.getOverlayFrameLayout(),
playerView.getOverlayFrameLayout(),
/* eventHandler= */ null,
/* eventListener= */ null);
......
......@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
<com.google.android.exoplayer2.ui.PlayerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/player_view"
android:layout_width="match_parent"
......
......@@ -68,8 +68,8 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.DebugTextViewHelper;
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.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.HttpDataSource;
......@@ -79,11 +79,9 @@ import java.net.CookieManager;
import java.net.CookiePolicy;
import java.util.UUID;
/**
* An activity that plays media using {@link SimpleExoPlayer}.
*/
public class PlayerActivity extends Activity implements OnClickListener,
PlaybackControlView.VisibilityListener {
/** An activity that plays media using {@link SimpleExoPlayer}. */
public class PlayerActivity extends Activity
implements OnClickListener, PlayerControlView.VisibilityListener {
public static final String DRM_SCHEME_EXTRA = "drm_scheme";
public static final String DRM_LICENSE_URL = "drm_license_url";
......@@ -112,7 +110,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
private Handler mainHandler;
private EventLogger eventLogger;
private SimpleExoPlayerView simpleExoPlayerView;
private PlayerView playerView;
private LinearLayout debugRootView;
private TextView debugTextView;
private Button retryButton;
......@@ -156,9 +154,9 @@ public class PlayerActivity extends Activity implements OnClickListener,
retryButton = findViewById(R.id.retry_button);
retryButton.setOnClickListener(this);
simpleExoPlayerView = findViewById(R.id.player_view);
simpleExoPlayerView.setControllerVisibilityListener(this);
simpleExoPlayerView.requestFocus();
playerView = findViewById(R.id.player_view);
playerView.setControllerVisibilityListener(this);
playerView.requestFocus();
}
@Override
......@@ -223,7 +221,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
// See whether the player view wants to handle media or DPAD keys events.
return simpleExoPlayerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
return playerView.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
}
// OnClickListener methods
......@@ -303,7 +301,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
player.addAudioDebugListener(eventLogger);
player.addVideoDebugListener(eventLogger);
simpleExoPlayerView.setPlayer(player);
playerView.setPlayer(player);
player.setPlayWhenReady(shouldAutoPlay);
debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start();
......@@ -470,7 +468,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
.newInstance(this, adTagUri);
adUiViewGroup = new FrameLayout(this);
// The demo app has a non-null overlay frame layout.
simpleExoPlayerView.getOverlayFrameLayout().addView(adUiViewGroup);
playerView.getOverlayFrameLayout().addView(adUiViewGroup);
}
AdsMediaSource.MediaSourceFactory adMediaSourceFactory =
new AdsMediaSource.MediaSourceFactory() {
......@@ -495,7 +493,7 @@ public class PlayerActivity extends Activity implements OnClickListener,
adsLoader.release();
adsLoader = null;
loadedAdTagUri = null;
simpleExoPlayerView.getOverlayFrameLayout().removeAllViews();
playerView.getOverlayFrameLayout().removeAllViews();
}
}
......
......@@ -20,7 +20,7 @@
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView android:id="@+id/player_view"
<com.google.android.exoplayer2.ui.PlayerView android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
......
......@@ -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.
......@@ -281,6 +281,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);
}
......
......@@ -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);
}
......
......@@ -125,6 +125,16 @@ import java.util.concurrent.CopyOnWriteArraySet;
}
@Override
public VideoComponent getVideoComponent() {
return null;
}
@Override
public TextComponent getTextComponent() {
return null;
}
@Override
public Looper getPlaybackLooper() {
return internalPlayer.getPlaybackLooper();
}
......
......@@ -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.
*
* <p>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
......
......@@ -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<VideoListener> videoListeners;
private final CopyOnWriteArraySet<com.google.android.exoplayer2.video.VideoListener>
videoListeners;
private final CopyOnWriteArraySet<TextOutput> textOutputs;
private final CopyOnWriteArraySet<MetadataOutput> metadataOutputs;
private final CopyOnWriteArraySet<VideoRendererEventListener> 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.
* <p>
* Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer} is
* enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
*
* <p>Note that the scaling mode only applies if a {@link MediaCodec}-based video {@link Renderer}
* is enabled and if the output surface is owned by a {@link android.view.SurfaceView}.
*
* @param videoScalingMode The video scaling mode.
*/
@Override
public void setVideoScalingMode(@C.VideoScalingMode int videoScalingMode) {
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.
* <p>
* 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();
}
}
......
/*
* 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();
}
......@@ -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.
*
* <p>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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a PlaybackControlView when used in a layout XML file:
*
* <p>
*
* <ul>
* <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
* being automatically hidden, in milliseconds. Use zero if the controls should not
* automatically timeout.
* <ul>
* <li>Corresponding method: {@link #setShowTimeoutMs(int)}
* <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
* </ul>
* <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the
* rewind button, in milliseconds. Use zero to disable the rewind button.
* <ul>
* <li>Corresponding method: {@link #setRewindIncrementMs(int)}
* <li>Default: {@link #DEFAULT_REWIND_MS}
* </ul>
* <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.
* <ul>
* <li>Corresponding method: {@link #setFastForwardIncrementMs(int)}
* <li>Default: {@link #DEFAULT_FAST_FORWARD_MS}
* </ul>
* <li><b>{@code repeat_toggle_modes}</b> - 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}.
* <ul>
* <li>Corresponding method: {@link #setRepeatToggleModes(int)}
* <li>Default: {@link PlaybackControlView#DEFAULT_REPEAT_TOGGLE_MODES}
* </ul>
* <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
* <li>Default: false
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See
* below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_playback_control_view}
* </ul>
* </ul>
*
* <h3>Overriding the layout file</h3>
*
* 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:
*
* <p>
*
* <ul>
* <li><b>{@code exo_play}</b> - The play button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_pause}</b> - The pause button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_ffwd}</b> - The fast forward button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_rew}</b> - The rewind button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_prev}</b> - The previous track button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_next}</b> - The next track button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_repeat_toggle}</b> - The repeat toggle button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_shuffle}</b> - The shuffle button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_position}</b> - Text view displaying the current playback position.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
* <ul>
* <li>Type: {@link TimeBar}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
*
* 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);
}
@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;
super(context, attrs, defStyleAttr, playbackAttrs);
}
/**
* 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();
}
}
}
/*
* 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.
*
* <p>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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a PlayerControlView when used in a layout XML file:
*
* <ul>
* <li><b>{@code show_timeout}</b> - The time between the last user interaction and the controls
* being automatically hidden, in milliseconds. Use zero if the controls should not
* automatically timeout.
* <ul>
* <li>Corresponding method: {@link #setShowTimeoutMs(int)}
* <li>Default: {@link #DEFAULT_SHOW_TIMEOUT_MS}
* </ul>
* <li><b>{@code rewind_increment}</b> - The duration of the rewind applied when the user taps the
* rewind button, in milliseconds. Use zero to disable the rewind button.
* <ul>
* <li>Corresponding method: {@link #setRewindIncrementMs(int)}
* <li>Default: {@link #DEFAULT_REWIND_MS}
* </ul>
* <li><b>{@code fastforward_increment}</b> - Like {@code rewind_increment}, but for fast forward.
* <ul>
* <li>Corresponding method: {@link #setFastForwardIncrementMs(int)}
* <li>Default: {@link #DEFAULT_FAST_FORWARD_MS}
* </ul>
* <li><b>{@code repeat_toggle_modes}</b> - 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}.
* <ul>
* <li>Corresponding method: {@link #setRepeatToggleModes(int)}
* <li>Default: {@link PlayerControlView#DEFAULT_REPEAT_TOGGLE_MODES}
* </ul>
* <li><b>{@code show_shuffle_button}</b> - Whether the shuffle button is shown.
* <ul>
* <li>Corresponding method: {@link #setShowShuffleButton(boolean)}
* <li>Default: false
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout to be inflated. See
* below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_player_control_view}
* </ul>
* </ul>
*
* <h3>Overriding the layout file</h3>
*
* 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:
*
* <p>
*
* <ul>
* <li><b>{@code exo_play}</b> - The play button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_pause}</b> - The pause button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_ffwd}</b> - The fast forward button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_rew}</b> - The rewind button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_prev}</b> - The previous track button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_next}</b> - The next track button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_repeat_toggle}</b> - The repeat toggle button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_shuffle}</b> - The shuffle button.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_position}</b> - Text view displaying the current playback position.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_duration}</b> - Text view displaying the current media duration.
* <ul>
* <li>Type: {@link TextView}
* </ul>
* <li><b>{@code exo_progress}</b> - Time bar that's updated during playback and allows seeking.
* <ul>
* <li>Type: {@link TimeBar}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
*
* 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();
}
}
}
/*
* 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}.
*
* <p>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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a PlayerView when used in a layout XML file:
*
* <ul>
* <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.
* <ul>
* <li>Corresponding method: {@link #setUseArtwork(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio
* streams.
* <ul>
* <li>Corresponding method: {@link #setDefaultArtwork(Bitmap)}
* <li>Default: {@code null}
* </ul>
* <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.
* <ul>
* <li>Corresponding method: {@link #setUseController(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.
* <ul>
* <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code auto_show}</b> - 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()}.
* <ul>
* <li>Corresponding method: {@link #setControllerAutoShow(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.
* Controls are always shown during ads if they are enabled and the player is paused.
* <ul>
* <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code resize_mode}</b> - 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}.
* <ul>
* <li>Corresponding method: {@link #setResizeMode(int)}
* <li>Default: {@code fit}
* </ul>
* <li><b>{@code surface_type}</b> - 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.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code surface_view}
* </ul>
* <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
* view.
* <ul>
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
* <li>Default: {@code unset}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_player_view}
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be
* inflated by the child {@link PlayerControlView}. See below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_player_control_view}
* </ul>
* <li>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).
* </ul>
*
* <h3>Overriding the layout file</h3>
*
* 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:
*
* <p>
*
* <ul>
* <li><b>{@code exo_content_frame}</b> - 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.
* <ul>
* <li>Type: {@link AspectRatioFrameLayout}
* </ul>
* <li><b>{@code exo_shutter}</b> - 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.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_subtitles}</b> - Displays subtitles.
* <ul>
* <li>Type: {@link SubtitleView}
* </ul>
* <li><b>{@code exo_artwork}</b> - Displays album art.
* <ul>
* <li>Type: {@link ImageView}
* </ul>
* <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link PlayerControlView}. Ignored if an {@code exo_controller} view exists.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_controller}</b> - 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.
* <ul>
* <li>Type: {@link PlayerControlView}
* </ul>
* <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
* the app can access via {@link #getOverlayFrameLayout()}, provided for convenience.
* <ul>
* <li>Type: {@link FrameLayout}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
*
* 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.
*
* <p>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 <em>before</em>
* 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.
*
* <p>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:
*
* <ul>
* <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
* surface_view}.
* <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
* <li>{@code null} if {@code surface_type} is {@code none}.
* </ul>
*
* @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<Cue> 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);
}
}
}
......@@ -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}.
*
* <p>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.
*
* <h3>Attributes</h3>
*
* The following attributes can be set on a SimpleExoPlayerView when used in a layout XML file:
*
* <p>
*
* <ul>
* <li><b>{@code use_artwork}</b> - Whether artwork is used if available in audio streams.
* <ul>
* <li>Corresponding method: {@link #setUseArtwork(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code default_artwork}</b> - Default artwork to use if no artwork available in audio
* streams.
* <ul>
* <li>Corresponding method: {@link #setDefaultArtwork(Bitmap)}
* <li>Default: {@code null}
* </ul>
* <li><b>{@code use_controller}</b> - Whether the playback controls can be shown.
* <ul>
* <li>Corresponding method: {@link #setUseController(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_on_touch}</b> - Whether the playback controls are hidden by touch events.
* <ul>
* <li>Corresponding method: {@link #setControllerHideOnTouch(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code auto_show}</b> - 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()}.
* <ul>
* <li>Corresponding method: {@link #setControllerAutoShow(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code hide_during_ads}</b> - Whether the playback controls are hidden during ads.
* Controls are always shown during ads if they are enabled and the player is paused.
* <ul>
* <li>Corresponding method: {@link #setControllerHideDuringAds(boolean)}
* <li>Default: {@code true}
* </ul>
* <li><b>{@code resize_mode}</b> - 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}.
* <ul>
* <li>Corresponding method: {@link #setResizeMode(int)}
* <li>Default: {@code fit}
* </ul>
* <li><b>{@code surface_type}</b> - 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.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code surface_view}
* </ul>
* <li><b>{@code shutter_background_color}</b> - The background color of the {@code exo_shutter}
* view.
* <ul>
* <li>Corresponding method: {@link #setShutterBackgroundColor(int)}
* <li>Default: {@code unset}
* </ul>
* <li><b>{@code player_layout_id}</b> - Specifies the id of the layout to be inflated. See below
* for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_simple_player_view}
* </ul>
* <li><b>{@code controller_layout_id}</b> - Specifies the id of the layout resource to be
* inflated by the child {@link PlaybackControlView}. See below for more details.
* <ul>
* <li>Corresponding method: None
* <li>Default: {@code R.id.exo_playback_control_view}
* </ul>
* <li>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).
* </ul>
*
* <h3>Overriding the layout file</h3>
*
* 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:
*
* <p>
*
* <ul>
* <li><b>{@code exo_content_frame}</b> - 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.
* <ul>
* <li>Type: {@link AspectRatioFrameLayout}
* </ul>
* <li><b>{@code exo_shutter}</b> - 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.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_subtitles}</b> - Displays subtitles.
* <ul>
* <li>Type: {@link SubtitleView}
* </ul>
* <li><b>{@code exo_artwork}</b> - Displays album art.
* <ul>
* <li>Type: {@link ImageView}
* </ul>
* <li><b>{@code exo_controller_placeholder}</b> - A placeholder that's replaced with the inflated
* {@link PlaybackControlView}. Ignored if an {@code exo_controller} view exists.
* <ul>
* <li>Type: {@link View}
* </ul>
* <li><b>{@code exo_controller}</b> - 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.
* <ul>
* <li>Type: {@link PlaybackControlView}
* </ul>
* <li><b>{@code exo_overlay}</b> - A {@link FrameLayout} positioned on top of the player which
* the app can access via {@link #getOverlayFrameLayout()}, provided for convenience.
* <ul>
* <li>Type: {@link FrameLayout}
* </ul>
* </ul>
*
* <p>All child views are optional and so can be omitted if not required, however where defined they
* must be of the expected type.
*
* <h3>Specifying a custom layout file</h3>
*
* 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);
}
}
/** 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.
*
* <p>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 <em>before</em> 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.
*
* <p>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:
*
* <ul>
* <li>{@link SurfaceView} by default, or if the {@code surface_type} attribute is set to {@code
* surface_view}.
* <li>{@link TextureView} if {@code surface_type} is {@code texture_view}.
* <li>{@code null} if {@code surface_type} is {@code none}.
* </ul>
*
* @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);
PlayerView.switchTargetView(player, oldPlayerView, newPlayerView);
}
/** 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<Cue> 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);
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<merge>
<include layout="@layout/exo_playback_control_view"/>
</merge>
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<merge>
<include layout="@layout/exo_simple_player_view"/>
</merge>
......@@ -41,7 +41,7 @@
</attr>
<attr name="show_shuffle_button" format="boolean"/>
<declare-styleable name="SimpleExoPlayerView">
<declare-styleable name="PlayerView">
<attr name="use_artwork" format="boolean"/>
<attr name="shutter_background_color" format="color"/>
<attr name="default_artwork" format="reference"/>
......@@ -52,7 +52,7 @@
<attr name="resize_mode"/>
<attr name="surface_type"/>
<attr name="player_layout_id"/>
<!-- PlaybackControlView attrs -->
<!-- PlayerControlView attributes -->
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
......@@ -65,7 +65,7 @@
<attr name="resize_mode"/>
</declare-styleable>
<declare-styleable name="PlaybackControlView">
<declare-styleable name="PlayerControlView">
<attr name="show_timeout"/>
<attr name="rewind_increment"/>
<attr name="fastforward_increment"/>
......
......@@ -33,6 +33,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();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment