DistributedVideoPlayer 分布式视频播放器(二)
想了解更多内容,分布放器请访问:
和华为官方合作共建的式视鸿蒙技术社区
https://harmonyos.51cto.com
介绍
上一期我们实现了视频的播放功能,播放列表还有评论功能.这一期,我们来看一下手机端是如何实现一个对远端TV视频播放的遥控功能.
[本文正在参与优质创作者激励]
效果展示

搭建环境
安装DevEco Studio,详情请参考DevEco Studio下载。频播
设置DevEco Studio开发环境,分布放器DevEco Studio开发环境需要依赖于网络环境,式视需要连接上网络才能确保工具的频播正常使用,可以根据如下两种情况来配置开发环境:
如果可以直接访问Internet,分布放器只需进行下载HarmonyOS SDK操作。式视
如果网络不能直接访问Internet,频播需要通过代理服务器才可以访问,分布放器请参考配置开发环境。式视
下载源码后,频播使用DevEco 打开项目。分布放器
代码结构
手机端
Java后台
│ config.json │ ├─java │ └─com │ └─buty │ └─distributedvideoplayer │ │ MainAbility.java │ │ MyApplication.java │ │ │ ├─ability │ │ DevicesSelectAbility.java #可流转的式视设备列表 │ │ MainAbilitySlice.java #视频播放列表页 │ │ SyncControlServiceAbility.java #同步控制服务,TV-->Phone │ │ VideoPlayAbility.java #视频播放Ability │ │ VideoPlayAbilitySlice.java #视频播放详情和评论页 │ │ │ ├─components │ │ EpisodesSelectionDialog.java │ │ RemoteController.java #远端控制器 │ │ VideoPlayerPlaybackButton.java #播放按钮组件 │ │ VideoPlayerSlider.java #播放时间进度条 │ │ │ ├─constant │ │ Constants.java #常量 │ │ ResolutionEnum.java #分辨率枚举 │ │ RouteRegister.java #自定义路由 │ │ │ ├─data │ │ VideoInfo.java #视频基础信息 │ │ VideoInfoService.java #视频信息服务,频播用于模拟数据 │ │ Videos.java #视频列表 │ │ │ ├─model │ │ CommentModel.java #评论模型 │ │ DeviceModel.java #设备模型 │ │ ResolutionModel.java #解析度模型 │ │ VideoModel.java #视频模型 │ │ │ ├─provider │ │ CommentItemProvider.java #评论数据提供程序 │ │ DeviceItemProvider.java #设备列表提供程序 │ │ ResolutionItemProvider.java #解析度数据提供程序 │ │ VideoItemProvider.java #视频数据提供程序 │ │ │ └─utils │ AppUtil.java #工具类 │ DateUtils.java
页面布局
│ │ │ ├─layout │ │ ability_main.xml #播放列表布局 │ │ comments_item.xml #单条评论布局 │ │ dialog_playlist.xml │ │ dialog_resolution_list.xml │ │ dialog_table_layout.xml │ │ hm_sample_ability_video_box.xml #视频播放组件页 │ │ hm_sample_ability_video_comments.xml #播放详情布局页 │ │ hm_sample_view_video_box_seek_bar_style1.xml #播放进度条布局 │ │ hm_sample_view_video_box_seek_bar_style2.xml │ │ remote_ability_control.xml #远程控制器布局 │ │ remote_ability_episodes.xml │ │ remote_ability_select_devices.xml #可流转设备列表布局 │ │ remote_ability_sound_equipment.xml │ │ remote_device_item.xml #设备子项显示布局 │ │ remote_episodes_item.xml │ │ remote_video_quality_item.xmlTV端
Java后台
├─main │ │ config.json │ │ │ ├─java │ │ └─com │ │ └─buty │ │ └─distributedvideoplayer │ │ │ MainAbility.java │ │ │ MyApplication.java │ │ │ VideoControlServiceAbility.java #视频控制服务 Phone--->TV │ │ │ │ │ ├─component │ │ │ VideoSetting.java │ │ │ │ │ ├─constant #一些常量和枚举值 │ │ │ Constants.java │ │ │ ResolutionEnum.java │ │ │ SettingOptionEnum.java │ │ │ SpeedEnum.java │ │ │ │ │ ├─data │ │ │ VideoInfo.java #视频基本信息 │ │ │ VideoInfoService.java #视频数据服务,读取json中的数据 │ │ │ Videos.java #视频对象 │ │ │ │ │ ├─model #一些数据模型 │ │ │ ResolutionModel.java │ │ │ SettingComponentTag.java │ │ │ SettingModel.java │ │ │ VideoModel.java │ │ │ │ │ ├─provider │ │ │ SettingProvider.java │ │ │ VideoEpisodesSelectProvider.java │ │ │ VideoSettingProvider.java │ │ │ │ │ ├─slice │ │ │ MainAbilitySlice.java │ │ │ VideoPlayAbilitySlice.java #视频播放能力页 │ │ │ │ │ ├─utils │ │ │ AppUtil.java │ │ │ │ │ └─view │ │ VideoPlayerPlaybackButton.java │ │ VideoPlayerSlider.java页面布局
│ │ │ │ │ ├─layout │ │ │ ability_main.xml │ │ │ ability_video_box.xml #播放器布局页面 │ │ │ video_common_item.xml │ │ │ video_episodes_item.xml │ │ │ video_setting.xml │ │ │ video_setting_item.xml │ │ │ view_video_box_seek_bar_style1.xml #播放器进度条布局实现步骤
1.手机端
1.1.页面布局,控制器布局页 remote_ability_control.xml
使用了DependentLayout,DirectionalLayout,TableLayout 布局组件 和 其他常用的组件.

1.2.页面布局,选择设备组件布局页 remote_ability_select_devices.xml
使用了DependentLayout,DirectionalLayout布局组件 和 ListContainer 等组件.

1.3.Java代码,远端控制器视图组件 RemoteController.java
RemoteController继承自DependentLayout布局组件,实现了Component.ClickedListener和Slider.ValueChangedListener,用于处理 点击事件 和 滑块滑动事件。
/** * 控制器面板组件 * Remote Control Page */ public class RemoteController extends DependentLayout //实现了 组件的点击监听和滑块的值变化监听 的接口 implements Component.ClickedListener, Slider.ValueChangedListener { ...控制器面板视图组件的组成,包括两大部分,
第一部分是云服务器:组件的初始化,包括:控制组件的初始化, 播放进度组件的初始化, 剧集组件的初始化
/** * 初始化远端控制视图的各个组件 */ private void initView() { //设置隐藏 setVisibility(INVISIBLE); if (controllerView == null) { controllerView = LayoutScatter.getInstance(slice).parse(ResourceTable.Layout_remote_ability_control, this, false); } //初始化文本 initItemText(); initItemSize(); initItemImage(); //进度滑块 initProgressSlider(); //初始化按钮 initButton(ResourceTable.Id_app_bar_back); initButton(ResourceTable.Id_control_episodes_num); initButton(ResourceTable.Id_control_all_episodes); initButton(ResourceTable.Id_control_play); initButton(ResourceTable.Id_control_backword); initButton(ResourceTable.Id_control_forward); initButton(ResourceTable.Id_control_voice_down); initButton(ResourceTable.Id_control_voice_up); //初始化底部的显示的视频剧集 initBottomComponent(); //将组件追加到队列末尾 addComponent(controllerView); //初始化剧集对话框 initEpisodesDialog(); isPlaying = true; }第二部分是:自定义了控制监听器(RemoteControllerListener )和接口,结合点击事和滑块滑动事件将自己的操作传递给手机视频播放器类(VideoPlayAbilitySlice)。
/** * 控制器面板操作监听 * 播放/快退/快进/音量加减/停止连接/切换视频/切换解析度 * RemoteControllerListener */ public interface RemoteControllerListener { //发送控制码给该接口的实现 void sendControl(int code, String extra); } /** * * 设置控制器监听器 * setRemoteControllerCallback * * @param listener listener */ public void setRemoteControllerCallback(RemoteControllerListener listener) { remoteControllerListener = listener; } /** * 点击事件进行统一处理,通过sendControl发送出去 */ @Override public void onClick(Component component) { switch (component.getId()) { //返回组件 case ResourceTable.Id_app_bar_back: hide(true); break; case ResourceTable.Id_control_episodes_num: //剧集组件,显示剧集对话框 case ResourceTable.Id_control_all_episodes: episodesDialog.setVisibility(VISIBLE); break; //播放组件,发送播放的控制指令 case ResourceTable.Id_control_play: remoteControllerListener.sendControl(ControlCode.PLAY.getCode(), ""); break; //快退组件,发送快退指令 case ResourceTable.Id_control_backword: remoteControllerListener.sendControl(ControlCode.BACKWARD.getCode(), ""); break; //快进组件,发送快进指令 case ResourceTable.Id_control_forward: remoteControllerListener.sendControl(ControlCode.FORWARD.getCode(), ""); break; //增加音量,发送给增加音量指令 case ResourceTable.Id_control_voice_up: remoteControllerListener.sendControl(ControlCode.VOLUME_ADD.getCode(), ""); break; //降低音量,发送降低音量指令 case ResourceTable.Id_control_voice_down: //关闭显示的对话框 if (getDialogVisibility()) { remoteControllerListener.sendControl(ControlCode.VOLUME_REDUCED.getCode(), ""); } break; default: break; } } /** * 时间进度条值变化时,设置当前的播放时间 * @param slider * @param value * @param fromUser */ @Override public void onProgressUpdated(Slider slider, int value, boolean fromUser) { HiLog.debug(LABEL,"onProgressUpdated"); slice.getUITaskDispatcher() .delayDispatch( () -> { //当前播放的时间进度 Text currentTime = (Text) controllerView.findComponentById(ResourceTable.Id_control_current_time); //设置显示的时间 currentTime.setText( DateUtils.msToString(totalTime * value / Constants.ONE_HUNDRED_PERCENT)); }, 0); } @Override public void onTouchStart(Slider slider) { isSliderTouching = true; } /** * 进度条滑块拖拽结束触发,sendControl发送出去 * @param slider */ @Override public void onTouchEnd(Slider slider) { // The pop-up box cannot block the slider touch event. // This event is not processed when a dialog box is displayed. //滑动结束,发送seek指令到远端 if (getDialogVisibility()) { // remoteControllerListener.sendControl(ControlCode.SEEK.getCode(), String.valueOf(slider.getProgress())); } isSliderTouching = false; }1.4.Java代码,流转设备列表页面 DevicesSelectAbility.java
主要是提供设备选择列表以及选择设备后返回设备信息
/** * 可供选择的远端设备能力 * Remote Device Selection Ability */ public class DevicesSelectAbility extends Ability { @Override public void onStart(Intent intent) { //请求数据流转权限 requestPermissionsFromUser(new String[]{ "ohos.permission.DISTRIBUTED_DATASYNC"}, 0); super.onStart(intent); super.setUIContent(ResourceTable.Layout_remote_ability_select_devices); this.initPage(intent); } /** * 初始化页面组件 * * @param intent */ private void initPage(Intent intent) { //从json中获取视频数据 VideoInfoService videoService = new VideoInfoService(this); //设置app名称 Text appName = (Text) findComponentById(ResourceTable.Id_devices_head_app_name); appName.setText(ResourceTable.String_entry_MainAbility); //视频名称组件 Text videoName = (Text) findComponentById(ResourceTable.Id_devices_head_video_name); //当前播放视频的索引 int currentPlayingIndex = intent.getIntParam(Constants.PARAM_VIDEO_INDEX, 0) + 1; //当前播放视频的剧集 String playingEpisodes = AppUtil.getStringResource(this, ResourceTable.String_control_playing_episodes) .replaceAll("\\?", String.valueOf(currentPlayingIndex)); //设置播放视频名称和剧集 videoName.setText(videoService.getAllVideoInfo().getVideoName() + " " + playingEpisodes); //在线设备列表,以及设置点击的监听事件、传递数据 ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_devices_container); List<DeviceModel> devices = AppUtil.getDevicesInfo(); //容器绑定数据提供程序 DeviceItemProvider provider = new DeviceItemProvider(this, devices); listContainer.setItemProvider(provider); //设置点击监听处理 listContainer.setItemClickedListener( (container, component, position, id) -> { //获取点击的item DeviceModel item = (DeviceModel) listContainer.getItemProvider().getItem(position); //返回数据意图 Intent intentResult = new Intent(); //设置要返回的参数 intentResult.setParam(Constants.PARAM_DEVICE_TYPE, item.getDeviceType()); intentResult.setParam(Constants.PARAM_DEVICE_ID, item.getDeviceId()); intentResult.setParam(Constants.PARAM_DEVICE_NAME, item.getDeviceName()); //设置返回结果 setResult(0, intentResult); //关闭当前Ability this.terminateAbility(); }); } }可用设备列表提供程序 DeviceItemProvider.java
/** * 设备列表提供程序 * Device information list processing class */ public class DeviceItemProvider extends BaseItemProvider { private final Context context; private final List<DeviceModel> list; /** * Initialization */ public DeviceItemProvider(Context context, List<DeviceModel> list) { this.context = context; this.list = list; } @Override public int getCount() { return list == null ? 0 : list.size(); } @Override public Object getItem(int position) { if (list != null && position >= 0 && position < list.size()) { return list.get(position); } return new DeviceModel(); } @Override public long getItemId(int position) { return position; } @Override public Component getComponent(int position, Component convertComponent, ComponentContainer componentContainer) { final Component cpt; if (convertComponent == null) { cpt = LayoutScatter.getInstance(context).parse(ResourceTable.Layout_remote_device_item, null, false); } else { cpt = convertComponent; } DeviceModel deviceItem = list.get(position); //设备名称 Text deviceName = (Text) cpt.findComponentById(ResourceTable.Id_device_item_name); deviceName.setText(deviceItem.getDeviceName()); //设备图标 Image deviceIcon = (Image) cpt.findComponentById(ResourceTable.Id_device_item_icon); AppUtil.setDeviceIcon(deviceItem.getDeviceType(), deviceIcon); if (position == list.size() - 1) { Component divider = cpt.findComponentById(ResourceTable.Id_device_item_divider); divider.setVisibility(Component.INVISIBLE); } return cpt; } }1.5.Java代码,视频播放器页面 VideoPlayAbilitySlice.java
视频播放器页面 远端控制操作的代码主要包括两部分,服务器托管
第一部分是:点击“流转” 按钮时,打开可用设备列表,点击要流转的设备后,在onAbilityResult方法中,打开远端TV设备的播放器能力页(MainAbility) 并 连接上控制元服务(VideoControlServiceAbility)
/** * 打开设备选择Ability后,选择流转的设备setResult后触发 * @param requestCode * @param resultCode * @param resultIntent */ @Override protected void onAbilityResult(int requestCode, int resultCode, Intent resultIntent) { HiLog.debug(LABEL, "onAbilityResult"); // if (requestCode == Constants.PRESENT_SELECT_DEVICES_REQUEST_CODE && resultIntent != null) { // startRemoteAbilityPa(resultIntent); return; } // setDisplayOrientation(AbilityInfo.DisplayOrientation.values()[sourceDisplayOrientation + 1]); if (isVideoPlaying) { player.start(); } } /** * 开启远端Ability * * @param resultIntent */ private void startRemoteAbilityPa(Intent resultIntent) { //远端TV设备ID String devicesId = resultIntent.getStringParam(Constants.PARAM_DEVICE_ID); Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId(devicesId) .withBundleName(getBundleName()) .withAbilityName("com.buty.distributedvideoplayer.MainAbility") .withAction("action.video.play") .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); //本地存储设备ID String localDeviceId = KvManagerFactory.getInstance().createKvManager(new KvManagerConfig(this)).getLocalDeviceInfo().getId(); HiLog.debug(LABEL, "remoteDevicesId:" + devicesId + ",localDeviceId:" + localDeviceId); //播放的视频路径 String path = videoService .getVideoInfoByIndex(currentPlayingIndex) .getResolutions() .get(currentPlayingResolutionIndex) .getUrl(); //本地ph()one设备ID intent.setParam(RemoteConstant.INTENT_PARAM_REMOTE_DEVICE_ID, localDeviceId); //播放视频的URL intent.setParam(RemoteConstant.INTENT_PARAM_REMOTE_VIDEO_PATH, path); //播放不同分辨率视频的索引 intent.setParam(RemoteConstant.INTENT_PARAM_REMOTE_VIDEO_INDEX, currentPlayingIndex); //播放进度位置 intent.setParam(RemoteConstant.INTENT_PARAM_REMOTE_START_POSITION, (int) player.getSeekWhenPrepared()); intent.setOperation(operation); //启动远端的播放Ability startAbility(intent); //远端视频控制元服务 Intent remotePaIntent = new Intent(); Operation paOperation = new Intent.OperationBuilder() .withDeviceId(devicesId) .withBundleName(getBundleName()) .withAbilityName("com.buty.distributedvideoplayer.VideoControlServiceAbility") .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); remotePaIntent.setOperation(paOperation); //连接远端视频控制服务,使用2台P40的超级终端模拟器连接不成功 //Context::connectRemoteAbility failed, errorCode is 1319 boolean connectFlag = connectAbility(remotePaIntent, connection); if (connectFlag) { HiLog.debug(LABEL, "start remote ability PA success"); //设置显示方向为竖屏 setDisplayOrientation(AbilityInfo.DisplayOrientation.PORTRAIT); //初始化远端控制 initRemoteController(); //设置播放进度、状态、等 remoteController.setVideoInfo( resultIntent.getStringParam(Constants.PARAM_DEVICE_NAME), currentPlayingIndex, (int) player.getCurrentPosition(), (int) player.getDuration()); remoteController.show(); } else { HiLog.error(LABEL, "start remote ability PA failed"); stopAbility(intent); } }第二部分是:成功连接到远端视频控制元服务后,初始化远端控制器(RemoteController)并实现控制器面板的监听器接口(sendControl),通过mProxy发送控制指令到TV端(sendDataToRemote)
/** * 初始化控制器 及 监听 */ private void initRemoteController() { if (remoteController == null) { remoteController = new RemoteController(this); //手机端控制面板操作的监听回调 remoteController.setRemoteControllerCallback( (code, extra) -> { if (mProxy == null) { return; } //发送控制指令到TV端 boolean result = mProxy.sendDataToRemote(RemoteConstant.REQUEST_CONTROL_REMOTE_DEVICE, code, extra); if (!result) { new ToastDialog(getContext()) .setText( AppUtil.getStringResource( getContext(), ResourceTable.String_send_failed_tips)) .show(); remoteController.hide(false); } }); StackLayout rootLayout = (StackLayout) findComponentById(ResourceTable.Id_root_layout); rootLayout.addComponent(remoteController); } }第三部分是:订阅手机端控制事件(Constants.PHONE_CONTROL_EVENT)用于处理同步控制服务(SyncControlServiceAbility)发过来的事件,目的是把TV端的状态同步给手机控制端
/** * 订阅事件,用于 "TV端->手机端" 方向的播放状态的同步 */ private void subscribe() { HiLog.debug(LABEL, "subscribe"); MatchingSkills matchingSkills = new MatchingSkills(); //手机端控制面板的 控制事件 matchingSkills.addEvent(Constants.PHONE_CONTROL_EVENT); matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON); CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills); //事件订阅器 TODO subscriber = new MyCommonEventSubscriber(subscribeInfo); try { CommonEventManager.subscribeCommonEvent(subscriber); } catch (RemoteException e) { HiLog.error(LABEL, "subscribeCommonEvent occur exception."); } } /** * 取消订阅 */ private void unSubscribe() { HiLog.debug(LABEL, "unSubscribe"); try { CommonEventManager.unsubscribeCommonEvent(subscriber); } catch (RemoteException e) { HiLog.error(LABEL, "unsubscribecommonevent occur exception."); } } /** * 事件订阅器,用于 "TV端->手机端" 方向的播放状态的同步 */ class MyCommonEventSubscriber extends CommonEventSubscriber { MyCommonEventSubscriber(CommonEventSubscribeInfo info) { super(info); } @Override public void onReceiveEvent(CommonEventData commonEventData) { Intent intent = commonEventData.getIntent(); //获取事件参数,控制指令码 int controlCode = intent.getIntParam(Constants.KEY_CONTROL_CODE, 0); HiLog.debug(LABEL,"onReceiveEvent: controlCode"+controlCode); //未进行远端控制 if (remoteController == null || !remoteController.isShown()) { HiLog.debug(LABEL, "remote controller is hidden now"); return; } //如果是源码下载视频播放进度指令 if (controlCode == ControlCode.SYNC_VIDEO_PROCESS.getCode()) { int totalTime = Integer.parseInt(intent.getStringParam(Constants.KEY_CONTROL_VIDEO_TIME)); int progress = Integer.parseInt(intent.getStringParam(Constants.KEY_CONTROL_VIDEO_PROGRESS)); //更新的控制面板的进度条 remoteController.syncVideoPlayProcess(totalTime, progress); //更新控制面板的视频播放状态 } else if (controlCode == ControlCode.SYNC_VIDEO_STATUS.getCode()) { boolean isPlaying = Boolean.parseBoolean(intent.getStringParam(Constants.KEY_CONTROL_VIDEO_PLAYBACK_STATUS)); if (remoteController.getPlayingStatus() != isPlaying) { remoteController.changePlayingStatus(); } //更新控制面板的音量 } else { int currentVolume = Integer.parseInt(intent.getStringParam(Constants.KEY_CONTROL_VIDEO_VOLUME)); remoteController.changeVolumeIcon(currentVolume); } } }1.6.Java代码,远端视频控制同步服务 SyncControlServiceAbility.java
这个服务是给TV端连接使用的,对端连接过来,将 播放状态、播放进度、音量值同步过来
/** * 同步控制元服务 * Video Control Synchronization Service */ public class SyncControlServiceAbility extends Ability { private static final HiLogLabel LABEL = new HiLogLabel(0, 0, "=>SyncControlServiceAbility"); //远端设备代理 private final MyRemote remote = new MyRemote(RemoteConstant.REQUEST_SYNC_VIDEO_STATUS); @Override public void onStart(Intent intent) { super.onStart(intent); // remote.setRemoteRequestCallback( this::sendEvent); } @Override public void onBackground() { super.onBackground(); } @Override public void onStop() { super.onStop(); } @Override protected IRemoteObject onConnect(Intent intent) { super.onConnect(intent); return remote.asObject(); } /** * 发送播放器事件 * @param controlCode * @param value */ private void sendEvent(int controlCode, Map<?, ?> value) { HiLog.debug(LABEL,"sendEvent,controlCode:"+controlCode+",value:"+value.toString()); try { Intent intent = new Intent(); Operation operation = new Intent.OperationBuilder().withAction(Constants.PHONE_CONTROL_EVENT).build(); intent.setOperation(operation); intent.setParam(Constants.KEY_CONTROL_CODE, controlCode); //播放进度 if (controlCode == ControlCode.SYNC_VIDEO_PROCESS.getCode()) { intent.setParam(Constants.KEY_CONTROL_VIDEO_TIME, String.valueOf(value.get(RemoteConstant.REMOTE_KEY_VIDEO_TOTAL_TIME))); intent.setParam(Constants.KEY_CONTROL_VIDEO_PROGRESS, String.valueOf(value.get(RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_PROGRESS))); //播放状态 } else if (controlCode == ControlCode.SYNC_VIDEO_STATUS.getCode()) { intent.setParam(Constants.KEY_CONTROL_VIDEO_PLAYBACK_STATUS, String.valueOf(value.get(RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_PLAYBACK_STATUS))); //播放音量 } else { intent.setParam(Constants.KEY_CONTROL_VIDEO_VOLUME, String.valueOf(value.get(RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_VOLUME))); } CommonEventData eventData = new CommonEventData(intent); //发布事件 CommonEventManager.publishCommonEvent(eventData); } catch (RemoteException e) { HiLog.error(LABEL, "publishCommonEvent occur exception."); } } }2.TV端
2.1.页面布局,视频播放器布局组件 ability_video_box.xml
播放器组件VideoPlayerView
<?xml version="1.0" encoding="utf-8"?> <StackLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:id="$+id:root_layout" ohos:height="match_parent" ohos:width="match_parent" ohos:background_element="#FFFFFFFF" ohos:orientation="vertical"> <com.buty.distributedvideoplayer.player.ui.widget.media.VideoPlayerView ohos:id="$+id:video_view" ohos:height="match_parent" ohos:width="match_parent"/> </StackLayot>2.2.页面布局,视频播放器的进度条布局组件 view_video_box_seek_bar_style1.xml
<?xml version="1.0" encoding="utf-8"?> <!--Time is above the progress bar--> <DependentLayout xmlns:ohos="http://schemas.huawei.com/res/ohos" ohos:height="54vp" ohos:width="match_parent" ohos:orientation="horizontal"> <Text ohos:id="$+id:current_time" ohos:height="match_content" ohos:width="match_content" ohos:above="$id:seek_bar" ohos:align_parent_start="true" ohos:start_margin="12vp" ohos:text_color="white" ohos:text_size="10fp"/> <Slider ohos:id="$+id:seek_bar" ohos:height="match_content" ohos:width="match_parent" ohos:background_instruct_element="$color:seek_bar_background_instruct_color" ohos:center_in_parent="true" ohos:progress_element="$color:seek_bar_progress_color" ohos:thumb_element="$graphic:hm_sample_slider_thumb" ohos:vice_progress_element="$color:seek_bar_vice_progress_color" /> <Text ohos:id="$+id:end_time" ohos:height="match_content" ohos:width="match_content" ohos:above="$id:seek_bar" ohos:align_parent_end="true" ohos:end_margin="12vp" ohos:text_color="white" ohos:text_size="10fp"/> </DependentLayout>2.3.Java代码, 视频控制元服务 VideoControlServiceAbility.java
分为两部分,
第一部分是:手机端连接过来后,asObject。
/** * 远程设备的代理,来源commonlib */ private final MyRemote remote = new MyRemote(RemoteConstant.REQUEST_CONTROL_REMOTE_DEVICE); @Override protected IRemoteObject onConnect(Intent intent) { HiLog.debug(LABEL, "onConnect"); super.onConnect(intent); //返回代理对象 return remote.asObject(); }第二部分是:发送事件通知到订阅方(VideoPlayAbilitySlice)
/** * 发送事件通知 VideoPlayAbilitySlice * @param controlCode 控制码 * @param value */ private void sendEvent(int controlCode, Map<?, ?> value) { HiLog.debug(LABEL, "sendEvent:"+controlCode+","+value.toString()); try { //意图 Intent intent = new Intent(); //TV控制事件操作 Operation operation = new Intent.OperationBuilder() .withAction(Constants.TV_CONTROL_EVENT) .build(); intent.setOperation(operation); //设置控制参数 intent.setParam(Constants.KEY_CONTROL_CODE, controlCode); intent.setParam(Constants.KEY_CONTROL_VALUE, (String) value.get(RemoteConstant.REMOTE_KEY_CONTROL_VALUE)); //封装时间数据 CommonEventData eventData = new CommonEventData(intent); //通用事件管理器,发布事件 CommonEventManager.publishCommonEvent(eventData); } catch (RemoteException e) { HiLog.error(LABEL, "publishCommonEvent occur exception."); } }2.4.Java代码, 视频播放器能力页 VideoPlayAbilitySlice.java
第一部分是:连接手机端的同步控制元服务(SyncControlServiceAbility),建立连接后,初始化远端代理(MyRemoteProxy)。
//连接的phone设备 connectRemoteDevice( //从意图中获取远端phone设备ID intent.getStringParam(RemoteConstant.INTENT_PARAM_REMOTE_DEVICE_ID)); /** * 连接远端phone设备的同步服务 * @param deviceId */ private void connectRemoteDevice(String deviceId) { HiLog.debug(LABEL,"connectRemoteDevice:"+deviceId); Intent connectPaIntent = new Intent(); Operation operation = new Intent.OperationBuilder() .withDeviceId(deviceId) .withBundleName(getBundleName()) .withAbilityName(REMOTE_PHONE_ABILITY) .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) .build(); connectPaIntent.setOperation(operation); connectAbility(connectPaIntent, connection); } // Creating a Connection Callback Instance private final IAbilityConnection connection = new IAbilityConnection() { // Callback for connecting to a service @Override public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) { myProxy = new MyRemoteProxy(iRemoteObject); } // Callback for disconnecting from the service @Override public void onAbilityDisconnectDone(ElementName elementName, int resultCode) { disconnectAbility(this); } };第二部分是:注册远端控制回调,实现视频播放器组件(VideoPlayerView)的RemoteControlCallback接口,使用远端代理对象(MyRemoteProxy)发送数据到手机端同步当前播放器信息
//注册远端控制回调 videoBox.registerRemoteControlCallback(remoteControlCallback); /** * 远端控制回调,来源commonlib,用于同步进度条进度/播放状态/音量 */ private VideoPlayerView.RemoteControlCallback remoteControlCallback = new VideoPlayerView.RemoteControlCallback() { @Override //进度条变化 public void onProgressChanged(long totalTime, int progress) { HiLog.debug(LABEL,"onProgressChanged,myProxy:"+myProxy); if (myProxy != null) { Map<String, String> progressValue = new HashMap<>(); //设置总时间和进度值 progressValue.put(RemoteConstant.REMOTE_KEY_VIDEO_TOTAL_TIME, String.valueOf(totalTime)); progressValue.put(RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_PROGRESS, String.valueOf(progress)); //同步进度之给手机端的控制面板 myProxy.sendDataToRemote( RemoteConstant.REQUEST_SYNC_VIDEO_STATUS, ControlCode.SYNC_VIDEO_PROCESS.getCode(), progressValue); } } @Override //播放状态变化 public void onPlayingStatusChanged(boolean isPlaying) { if (myProxy != null) { Map<String, String> videoStatusMap = new HashMap<>(); videoStatusMap.put( RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_PLAYBACK_STATUS, String.valueOf(isPlaying)); HiLog.debug(LABEL, "isPlaying = " + String.valueOf(isPlaying)); myProxy.sendDataToRemote( RemoteConstant.REQUEST_SYNC_VIDEO_STATUS, ControlCode.SYNC_VIDEO_STATUS.getCode(), videoStatusMap); } } @Override //音量变化 public void onVolumeChanged(int volume) { if (myProxy != null) { Map<String, String> volumeMap = new HashMap<>(); volumeMap.put(RemoteConstant.REMOTE_KEY_VIDEO_CURRENT_VOLUME, String.valueOf(volume)); myProxy.sendDataToRemote( RemoteConstant.REQUEST_SYNC_VIDEO_STATUS, ControlCode.SYNC_VIDEO_VOLUME.getCode(), volumeMap); } } };第三部分是:订阅事件,处理视频控制服务(VideoControlServiceAbility)发送的播放器控制事件
/** * 通用事件订阅 */ private void subscribe() { HiLog.debug(LABEL,"subscribe"); MatchingSkills matchingSkills = new MatchingSkills(); matchingSkills.addEvent(Constants.TV_CONTROL_EVENT); matchingSkills.addEvent(CommonEventSupport.COMMON_EVENT_SCREEN_ON); CommonEventSubscribeInfo subscribeInfo = new CommonEventSubscribeInfo(matchingSkills); //订阅者 tvSubscriber = new MyCommonEventSubscriber(subscribeInfo); try { CommonEventManager.subscribeCommonEvent(tvSubscriber); } catch (RemoteException e) { HiLog.error(LABEL, "subscribeCommonEvent occur exception."); } } /** * 取消订阅 */ private void unSubscribe() { HiLog.debug(LABEL,"subscribe"); try { CommonEventManager.unsubscribeCommonEvent(tvSubscriber); } catch (RemoteException e) { HiLog.error(LABEL, "unSubscribe Exception"); } } /** * 视频控制服务(VideoControlServiceAbility)事件订阅者 */ class MyCommonEventSubscriber extends CommonEventSubscriber { MyCommonEventSubscriber(CommonEventSubscribeInfo info) { super(info); } @Override public void onReceiveEvent(CommonEventData commonEventData) { HiLog.info(LABEL, "onReceiveEvent....."); Intent intent = commonEventData.getIntent(); int controlCode = intent.getIntParam(Constants.KEY_CONTROL_CODE, 0); String extras = intent.getStringParam(Constants.KEY_CONTROL_VALUE); //播放or暂停 if (controlCode == ControlCode.PLAY.getCode()) { if (videoBox.isPlaying()) { videoBox.pause(); } else if (!videoBox.isPlaying() && !needResumeStatus) { videoBox.start(); } else { HiLog.error(LABEL, "Ignoring the case with player status"); } //拖动播放进度 } else if (controlCode == ControlCode.SEEK.getCode()) { videoBox.seekTo(videoBox.getDuration() * Integer.parseInt(extras) / 100); //快进 } else if (controlCode == ControlCode.FORWARD.getCode()) { videoBox.seekTo(videoBox.getCurrentPosition() + Constants.REWIND_STEP); //快退 } else if (controlCode == ControlCode.BACKWARD.getCode()) { videoBox.seekTo(videoBox.getCurrentPosition() - Constants.REWIND_STEP); //音量加 } else if (controlCode == ControlCode.VOLUME_ADD.getCode()) { videoBox.setVolume(Constants.VOLUME_STEP); //音量减 } else if (controlCode == ControlCode.VOLUME_REDUCED.getCode()) { videoBox.setVolume(-Constants.VOLUME_STEP); //切换播放速度 } else if (controlCode == ControlCode.SWITCH_SPEED.getCode()) { videoBox.setPlaybackSpeed(Float.parseFloat(extras)); //切换视频源,例如高清 } else if (controlCode == ControlCode.SWITCH_RESOLUTION.getCode()) { long currentPosition = videoBox.getCurrentPosition(); int resolutionIndex = Integer.parseInt(extras); VideoInfo videoInfo = videoInfoService.getVideoInfoByIndex(currentPlayingIndex); videoBox.pause(); //设置新的播放URL videoBox.setVideoPath(videoInfo.getResolutions().get(resolutionIndex).getUrl()); //调整到原播放位置 videoBox.setPlayerOnPreparedListener( () -> { videoBox.seekTo(currentPosition); videoBox.start(); }); //切换视频 } else if (controlCode == ControlCode.SWITCH_VIDEO.getCode()) { videoBox.pause(); currentPlayingIndex = Integer.parseInt(extras); VideoInfo videoInfo = videoInfoService.getVideoInfoByIndex(currentPlayingIndex); videoBox.setVideoPathAndTitle(videoInfo.getResolutions().get(0).getUrl(), videoInfo.getVideoDesc()); videoBox.setPlayerOnPreparedListener(() -> videoBox.start()); //停止连接 } else if (controlCode == ControlCode.STOP_CONNECTION.getCode()) { terminate(); } else { HiLog.error(LABEL, "Ignoring the case with control code"); } } }至此,手机端控制端和TV端的过程就解读完了,部分细节如切换视频解析度、切换视频剧集、TV端设置 等不影响全局流程就不展开了。
两端涉及的权限如下:
{ "name": "ohos.permission.INTERNET", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }, { "name": "ohos.permission.DISTRIBUTED_DATASYNC", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }, { "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }, { "name": "ohos.permission.GET_DISTRIBUTED_DEVICE_INFO", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }, { "name": "ohos.permission.GET_BUNDLE_INFO", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }, { "name": "ohos.permission.KEEP_BACKGROUND_RUNNING", "reason": "", "usedScene": { "ability": [ "VideoPlayAbilitySlice" ], "when": "inuse" } }回顾总结
手机端控制TV端视频播放的流程
手机端:
点击手机端播放器(VideoPlayAbilitySlice)的【流转】按钮-------获取&选择可以流转的设备----启动 TV端播放器(MainAbility/VideoPlayAbilitySlice) & 连接TV端播放控制服务(VideoControlServiceAbility)-----在建立连接后,初始化控制面板并且对控制操作进行监听。
当操作控制面板(RemoteController)时 --发布事件通知播放器组件(VideoPlayAbilitySlice)-----(VideoPlayAbilitySlice)使用控制服务的远端代理(MyRemoteProxy,commonlib提供)发送控制指令到 TV端播放控制服务(VideoControlServiceAbility)。
TV端:
当TV端播放器(VideoPlayAbilitySlice)被启动时-----初始化视频播放器组件& 注册远端控制回调(registerRemoteControlCallback) & 获取手机端Intent传递的视频索引+视频URL+播放进度+手机端设备ID-----然后连接手机端的同步控制服务(SyncControlServiceAbility)— 在建立连接后,初始化代理(MyRemoteProxy)-----订阅手机端播放器控制事件。
当播放控制服务(VideoControlServiceAbility)收到控制指令后-----通过事件方式发布通知-----视频播放器(VideoPlayAbilitySlice)收到通知后对播放器进行设置-----注册远端控制回调(remoteControlCallback)将状态同步给远端的手机端。
文章相关附件可以点击下面的原文链接前往下载
https://harmonyos.51cto.com/resource/1356
想了解更多内容,请访问:
和华为官方合作共建的鸿蒙技术社区
https://harmonyos.51cto.com