(目录)

DistributedVideoPlayer 分布式视频播放器(二)

介绍

上一期我们实现了视频的播放功能,播放列表还有评论功能.这一期,我们来看一下手机端是如何实现一个对远端TV视频播放的遥控功能. [本文正在参与优质创作者激励]

效果展示

动画32.gif

搭建环境

安装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 布局组件 和 其他常用的组件. image.png

<?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 等组件. e270110b9a6a147478116806f064705.png

<?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

想了解更多关于鸿蒙的内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com/#bkwz

::: hljs-center

21_9.jpg

:::