HarmonyOS上視頻跨設備協同技術超全詳解
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。
● 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. 參考