1. 介绍 此篇Codelab文档是基于实现一个简易播放器的高阶篇。在已实现视频播放功能的基础上,实现分布式跨设备视频操作。应用效果图如下:
在上个任务中,已列出需要的硬件、软件、技能要求、证书申请及编译方法,此处不再赘述。
你将会学到什么- 如何使用PageSlider、PageSliderIndicator和ListContainer编写定时滚动及可滑动的页面。
- 如何使用分布式能力实现跨设备视频播放。
- 如何使用HarmonyOS IDL跨进程通信实现远程控制视频播放。
技能要求- HarmonyOS Player接口熟练使用
- 基本组件熟练使用
2. 搭建HarmonyOS环境我们首先需要完成HarmonyOS开发环境搭建,可参照如下步骤进行。
- 安装DevEco Studio,详情请参考下载和安装软件。
- 设置DevEco Studio开发环境,DevEco Studio开发环境需要依赖于网络环境,需要连接上网络才能确保工具的正常使用,可以根据如下两种情况来配置开发环境:
- 如果可以直接访问Internet,只需进行下载HarmonyOS SDK操作。
- 如果网络不能直接访问Internet,需要通过代理服务器才可以访问,请参考配置开发环境。
- 开发者可以参考以下链接,完成设备调试的相关配置:
3. 代码结构 实现一个简易播放器已对视频播放和播放界面代码结构做了讲解,本次Codelab只对视频列表页、视频迁移设备列表、迁移后控制界面及迁移服务核心代码做讲解,对于完整代码,我们会在7 参考提供下载方式。代码结构图如下:
- provider:该目录包含CommonProvider、ViewProvider和AdvertisementProvider。CommonProvider是一个ListContainer 多样式提供者管理类。ViewProvider结合CommonProvider使用,可以把布局文件中需要赋值的控件单独提取出来进行赋值。AdvertisementProvider实现广告视频资源定时滚动的效果。
- ImplVideoMigration.idl:接口中定义了视频迁入、迁出、根据控制码对视频进行远程控制方法。
- data:该目录包括滚动视频广告对象封装、即将上映视频对象封装以及视频图片格式定义。
- VideoMigrateService:供远端连接的Service Ability。
- manager:该目录下的文件为ImplVideoMigration.idl在编译时自行生成,初始生成位置为entrybuildgeneratedsourceidlcomhuaweicodelab。
- MediaUtil:对广告和视频列表对象初始化赋值。
- config.json:配置文件,新增权限配置如下图:
- ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。
- ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。
- ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息。
- ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。
- ohos.permission.INTERNET:用于允许设备访问网络。
4. 创建应用程序布局文件
在路径"resources/base/layout"文件夹下创建video.xml为应用主页面,展示要播放的视频列表。
- <DirectionalLayout xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:width="match_parent"
- ohos:height="match_parent"
- ohos:orientation="vertical">
- <DirectionalLayout
- ohos:height="match_content"
- ohos:width="match_parent"
- ohos:orientation="vertical"
- >
- <!--滚动的视频图片-->
- <DependentLayout
- ohos:id="$+id:video_advertisement_container_view"
- ohos:width="match_parent"
- ohos:left_margin="20vp"
- ohos:height="175vp"
- ohos:top_margin="20vp"
- ohos:right_margin="12vp"
- >
- <PageSlider
- ohos:id="$+id:video_advertisement_viewpager"
- ohos:width="match_parent"
- ohos:height="match_parent"
- ohos:orientation="horizontal"/>
-
- <PageSliderIndicator
- ohos:id="$+id:video_advertisement_indicator"
- ohos:right_margin="8vp"
- ohos:bottom_margin="7vp"
- ohos:width="match_content"
- ohos:height="match_content"
- ohos:align_parent_bottom="true"
- ohos:align_parent_right="true" />
- </DependentLayout>
- <!--即将上映-->
- <DirectionalLayout
- ohos:width="match_parent"
- ohos:height="22vp"
- ohos:top_margin="12vp"
- ohos:left_margin="24vp"
- ohos:right_margin="12vp"
- ohos:orientation="horizontal">
- <Text
- ohos:id="$+id:video_play_title"
- ohos:text="Coming soon"
- ohos:text_size="16fp"
- ohos:text_color="#ff000000"
- ohos:text_alignment="4"
- ohos:layout_alignment="vertical_center"
- ohos:width="match_content"
- ohos:height="match_content" />
- <Image
- ohos:left_margin="6vp"
- ohos:width="13vp"
- ohos:height="13vp"
- ohos:layout_alignment="vertical_center"
- ohos:image_src="$media:ic_next"/>
-
- </DirectionalLayout>
- <!--可横向滑动的视频图片-->
- <DirectionalLayout
- ohos:width="match_parent"
- ohos:height="500vp"
- ohos:orientation="vertical">
- <ListContainer
- ohos:id="$+id:video_list_play_view"
- ohos:width="match_parent"
- ohos:height="match_content"
- ohos:orientation="horizontal"
- ohos:left_margin="18vp"
- ohos:top_margin="12vp"
- >
- </ListContainer>
- </DirectionalLayout>
- </DirectionalLayout>
-
- </DirectionalLayout>
复制代码
video.xml采用垂直方向的线性布局方式。整个页面分为三部分的内容。从上至下依次是PageSlider滚动广告布局,即将上映视频图标布局,可左右滑动的ListContainer布局。
PageSlider是一个描述滚动页面的组件,PageSliderIndicator是一个将滚动页面组件和其它组件比如图标、按钮等组合管理的管理器。本应用程序展示的滚动广告页面采取的是三组广告图片和图片title组成的PageSlider,广告图片和图片title组合样式由AdvertisementProvider定义。AdvertisementMo初始化代码如下:
- public AdvertisementMo(int sourceId, String description) {
- this.sourceId = sourceId;
- this.description = description;
- }
- videoAdvertisementMos.add(new AdvertisementMo(ResourceTable.Media_video_advertisement0, "玩心释放 尽情创想"));
- videoAdvertisementMos.add(new AdvertisementMo(ResourceTable.Media_video_advertisement1, "玩心释放 尽情创想"));
- videoAdvertisementMos.add(new AdvertisementMo(ResourceTable.Media_video_advertisement2, "一起创造 焕新假期"));
复制代码
AdvertisementProvider对滚动视频广告组件以list形式进行封装。
- public class AdvertisementProvider<T extends Component> extends PageSliderProvider {
- private List<T> componentList;
- public AdvertisementProvider(List<T> componentList) {
- this.componentList = componentList;
- }
- }
复制代码
通过PageSlider的setProvider(CommProvider)方法即可达到对图片列表地滚动显示效果。
- advertisementProvider = new AdvertisementProvider<Component>(getAdvertisementComponents());
- Component advViewPager = findComponentById(ResourceTable.Id_video_advertisement_viewpager);
- if (advViewPager instanceof PageSlider) {
- advPageSlider = (PageSlider) advViewPager;
- advPageSlider.setProvider(advertisementProvider);
- }
复制代码
getAdertisementCompoents方法将滚动视频广告添加到list。
- private List<Component> getAdvertisementComponents() {
- List<AdvertisementMo> advertisementMos = MediaUtil.getVideoAdvertisementInfo();
- List<Component> componentList = new ArrayList<>(advertisementMos.size());
- Font.Builder fb = new Font.Builder(VideoTabStyle.BOLD_FONT_NAME);
- fb.setWeight(Font.BOLD);
- Font newFont = fb.build();
- for (AdvertisementMo advertisementMo : advertisementMos) {
- Component advRootView = LayoutScatter.getInstance(getContext()).parse(
- ResourceTable.Layout_video_advertisement_item, null, false);
- Image imgTemp = null;
- if (advRootView.findComponentById(ResourceTable.Id_video_advertisement_poster) instanceof Image) {
- imgTemp = (Image) advRootView.findComponentById(ResourceTable.Id_video_advertisement_poster);
- }
- imgTemp.setPixelMap(advertisementMo.getSourceId());
- Text titleTmp = null;
- if (advRootView.findComponentById(ResourceTable.Id_video_advertisement_title) instanceof Text) {
- titleTmp = (Text) advRootView.findComponentById(ResourceTable.Id_video_advertisement_title);
- }
- titleTmp.setText(advertisementMo.getDescription());
- titleTmp.setFont(newFont);
- componentList.add(advRootView);
- }
-
- return componentList;
- }
复制代码
想要实现滚动到某一特定图片时呈现标志,在图片上方加上一组空心圆,当滚动到第一张图片时,第一个圆变为实心,此联动实现效果可通过PageSliderIndicator实现。
- PageSliderIndicator advIndicator = null;
- if (findComponentById(ResourceTable.Id_video_advertisement_indicator) instanceof PageSliderIndicator) {
- advIndicator = (PageSliderIndicator) findComponentById(
- ResourceTable.Id_video_advertisement_indicator);
- }
- advIndicator.setItemOffset(VideoTabStyle.INDICATOR_OFFSET);
复制代码
实心圆效果:
- ShapeElement normalDrawable = new ShapeElement();
- normalDrawable.setRgbColor(RgbColor.fromRgbaInt(Color.WHITE.getValue()));
- normalDrawable.setAlpha(VideoTabStyle.INDICATOR_NORMA_ALPHA);
- normalDrawable.setShape(ShapeElement.OVAL);
- normalDrawable.setBounds(0, 0, VideoTabStyle.INDICATOR_BONDS, VideoTabStyle.INDICATOR_BONDS);
复制代码
空心圆效果:
- ShapeElement selectedDrawable = new ShapeElement();
- selectedDrawable.setRgbColor(RgbColor.fromRgbaInt(Color.WHITE.getValue()));
- selectedDrawable.setShape(ShapeElement.OVAL);
- selectedDrawable.setBounds(0, 0, VideoTabStyle.INDICATOR_BONDS, VideoTabStyle.INDICATOR_BONDS);
复制代码
实心圆、空心圆效果如下图:
PageSliderIndicator通过设置可选类型将会实现图片被选中时,将会显示实心圆。
- advIndicator.setItemElement(normalDrawable, selectedDrawable);
- advIndicator.setViewPager((PageSlider) advViewPager);
复制代码
本节任务完成的效果如下图:
视频播放业务本次Codelab不再描述,下面直接进入视频流转环节。
5. 视频跨设备协同 HarmonyOS提供了分布式跨设备能力,本小节可以实现将视频协同到分布式环境中的其它设备上,原设备可以实现对协同设备的视频操作控制。
首先对视频播放界面中迁移按钮增加监听事件,在点击时,从窗口底部滑出分布式设备列表界面可供选择迁移。
- tv = (Image) simplePlayerController.findComponentById(ResourceTable.Id_tv);
- tv.setClickedListener(new Component.ClickedListener() {
- [url=home.php?mod=space&uid=2735960]@Override[/url]
- public void onClick(Component component) {
- initDevices();
- showDeviceList();
- }
- });
复制代码
通过分布式设备管理器DeviceManager获取到当前分布式网络中可发现的所有设备并全部添加到设备列表。如果设备列表初始不为空,先将列表清空,再添加,以达到刷新设备列表效果。
- private void initDevices() {
- if (devices.size() > 0) {
- devices.clear();
- }
- // 通过FLAG_GET_ONLINE_DEVICE标记获得在线设备列表
- List<DeviceInfo> deviceInfos = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
- devices.addAll(deviceInfos);
- }
复制代码
使用单样式的内容提供器CommonProvider设置设备名字样式,在设备列表中显示。
- private void showDeviceList() {
- CommonProvider commonProvider = new CommonProvider<DeviceInfo>(devices,getContext(), ResourceTable.Layout_device_list_item) {
- @Override
- protected void convert(ViewProvider viewProvider, DeviceInfo item, int position) {
- viewProvider.setText(ResourceTable.Id_device_text, item.getDeviceName());
- }
- };
- // 对deviceListContainer注入commonProvider,完成设备列表资源样式设置
- deviceListContainer.setItemProvider(commonProvider);
- // 通知列表数据发生变化更新设备列表
- commonProvider.notifyDataChanged();
- transWindow.show();
- }
复制代码
创建设备列表显示组件SlidePopupWindow。设备列表是一个从底部滑出的一个窗口,属于自定义组件。核心功能是设备列表的显示与隐藏。
- public void show() {
- if (!isShow) {
- isShow = true;
- animatorProperty
- .moveFromX(startX)
- .moveToX(endX)
- .moveFromY(startY)
- .moveToY(endY)
- .setCurveType(Animator.CurveType.LINEAR)
- .setDuration(ANIM_DURATION)
- .start();
- }
- }
-
- public void hide() {
- if (isShow) {
- isShow = false;
- animatorProperty
- .moveFromX(endX)
- .moveToX(startX)
- .moveFromY(endY)
- .moveToY(startY)
- .setCurveType(Animator.CurveType.LINEAR)
- .setDuration(ANIM_DURATION)
- .start();
- }
- }
复制代码
设备列表效果如下图:
点击列表中某一个设备,将在已选设备端拉起该视频应用。
- deviceListContainer.setItemClickedListener(new ListContainer.ItemClickedListener() {
- @Override
- public void onItemClicked(ListContainer listContainer, Component component, int num, long id) {
- // 列表窗口隐藏
- transWindow.hide();
- startAbilityFa(devices.get(num).getDeviceId());
- }
- });
复制代码
通过startAbilityFa()跨设备拉起视频FA,再调用connectAbility()异步对远端服务连接,成功连接后,在回调onAbilityConnectDone中服务端恢复视频数据。
- private void startAbilityFa(String devicesId) {
- Intent intent = new Intent();
- Operation operation =
- new Intent.OperationBuilder()
- .withDeviceId(devicesId)
- .withBundleName(getBundleName())
- .withAbilityName(VideoMigrateService.class.getName())
- .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
- .build();// 开发者需要在Intent中设置支持分布式的标记FLAG_ABILITYSLICE_MULTI_DEVICE,否则无法获得分布式能力
- intent.setOperation(operation);
- boolean connectFlag = connectAbility(intent,
- new IAbilityConnection() {
- @Override
- public void onAbilityConnectDone(
- ElementName elementName, IRemoteObject remoteObject, int resultCode) {
- // asInterface的作用是根据调用的服务是否属于同进程而返回不同的实例对象
- implVideoMigration = VideoMigrationStub.asInterface(remoteObject);
- try {
- implVideoMigration.flyIn(startMillisecond);
- } catch (RemoteException e) {
- LogUtil.error(TAG, "connect successful,but have remote exception");
- }
- }
-
- @Override
- public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
- disconnectAbility(this);
- }
- });
- if (connectFlag) {
- Toast.toast(this, "migrate successful!", TOAST_DURATION);
- remoteController.show();
- startMillisecond = implPlayer.getAudioCurrentPosition();// 获取视频当前播放进度
- implPlayer.release();// 释放资源
- } else {
- Toast.toast(this, "migrate failed!Please try again later.", TOAST_DURATION);
- }
- }
复制代码
通过指定abilityName为VideoMigrateService,执行VideoMigrateService中onConnect(intent)方法,返回binder对象,回调onAbilityConnectDone拿到具体的binder对象。VideoMigrationStub.asInterface(remoteObject)根据调用是否属于同进程而返回不同的实例对象, 由于返回的binder不是本进程的,所以返回的是VideoMigrationProxy对象。
接下来我们分别把本端设备称为设备A,跨设备协同端称为设备B。 implVideoMigration.flyIn(startMillisecond);由设备A即VideoMigrationProxy执行,通过sendRequest发送到设备B。
- remote.sendRequest(COMMAND_FLY_IN, data, reply, option);
复制代码
设备B通过接收到的code类型为COMMAND_FLY_IN在服务端执行视频数据恢复。
- @Override
- public void flyIn(int startTimemiles) throws RemoteException {
- Intent intent = new Intent();
- Operation operation =
- new Intent.OperationBuilder()
- .withBundleName(getBundleName())
- .withAbilityName(MainAbility.class.getName())
- .withAction("action.video.play")
- .build();
- intent.setOperation(operation);
- intent.setParam(Constants.INTENT_STARTTIME_PARAM, startTimemiles);
- startAbility(intent);
- }
复制代码
设备B呈现播放界面并跳转到Intent中携带的播放位置。在设备A的视频应用跨设备协同到设备B时,设备A会释放掉视频资源并展示RemoteController。
- if (connectFlag) {
- Toast.toast(this, "migrate successful!", TOAST_DURATION);
- remoteController.show();// 控制界面出现
- startMillisecond = implPlayer.getAudioCurrentPosition();
- implPlayer.release();
- }
复制代码
设备A的RemoteController在创建时初始化界面布局。通过操作界面控件来控制设备B视频播放。例如点击前进按钮,RemoteController发送FORWARD 控制码。SimplePlayerAbilitySlice通过添加RemoteController.RemoteControllerListener来执行回调方法sendControl,再通过implVideoMigration代理对象与对端进行通信。
- remoteController.setRemoteControllerCallback(new RemoteController.RemoteControllerListener() {
- @Override
- public void sendControl(int code, int extra) {
- try {
- if (implVideoMigration != null) {
- // 调用设备A服务代理对象的playControl方法通过binder对象调用设备B服务端的playControl方法
- implVideoMigration.playControl(code, extra);
- }
- } catch (RemoteException e) {
- LogUtil.error(TAG, "RemoteException occurs ");
- }
- }
- });
复制代码
设备A效果如下图:
设备B效果如下图:
当设备A在RemoteController界面执行返回操作时,会隐藏RemoteController,同时设备A继续播放。
- public void hide() {
- if (isShown) {
- isShown = false;
- setVisibility(INVISIBLE);
- if (remoteControllerListener != null) {
- remoteControllerListener.controllerDismiss();
- }
- }
- }
- remoteController.setRemoteControllerCallback(new RemoteController.RemoteControllerListener() {
- @Override
- public void controllerDismiss() {
- int progress = 0;
- try {
- if (implVideoMigration!= null) {
- // 迁回视频时获取进度条进度
- progress = implVideoMigration.flyOut();
- }
- } catch (RemoteException e) {
- LogUtil.e(TAG, "RemoteException occurs");
- }
- // 设备A视频按照迁回的视频进度继续播放
- implPlayer.reload(url, progress);
- }
- });
复制代码
说明
以上代码仅demo演示参考使用,产品化的代码需要使用国际化。
6. 恭喜你 - 通过使用PageSlider、PageSliderIndicator结合ListContainer编写定时滚动及可滑动的页面。
- HarmonyOS通过DeviceManger获取分布式网络中设备列表,选中设备ID之后,再通过IDL跨进程通信方式将FA或PA携带数据跨设备拉起。
- 整体运行效果图如下:
设备A视频跨设备协同后效果图如下:
至此,你已经完成HarmonyOS上视频跨设备协同的体验!
7. 参考