版权声明:本文为博主原创翻译文章,转载请注明出处。
推荐: 欢迎关注我创建的Android TV 简书专题,会定期给大家分享一些AndroidTv相关的内容:
在上一章中,我解释说,以下视频控件需要被压制。 1.Action的UI更新部分(在上一章完成) 2.视频控制部分(在上一章完成)MediaSession实现,通过MediaController的TransportControls进行视频控制(本章)
3.- 当用户按下电视遥控器的视频控制按钮时,MediaSession可以处理该动作。 它允许其他活动继承视频控制。特别是LeanbackLauncher,家庭显示器,可以在后台播放视频。
4.将MediaMetadata设置为MediaSession(本章) ( “ ”将出现在推荐行的顶部。)
在本章中,我们继续使用MediaSession实现视频控件。我们可以通过使用MediaSession将VideoView控件传递给LeanbackLauncher,从而在LeanbackLauncher中实现播放视频背景。
对于3,我们在PlaybackOverlayActivity中创建MediaSession,并从PlaybackOverlayFragment中的MediaController进行控制。
对于4,MediaSession的元数据使用MediaMetadata&PlaybackState类进行更新,以更新“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(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
如果MediaMetadata正确设置为MediaSession,则现在播放卡将显示在LeanbackLauncher(主屏幕)中。 它向用户通知当前播放媒体的信息。 此外,现在播放卡使用户可以回到您的应用程序来控制视频(暂停/转到下一个视频等)。 源码在上. 关注微信公众号,定期为你推荐移动开发相关文章。