您现在的位置是:亿华云 > 系统运维

HarmonyOS上视频跨设备协同技术超全详解

亿华云2025-10-02 19:13:00【系统运维】6人已围观

简介想了解更多内容,请访问:和华为官方合作共建的鸿蒙技术社区https://harmonyos.51cto.com1. 介绍您将会学到什么● 如何使用PageSlider、PageSliderIndica

想了解更多内容,上视频术超请访问:

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

https://harmonyos.51cto.com

1. 介绍

您将会学到什么

● 如何使用PageSlider、PageSliderIndicator和ListContainer编写定时滚动及可滑动的备协页面。

● 如何使用分布式能力实现跨设备视频播放。同技

● 如何使用HarmonyOS IDL跨进程通信实现远程控制视频播放。全详

技能要求

● HarmonyOS Player接口熟练使用

● 基本组件熟练使用

🕮 说明

本篇Codelab所附代码适合在真机运行。上视频术超运行时需要至少两台手机处于同一个分布式网络中,跨设可以通过操作如下配置实现:

● 所有手机接入同一网络

● 所有手机登录相同华为账号

● 所有手机上开启“设置->更多连接->多设备协同 ”

2. 代码结构

在鸿蒙上实现本地和Internet视频资源播放已对视频播放和播放界面代码结构做了讲解,备协本次Codelab只对视频列表页、同技视频迁移设备列表、全详迁移后控制界面及迁移服务核心代码做讲解,上视频术超对于完整代码,跨设我们会在参考提供下载方式。备协代码结构图如下:

● provider:该目录包含CommonProvider、同技ViewProvider和AdvertisementProvider。全详CommonProvider是一个ListContainer 多样式提供者管理类。ViewProvider结合CommonProvider使用,可以把布局文件中需要赋值的控件单独提取出来进行赋值。AdvertisementProvider实现广告视频资源定时滚动的效果。

● ImplVideoMigration.idl:接口中定义了视频迁入、迁出、根据控制码对视频进行远程控制方法。

● data:该目录包括滚动视频广告对象封装、即将上映视频对象封装以及视频图片格式定义。

● VideoMigrateService:供远端连接的Service Ability。

● manager:该目录下的文件为ImplVideoMigration.idl在编译时自行生成,初始生成位置为entry\build\generated\source\idl\com\huawei\codelab。云南idc服务商

● MediaUtil:对广告和视频列表对象初始化赋值。

● config.json:配置文件,新增权限配置如下图:

1. ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化。

2. ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息。

3. ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息。

4. ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换。

5. ohos.permission.INTERNET:用于允许设备访问网络。

3. 创建应用程序布局文件

在路径"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不再描述,下面直接进入视频流转环节。

4. 视频跨设备协同

HarmonyOS提供了分布式跨设备能力,本小节可以实现将视频迁移到分布式环境中的其它设备上,被迁移设备可以实现对迁移设备的视频操作控制。

首先对视频播放界面中迁移按钮增加监听事件,在点击时,从窗口底部滑出分布式设备列表界面可供选择迁移。

tv = (Image) simplePlayerController.findComponentById(ResourceTable.Id_tv);  tv.setClickedListener(new Component.ClickedListener() {       @Override      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 l) {           // 列表窗口隐藏          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 i) {                               // 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 i) {                               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演示参考使用,产品化的代码需要使用国际化。

5. 恭喜你

● 通过使用PageSlider、PageSliderIndicator结合ListContainer编写定时滚动及可滑动的页面。

● HarmonyOS通过DeviceManger获取分布式网络中设备列表,选中设备ID之后,再通过IDL跨进程通信方式将FA或PA携带数据跨设备拉起。

● 整体运行效果图如下:

设备A视频跨设备协同后效果图如下:

至此,您已经完成HarmonyOS上视频跨设备协同的体验!

6. 参考

gitee源码

github源码

想了解更多内容,请访问:

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

https://harmonyos.51cto.com

很赞哦!(7417)