版权声明:本文为博主原创翻译文章,转载请注明出处。
推荐:
欢迎关注我创建的Android TV 简书专题,会定期给大家分享一些AndroidTv相关的内容:
http://www.jianshu.com/c/37efc6e9799b
VideoControl
Video Controls implementation with MediaSession
示例实现是在Google最新的Android TV应用程序中完成的。 AOSP示例应用程序实现尚未实现MediaSession(在API 21,22中)。
在上一章中,我解释说,以下视频控件需要被压制。
1.Action的UI更新部分(在上一章完成)
2.视频控制部分(在上一章完成)MediaSession实现,通过MediaController的TransportControls进行视频控制(本章)
3.- 当用户按下电视遥控器的视频控制按钮时,MediaSession可以处理该动作。
它允许其他活动继承视频控制。特别是LeanbackLauncher,家庭显示器,可以在后台播放视频。
4.将MediaMetadata设置为MediaSession(本章)
( “ Now playing card ”将出现在推荐行的顶部。)
在本章中,我们继续使用MediaSession实现视频控件。我们可以通过使用MediaSession将VideoView控件传递给LeanbackLauncher,从而在LeanbackLauncher中实现播放视频背景。
对于3,我们在PlaybackOverlayActivity中创建MediaSession,并从PlaybackOverlayFragment中的MediaController进行控制。
对于4,MediaSession的元数据使用MediaMetadata&PlaybackState类进行更新,以更新“Now playing card”。
我建议您阅读 Displaying a Now Playing Card,以获得官方解释。
本章的实现与上一章的实现几乎是独立的。在实施MediaSession之前,我将执行requestVisibleBehind方法,以便我们可以在LeanbackLauncher应用程序的背景下播放视频。
Implement requestVisibleBehind
这个方法是添加在API 21(Lolipop)中,AOSP的解释解释了
If this call is successful then the activity will remain visible after onPause() is called, and is allowed to continue playing media in the background.
示例实现如下。
@Override
public void onPause() {
super.onPause();
if (!requestVisibleBehind(true)) {
// Try to play behind launcher, but if it fails, stop playback.
mPlaybackController.playPause(false);
}
}
实施后,当您在应用程序中播放视频内容并按“Home”键返回LeanbackLauncher时,该视频将在后台继续播放。。
本章课堂结构
我们在本章中处理3个课程。
- PlaybackOverlayActivity
- 管理生命周期,将意图信息传递给PlaybackController
- MediaSession的生活时间与活动有关
- PlaybackOverlayFragment
- 处理PlaybackControlsRow的UI
- MediaController回调功能用于根据当前播放状态更新UI
- PlaybackController
- 管理视频播放
- 视频控制功能
- MediaSessionCallback用于从电视遥控器接收视频控制键
Create & release MediaSession
到目前为止,我们无法使用遥控器中的视频控制键控制此视频。 让我们实现MediaSession来定义遥控器中每个视频控制键的动作。 首先,我们在PlaybackController的Constractor中创建MediaSession,由PlaybackOverlayActivity调用。
public PlaybackController(Activity activity) {
mActivity = activity;
// mVideoView = (VideoView) activity.findViewById(VIDEO_VIEW_RESOURCE_ID);
createMediaSession(mActivity);
}
private void createMediaSession(Activity activity) {
if (mSession == null) {
mSession = new MediaSession(activity, MEDIA_SESSION_TAG);
mMediaSessionCallback = new MediaSessionCallback();
mSession.setCallback(mMediaSessionCallback);
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS |
MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mSession.setActive(true);
activity.setMediaController(new MediaController(activity, mSession.getSessionToken()));
}
}
我们可以将回调MediaSessionCallback设置为MediaSession。 它定义了将在后面解释的每个视频控制按钮的精确行为。
需要使用参数FLAG_HANDLES_MEDIA_BUTTONS&FLAG_HANDLES_TRANSPORT_CONTROLS的setFlags方法才能使遥控器键控制视频。
创建后,我们必须在完成后释放MediaSession。
public void releaseMediaSession() {
if(mSession != null) {
mSession.release();
}
}
视频控制功能
视频控制功能在MediaSessionCallback类中实现。 顾名思义,每个视频控制动作都在相应的回调函数中实现。 这种回调是从2种方式调用的,即“遥控器媒体键”或“PlaybackControlsRow”中的UI视频控制按钮。
public void playPause(boolean doPlay) {
if (mCurrentPlaybackState == PlaybackState.STATE_NONE) {
/* Callbacks for mVideoView */
setupCallbacks();
}
//if (doPlay && mCurrentPlaybackState != PlaybackState.STATE_PLAYING) {
if (doPlay) { // Play
Log.d(TAG, "playPause: play");
if(mCurrentPlaybackState == PlaybackState.STATE_PLAYING) {
/* if current state is already playing, do nothing */
return;
} else {
mCurrentPlaybackState = PlaybackState.STATE_PLAYING;
mVideoView.start();
mStartTimeMillis = System.currentTimeMillis();
}
} else { // Pause
Log.d(TAG, "playPause: pause");
if(mCurrentPlaybackState == PlaybackState.STATE_PAUSED) {
/* if current state is already paused, do nothing */
return;
} else {
mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
}
setPosition(mVideoView.getCurrentPosition());
mVideoView.pause();
}
updatePlaybackState();
}
public void fastForward() {
if (mDuration != -1) {
// Fast forward 10 seconds.
setPosition(getCurrentPosition() + (10 * 1000));
mVideoView.seekTo(mPosition);
}
}
public void rewind() {
// rewind 10 seconds
setPosition(getCurrentPosition() - (10 * 1000));
mVideoView.seekTo(mPosition);
}
private class MediaSessionCallback extends MediaSession.Callback {
@Override
public void onPlay() {
playPause(true);
}
@Override
public void onPause() {
playPause(false);
}
@Override
public void onSkipToNext() {
if (++mCurrentItem >= mItems.size()) { // Current Item is set to next here
mCurrentItem = 0;
}
Movie movie = mItems.get(mCurrentItem);
//Movie movie = VideoProvider.getMovieById(mediaId);
if (movie != null) {
setVideoPath(movie.getVideoUrl());
//mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
//updateMetadata(movie);
updateMetadata();
playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
} else {
Log.e(TAG, "onSkipToNext movie is null!");
}
}
@Override
public void onSkipToPrevious() {
if (--mCurrentItem < 0) { // Current Item is set to previous here
mCurrentItem = mItems.size()-1;
}
Movie movie = mItems.get(mCurrentItem);
//Movie movie = VideoProvider.getMovieById(mediaId);
if (movie != null) {
setVideoPath(movie.getVideoUrl());
//mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
updateMetadata();
playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
} else {
Log.e(TAG, "onSkipToPrevious movie is null!");
}
}
@Override
public void onPlayFromMediaId(String mediaId, Bundle extras) {
mCurrentItem = Integer.parseInt(mediaId);
Movie movie = mItems.get(mCurrentItem);
//Movie movie = VideoProvider.getMovieById(mediaId);
if (movie != null) {
setVideoPath(movie.getVideoUrl());
// mCurrentPlaybackState = PlaybackState.STATE_PAUSED;
// updateMetadata(movie);
updateMetadata();
playPause(mCurrentPlaybackState == PlaybackState.STATE_PLAYING);
}
}
@Override
public void onSeekTo(long pos) {
setPosition((int) pos);
mVideoView.seekTo(mPosition);
updatePlaybackState();
}
@Override
public void onFastForward() {
fastForward();
}
@Override
public void onRewind() {
rewind();
}
}
视频控制按遥控器键
private void updatePlaybackState() {
PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
.setActions(getAvailableActions());
int state = PlaybackState.STATE_PLAYING;
if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) {
state = PlaybackState.STATE_PAUSED;
}
stateBuilder.setState(state, getCurrentPosition(), 1.0f);
mSession.setPlaybackState(stateBuilder.build());
}
private long getAvailableActions() {
long actions = PlaybackState.ACTION_PLAY |
PlaybackState.ACTION_PAUSE |
PlaybackState.ACTION_PLAY_PAUSE |
PlaybackState.ACTION_REWIND |
PlaybackState.ACTION_FAST_FORWARD |
PlaybackState.ACTION_SKIP_TO_PREVIOUS |
PlaybackState.ACTION_SKIP_TO_NEXT |
PlaybackState.ACTION_PLAY_FROM_MEDIA_ID |
PlaybackState.ACTION_PLAY_FROM_SEARCH;
return actions;
}
在这个例子中,可以通过getAvailableActions方法来确定可用的操作,通过使用逻辑分离来添加操作。
从UI控制视频- MediaController.getTransportControls
要通过VideoDetailsFragment中的PlaybackControlsRow控制MediaSession,我们使用MediaController。 MediaController是在PlaybackController的构造函数中创建的,它拥有MediaSession的标记。
当用户点击视频控制按钮时,它将使用MediaController.getTransportControls()方法调用MediaSessionCallback方法。
/* onClick */
playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() {
public void onActionClicked(Action action) {
if (action.getId() == mPlayPauseAction.getId()) {
/* PlayPause action */
if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY) {
mMediaController.getTransportControls().play();
} else if (mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PAUSE) {
mMediaController.getTransportControls().pause();
}
} else if (action.getId() == mSkipNextAction.getId()) {
/* SkipNext action */
mMediaController.getTransportControls().skipToNext();
} else if (action.getId() == mSkipPreviousAction.getId()) {
/* SkipPrevious action */
mMediaController.getTransportControls().skipToPrevious();
} else if (action.getId() == mFastForwardAction.getId()) {
/* FastForward action */
mMediaController.getTransportControls().fastForward();
} else if (action.getId() == mRewindAction.getId()) {
/* Rewind action */
mMediaController.getTransportControls().rewind();
}
}
视频控制部分完成。 但是,我们需要根据视频控制
更新PlayControlsRow的UI。
更新VideoDetailsFragment的UI
当执行视频控制动作时,需要更改UI,视频播放状态已更改。 我们可以使用MediaController的回调函数获取此事件。 以下介绍2种回调方法。
- onPlaybackStateChanged
- onMetadataChanged
要使用这些回调方法,您可以使扩展MediaController.Callback类的子类,并覆盖这些方法。 要使用这个类,我们可以调用MediaController的registerCallback / unregisterCallback方法来获取MediaController的事件。
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
mMediaController = getActivity().getMediaController();
Log.d(TAG, "register callback of mediaController");
if(mMediaController == null){
Log.e(TAG, "mMediaController is null");
}
mMediaController.registerCallback(mMediaControllerCallback);
}
@Override
public void onDetach() {
if (mMediaController != null) {
Log.d(TAG, "unregister callback of mediaController");
mMediaController.unregisterCallback(mMediaControllerCallback);
}
super.onDetach();
}
private class MediaControllerCallback extends MediaController.Callback {
@Override
public void onPlaybackStateChanged(final PlaybackState state) {
Log.d(TAG, "playback state changed: " + state.toString());
}
@Override
public void onMetadataChanged(final MediaMetadata metadata) {
Log.d(TAG, "received update of media metadata");
}
}
更新Playback StateChanged中的视频控制图标和更新PlaybackState
PlaybackState在播放控制器中更新。
private void updatePlaybackState() {
PlaybackState.Builder stateBuilder = new PlaybackState.Builder()
.setActions(getAvailableActions());
int state = PlaybackState.STATE_PLAYING;
if (mCurrentPlaybackState == PlaybackState.STATE_PAUSED || mCurrentPlaybackState == PlaybackState.STATE_NONE) {
state = PlaybackState.STATE_PAUSED;
}
// stateBuilder.setState(state, mPosition, 1.0f);
stateBuilder.setState(state, getCurrentPosition(), 1.0f);
mSession.setPlaybackState(stateBuilder.build());
}
例如,它将在playPause方法中调用。 当用户开始播放视频状态将从STATE_PLAYING变为STATE_PAUSED,反之亦然。 PlaybackState更新被设置(通知)到MediaSession。
Callback
当PlaybackState由上面的setPlaybackState更改时,可以使用onPlaybackStateChanged回调接收此事件。 我们可以在PlaybackControlsRow中更新播放/暂停图标。
private class MediaControllerCallback extends MediaController.Callback {
@Override
public void onPlaybackStateChanged(final PlaybackState state) {
Log.d(TAG, "playback state changed: " + state.toString());
mHandler.post(new Runnable() {
@Override
public void run() {
if (state.getState() == PlaybackState.STATE_PLAYING) {
mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PLAYING);
startProgressAutomation();
// setFadingEnabled(false);
mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PAUSE);
mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PAUSE));
notifyChanged(mPlayPauseAction);
} else if (state.getState() == PlaybackState.STATE_PAUSED) {
mPlaybackController.setCurrentPlaybackState(PlaybackState.STATE_PAUSED);
// setFadingEnabled(false);
mPlayPauseAction.setIndex(PlaybackControlsRow.PlayPauseAction.PLAY);
mPlayPauseAction.setIcon(mPlayPauseAction.getDrawable(PlaybackControlsRow.PlayPauseAction.PLAY));
notifyChanged(mPlayPauseAction);
}
int currentTime = (int) state.getPosition();
mPlaybackControlsRow.setCurrentTime(currentTime);
// mPlaybackControlsRow.setBufferedProgress(currentTime + SIMULATED_BUFFERED_TIME);
mPlaybackControlsRow.setBufferedProgress(mPlaybackController.calcBufferedTime(currentTime));
}
});
}
...
}
在onMetadataChanged上更新媒体信息
更新MediaMetadata
MediaMetadata类用于设置视频的元数据信息。 我们可以通过MediaMetadata.Builder中的put方法设置元数据的每个属性。 MediaMetadata更新被设置(通知)到MediaSession。
public void updateMetadata(Movie movie) {
final MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder();
String title = movie.getTitle().replace("_", " -");
metadataBuilder.putString(MediaMetadata.METADATA_KEY_MEDIA_ID, Long.toString(movie.getId()));
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title);
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, movie.getStudio());
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_DESCRIPTION, movie.getDescription());
metadataBuilder.putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, movie.getCardImageUrl());
metadataBuilder.putLong(MediaMetadata.METADATA_KEY_DURATION, mDuration);
// And at minimum the title and artist for legacy support
metadataBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, title);
metadataBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, movie.getStudio());
Glide.with(mActivity)
.load(Uri.parse(movie.getCardImageUrl()))
.asBitmap()
.into(new SimpleTarget<Bitmap>(500, 500) {
@Override
public void onResourceReady(Bitmap bitmap, GlideAnimation anim) {
metadataBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap);
mSession.setMetadata(metadataBuilder.build());
}
});
}
通过将MediaMetadata设置为MediaSession,Android TV将在LeanbackLauncher上显示Now Playing Card ,稍后再说明。
CallBack
当MediaMetadata已经通过setMetadata以上改变时,该事件可以与onMetadataChanged回调被接收。我们可以更新PlaybackControlsRow的项目值。
private class MediaControllerCallback extends MediaController.Callback {
...
@Override
public void onMetadataChanged(final MediaMetadata metadata) {
Log.d(TAG, "received update of media metadata");
updateMovieView(
metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE),
metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE),
metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI),
metadata.getLong(MediaMetadata.METADATA_KEY_DURATION)
);
}
}
private void updateMovieView(String title, String studio, String cardImageUrl, long duration) {
Log.d(TAG, "updateMovieView");
if (mPlaybackControlsRow.getItem() != null) {
Movie item = (Movie) mPlaybackControlsRow.getItem();
item.setTitle(title);
item.setStudio(studio);
} else {
Log.e(TAG, "mPlaybackControlsRow.getItem is null!");
}
mPlaybackControlsRow.setTotalTime((int) duration);
mPlaybackControlsRow.setCurrentTime(0);
mPlaybackControlsRow.setBufferedProgress(0);
mRowsAdapter.notifyArrayItemRangeChanged(0, mRowsAdapter.size());
// Show the video card image if there is enough room in the UI for it.
// If you have many primary actions, you may not have enough room.
if (SHOW_IMAGE) {
mPlaybackControlsRowTarget = new PicassoPlaybackControlsRowTarget(mPlaybackControlsRow);
updateVideoImage(cardImageUrl);
}
}
Now Playing Card
NowPlayingCard如果MediaMetadata正确设置为MediaSession,则现在播放卡将显示在LeanbackLauncher(主屏幕)中。 它向用户通知当前播放媒体的信息。 此外,现在播放卡使用户可以回到您的应用程序来控制视频(暂停/转到下一个视频等)。
源码在github上.
关注微信公众号,定期为你推荐移动开发相关文章。
网友评论