(目录)
DistributedVideoPlayer 分布式视频播放器(二)
介绍
上一期我们实现了视频的播放功能,播放列表还有评论功能.这一期,我们来看一下手机端是如何实现一个对远端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.xml
TV端 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 布局组件 和 其他常用的组件.
<?xml version="1.0" encoding="utf-8"?>
<DependentLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:clickable="true">
<DirectionalLayout
ohos:height="match_parent"
ohos:width="match_parent"
ohos:background_element="$graphic:background_ability_control_bg"
ohos:orientation="vertical">
<StackLayout
ohos:id="$+id:control_app_bar"
ohos:height="match_content"
ohos:width="match_parent">
<DirectionalLayout
ohos:id="$+id:control_app_bar_left"
ohos:height="56vp"
ohos:width="match_content"
ohos:layout_alignment="vertical_center"
ohos:orientation="horizontal">
<Image
ohos:id="$+id:app_bar_back"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:foreground_element="$media:ic_back"
ohos:layout_alignment="center"
ohos:start_margin="$float:default_margin">
</Image>
<Text
ohos:id="$+id:app_bar_device_name"
ohos:height="match_parent"
ohos:width="match_content"
ohos:start_margin="12vp"
ohos:text=""
ohos:text_color="$color:default_white_color"
ohos:text_size="$float:normal_text_size_20"
ohos:truncation_mode="ellipsis_at_end"/>
</DirectionalLayout>
</StackLayout>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:layout_alignment="vertical_center"
ohos:orientation="horizontal">
<Image
ohos:height="16vp"
ohos:width="16vp"
ohos:foreground_element="$media:ic_play"
ohos:layout_alignment="center"
ohos:start_margin="$float:default_margin">
</Image>
<Text
ohos:id="$+id:device_video_desc"
ohos:height="match_content"
ohos:width="match_parent"
ohos:auto_scrolling_count="unlimited"
ohos:end_margin="$float:default_margin"
ohos:start_margin="16vp"
ohos:text=""
ohos:text_color="$color:default_white_color"
ohos:text_size="$float:little_text_size_12"
ohos:truncation_mode="auto_scrolling"/>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:control_middle_panel"
ohos:height="225vp"
ohos:width="225vp"
ohos:background_element="$graphic:background_ability_control_middle"
ohos:layout_alignment="center"
ohos:orientation="vertical"
ohos:top_margin="64vp">
<DirectionalLayout
ohos:id="$+id:control_middle_panel_top"
ohos:height="75vp"
ohos:width="match_parent">
<Image
ohos:id="$+id:control_voice_up"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:background_element="$graphic:background_button_click"
ohos:foreground_element="$media:ic_voice"
ohos:layout_alignment="center"
ohos:top_margin="28vp"/>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:control_middle_panel_center"
ohos:height="75vp"
ohos:width="match_parent"
ohos:orientation="horizontal">
<DirectionalLayout
ohos:id="$+id:control_backword_parent"
ohos:height="match_parent"
ohos:width="75vp"
ohos:alignment="vertical_center">
<Image
ohos:id="$+id:control_backword"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:background_element="$graphic:background_button_click"
ohos:foreground_element="$media:ic_anthology"
ohos:layout_alignment="center"/>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:control_play_parent"
ohos:height="match_parent"
ohos:width="75vp"
ohos:alignment="center">
<Image
ohos:id="$+id:control_play"
ohos:height="45vp"
ohos:width="45vp"
ohos:background_element="$graphic:background_ability_control_ok"
ohos:image_src="$media:ic_pause_black"
ohos:layout_alignment="center"/>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:control_forward_parent"
ohos:height="match_parent"
ohos:width="75vp"
ohos:alignment="vertical_center">
<Image
ohos:id="$+id:control_forward"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:background_element="$graphic:background_button_click"
ohos:foreground_element="$media:ic_anthology"
ohos:layout_alignment="center"
ohos:rotate="180"/>
</DirectionalLayout>
</DirectionalLayout>
<DirectionalLayout
ohos:id="$+id:control_middle_panel_bottom"
ohos:height="75vp"
ohos:width="match_parent">
<Image
ohos:id="$+id:control_voice_down"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:background_element="$graphic:background_button_click"
ohos:foreground_element="$media:ic_voice"
ohos:layout_alignment="center"
ohos:top_margin="23vp"/>
</DirectionalLayout>
</DirectionalLayout>
<DirectionalLayout
ohos:height="0vp"
ohos:width="match_parent"
ohos:alignment="vertical_center"
ohos:orientation="horizontal"
ohos:weight="2">
<Text
ohos:id="$+id:control_current_time"
ohos:height="match_content"
ohos:width="match_content"
ohos:end_margin="4vp"
ohos:start_margin="$float:default_margin"
ohos:text=""
ohos:text_color="$color:default_white_color"
ohos:text_size="12vp"/>
<Slider
ohos:id="$+id:control_progress"
ohos:height="10vp"
ohos:width="0vp"
ohos:orientation="horizontal"
ohos:progress_color="#FF6103"
ohos:thumb_element="$graphic:background_slide_thumb"
ohos:weight="5"/>
<Text
ohos:id="$+id:control_end_time"
ohos:height="match_content"
ohos:width="match_content"
ohos:end_margin="$float:default_margin"
ohos:text=""
ohos:text_color="$color:default_white_color"
ohos:text_size="12vp"/>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:bottom_margin="48vp"
ohos:start_margin="16vp"
ohos:top_margin="48vp">
<StackLayout
ohos:height="26vp"
ohos:width="match_parent">
<DirectionalLayout
ohos:height="match_parent"
ohos:width="match_parent">
<Text
ohos:height="match_parent"
ohos:width="match_parent"
ohos:text="$string:control_episodes"
ohos:text_alignment="vertical_center"
ohos:text_color="#000000"
ohos:text_size="18fp"/>
</DirectionalLayout>
<DirectionalLayout
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="right"
ohos:orientation="horizontal">
<Text
ohos:id="$+id:control_episodes_num"
ohos:height="match_parent"
ohos:width="match_content"
ohos:background_element="$graphic:background_button_click"
ohos:text=""
ohos:text_color="$color:default_black_color"
ohos:text_size="14fp"/>
<Image
ohos:id="$+id:control_all_episodes"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:background_element="$graphic:background_button_click"
ohos:end_margin="8vp"
ohos:foreground_element="$media:ic_right_arrow"
ohos:layout_alignment="center"/>
</DirectionalLayout>
</StackLayout>
<TableLayout
ohos:id="$+id:cotrol_bottom_item"
ohos:height="match_content"
ohos:width="match_parent"
ohos:below="$id:episodes_header"
ohos:column_count="6"
ohos:top_margin="12vp">
</TableLayout>
</DirectionalLayout>
</DirectionalLayout>
</DependentLayout>
1.2.页面布局,选择设备组件布局页 remote_ability_select_devices.xml
使用了DependentLayout,DirectionalLayout布局组件 和 ListContainer 等组件.
<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="vertical_center"
ohos:background_element="$color:default_panel_background"
ohos:orientation="vertical">
<DependentLayout
ohos:height="100vp"
ohos:width="match_parent"
ohos:background_element="$graphic:background_ability_devices"
ohos:end_margin="12vp"
ohos:end_padding="$float:default_margin"
ohos:layout_alignment="vertical_center"
ohos:start_margin="12vp"
ohos:start_padding="$float:default_margin">
<Text
ohos:id="$+id:devices_title"
ohos:height="match_content"
ohos:width="match_parent"
ohos:text="$string:local_machine"
ohos:text_color="$color:default_black_color"
ohos:text_size="14fp"
ohos:top_margin="12vp"/>
<Image
ohos:id="$+id:devices_head_icon"
ohos:height="$float:default_image_size"
ohos:width="$float:default_image_size"
ohos:below="$id:devices_title"
ohos:foreground_element="$media:icon"
ohos:top_margin="20vp"/>
<DirectionalLayout
ohos:height="match_content"
ohos:width="match_parent"
ohos:below="$id:devices_title"
ohos:end_of="$id:devices_head_icon"
ohos:orientation="vertical"
ohos:start_padding="12vp"
ohos:top_margin="12vp">
<Text
ohos:id="$+id:devices_head_app_name"
ohos:height="match_content"
ohos:width="match_parent"
ohos:max_height="28vp"
ohos:text=""
ohos:text_color="$color:default_black_color"
ohos:text_size="14fp"/>
<Text
ohos:id="$+id:devices_head_video_name"
ohos:height="18vp"
ohos:width="match_parent"
ohos:text=""
ohos:text_color="#99000000"
ohos:text_size="12fp"
ohos:truncation_mode="ellipsis_at_end"/>
</DirectionalLayout>
</DependentLayout>
<DirectionalLayout
ohos:height="300vp"
ohos:width="match_parent"
ohos:background_element="$graphic:background_ability_devices"
ohos:end_margin="12vp"
ohos:orientation="vertical"
ohos:padding="$float:default_margin"
ohos:start_margin="12vp"
ohos:top_margin="12vp">
<Text
ohos:height="21vp"
ohos:width="match_parent"
ohos:bottom_margin="10vp"
ohos:text="$string:my_devices"
ohos:text_color="$color:default_black_color"
ohos:text_size="16vp"/>
<ListContainer
ohos:id="$+id:devices_container"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:layout_alignment="horizontal_center"
ohos:orientation="vertical"/>
</DirectionalLayout>
</DirectionalLayout>
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#bkwz
https://harmonyos.51cto.com/#bkwz
::: hljs-center
:::