HarmonyOS上視頻跨設(shè)備協(xié)同技術(shù)超全詳解
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
1. 介紹
您將會學(xué)到什么
● 如何使用PageSlider、PageSliderIndicator和ListContainer編寫定時滾動及可滑動的頁面。
● 如何使用分布式能力實(shí)現(xiàn)跨設(shè)備視頻播放。
● 如何使用HarmonyOS IDL跨進(jìn)程通信實(shí)現(xiàn)遠(yuǎn)程控制視頻播放。
技能要求
● HarmonyOS Player接口熟練使用
● 基本組件熟練使用
🕮 說明
本篇Codelab所附代碼適合在真機(jī)運(yùn)行。運(yùn)行時需要至少兩臺手機(jī)處于同一個分布式網(wǎng)絡(luò)中,可以通過操作如下配置實(shí)現(xiàn):
● 所有手機(jī)接入同一網(wǎng)絡(luò)
● 所有手機(jī)登錄相同華為賬號
● 所有手機(jī)上開啟“設(shè)置->更多連接->多設(shè)備協(xié)同 ”
2. 代碼結(jié)構(gòu)
在鴻蒙上實(shí)現(xiàn)本地和Internet視頻資源播放已對視頻播放和播放界面代碼結(jié)構(gòu)做了講解,本次Codelab只對視頻列表頁、視頻遷移設(shè)備列表、遷移后控制界面及遷移服務(wù)核心代碼做講解,對于完整代碼,我們會在參考提供下載方式。代碼結(jié)構(gòu)圖如下:
● provider:該目錄包含CommonProvider、ViewProvider和AdvertisementProvider。CommonProvider是一個ListContainer 多樣式提供者管理類。ViewProvider結(jié)合CommonProvider使用,可以把布局文件中需要賦值的控件單獨(dú)提取出來進(jìn)行賦值。AdvertisementProvider實(shí)現(xiàn)廣告視頻資源定時滾動的效果。
● ImplVideoMigration.idl:接口中定義了視頻遷入、遷出、根據(jù)控制碼對視頻進(jìn)行遠(yuǎn)程控制方法。
● data:該目錄包括滾動視頻廣告對象封裝、即將上映視頻對象封裝以及視頻圖片格式定義。
● VideoMigrateService:供遠(yuǎn)端連接的Service Ability。
● manager:該目錄下的文件為ImplVideoMigration.idl在編譯時自行生成,初始生成位置為entry\build\generated\source\idl\com\huawei\codelab。
● MediaUtil:對廣告和視頻列表對象初始化賦值。
● config.json:配置文件,新增權(quán)限配置如下圖:
1. ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允許監(jiān)聽分布式組網(wǎng)內(nèi)的設(shè)備狀態(tài)變化。
2. ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允許獲取分布式組網(wǎng)內(nèi)的設(shè)備列表和設(shè)備信息。
3. ohos.permission.GET_BUNDLE_INFO:用于查詢其他應(yīng)用的信息。
4. ohos.permission.DISTRIBUTED_DATASYNC:用于允許不同設(shè)備間的數(shù)據(jù)交換。
5. ohos.permission.INTERNET:用于允許設(shè)備訪問網(wǎng)絡(luò)。
3. 創(chuàng)建應(yīng)用程序布局文件
在路徑"resources/base/layout"文件夾下創(chuàng)建video.xml為應(yīng)用主頁面,展示要播放的視頻列表。
- <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采用垂直方向的線性布局方式。整個頁面分為三部分的內(nèi)容。從上至下依次是PageSlider滾動廣告布局,即將上映視頻圖標(biāo)布局,可左右滑動的listContainer布局。
PageSlider是一個描述滾動頁面的組件,PageSliderIndicator是一個將滾動頁面組件和其它組件比如圖標(biāo)、按鈕等組合管理的管理器。本應(yīng)用程序展示的滾動廣告頁面采取的是三組廣告圖片和圖片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, "玩心釋放 盡情創(chuàng)想"));
- videoAdvertisementMos.add(new AdvertisementMo(ResourceTable.Media_video_advertisement1, "玩心釋放 盡情創(chuàng)想"));
- videoAdvertisementMos.add(new AdvertisementMo(ResourceTable.Media_video_advertisement2, "一起創(chuàng)造 煥新假期"));
AdvertisementProvider對滾動視頻廣告組件以list形式進(jìn)行封裝。
- public class AdvertisementProvider<T extends Component> extends PageSliderProvider {
- private List<T> componentList;
- public AdvertisementProvider(List<T> componentList) {
- this.componentList = componentList;
- }
- }
通過PageSlider對象的setProvider(CommProvider)方法即可達(dá)到對圖片列表地滾動顯示效果。
- 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;
- }
想要實(shí)現(xiàn)滾動到某一特定圖片時呈現(xiàn)標(biāo)志,在圖片上方加上一組空心圓,當(dāng)滾動到第一張圖片時,第一個圓變?yōu)閷?shí)心,此聯(lián)動實(shí)現(xiàn)效果可通過PageSliderIndicator實(shí)現(xiàn)。
- 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);
實(shí)心圓效果:
- 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);
實(shí)心圓、空心圓效果如下圖:
PageSliderIndicator通過設(shè)置可選類型將會實(shí)現(xiàn)圖片被選中時,將會顯示實(shí)心圓。
- advIndicator.setItemElement(normalDrawable, selectedDrawable);
- advIndicator.setViewPager((PageSlider) advViewPager);
本節(jié)任務(wù)完成的效果如下圖:
視頻播放業(yè)務(wù)本次Codelab不再描述,下面直接進(jìn)入視頻流轉(zhuǎn)環(huán)節(jié)。
4. 視頻跨設(shè)備協(xié)同
HarmonyOS提供了分布式跨設(shè)備能力,本小節(jié)可以實(shí)現(xiàn)將視頻遷移到分布式環(huán)境中的其它設(shè)備上,被遷移設(shè)備可以實(shí)現(xiàn)對遷移設(shè)備的視頻操作控制。
首先對視頻播放界面中遷移按鈕增加監(jiān)聽事件,在點(diǎn)擊時,從窗口底部滑出分布式設(shè)備列表界面可供選擇遷移。
- tv = (Image) simplePlayerController.findComponentById(ResourceTable.Id_tv);
- tv.setClickedListener(new Component.ClickedListener() {
- @Override
- public void onClick(Component component) {
- initDevices();
- showDeviceList();
- }
- });
通過分布式設(shè)備管理器DeviceManager獲取到當(dāng)前分布式網(wǎng)絡(luò)中可發(fā)現(xiàn)的所有設(shè)備并全部添加到設(shè)備列表。如果設(shè)備列表初始不為空,先將列表清空,再添加,以達(dá)到刷新設(shè)備列表效果。
- private void initDevices() {
- if (devices.size() > 0) {
- devices.clear();
- }
- // 通過FLAG_GET_ONLINE_DEVICE標(biāo)記獲得在線設(shè)備列表
- List<DeviceInfo> deviceInfos = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ONLINE_DEVICE);
- devices.addAll(deviceInfos);
- }
顯示設(shè)備列表使用單樣式的內(nèi)容提供器CommonProvider,設(shè)置設(shè)備名字樣式。
- 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,完成設(shè)備列表資源樣式設(shè)置
- deviceListContainer.setItemProvider(commonProvider);
- // 通知列表數(shù)據(jù)發(fā)生變化更新設(shè)備列表
- commonProvider.notifyDataChanged();
- transWindow.show();
- }
創(chuàng)建設(shè)備列表顯示組件SlidePopupWindow。設(shè)備列表是一個從底部滑出的一個窗口,屬于自定義組件。核心功能是設(shè)備列表的顯示與隱藏。
- 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();
- }
- }
設(shè)備列表效果如下圖:
點(diǎn)擊列表中某一個設(shè)備,將在已選設(shè)備端拉起該視頻應(yīng)用。
- 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()跨設(shè)備拉起視頻FA,再調(diào)用connectAbility()異步對遠(yuǎn)端服務(wù)連接,成功連接后,在回調(diào)onAbilityConnectDone中服務(wù)端恢復(fù)視頻數(shù)據(jù)。
- 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();// 開發(fā)者需要在Intent中設(shè)置支持分布式的標(biāo)記FLAG_ABILITYSLICE_MULTI_DEVICE,否則無法獲得分布式能力
- intent.setOperation(operation);
- boolean connectFlag = connectAbility(intent,
- new IAbilityConnection() {
- @Override
- public void onAbilityConnectDone(
- ElementName elementName, IRemoteObject remoteObject, int i) {
- // asInterface的作用是根據(jù)調(diào)用的服務(wù)是否屬于同進(jìn)程而返回不同的實(shí)例對象
- 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();// 獲取視頻當(dāng)前播放進(jìn)度
- implPlayer.release();// 釋放資源
- } else {
- Toast.toast(this, "migrate failed!Please try again later.", TOAST_DURATION);
- }
- }
通過指定abilityName為VideoMigrateService,執(zhí)行VideoMigrateService中onConnect(intent)方法,返回binder對象,回調(diào)onAbilityConnectDone拿到具體的binder對象。VideoMigrationStub.asInterface(remoteObject)根據(jù)調(diào)用是否屬于同進(jìn)程而返回不同的實(shí)例對象, 由于返回的binder不是本進(jìn)程的,所以返回的是VideoMigrationProxy對象。
接下來我們分別把本端設(shè)備稱為設(shè)備A,跨設(shè)備協(xié)同端稱為設(shè)備B。 implVideoMigration.flyIn(startMillisecond)由設(shè)備A即VideoMigrationProxy執(zhí)行,通過sendRequest發(fā)送到設(shè)備B。
- remote.sendRequest(COMMAND_FLY_IN, data, reply, option);
設(shè)備B通過接收到的code類型為COMMAND_FLY_IN在服務(wù)端執(zhí)行視頻數(shù)據(jù)恢復(fù)。
- @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);
- }
設(shè)備B呈現(xiàn)播放界面并跳轉(zhuǎn)到Intent中攜帶的播放位置。在設(shè)備A的視頻應(yīng)用跨設(shè)備協(xié)同到設(shè)備B時,設(shè)備A會釋放掉視頻資源并展示RemoteController。
- if (connectFlag) {
- Toast.toast(this, "migrate successful!", TOAST_DURATION);
- remoteController.show();// 控制界面出現(xiàn)
- startMillisecond = implPlayer.getAudioCurrentPosition();
- implPlayer.release();
- }
設(shè)備A的RemoteController在創(chuàng)建時初始化界面布局。通過操作界面控件來控制設(shè)備B視頻播放。例如點(diǎn)擊前進(jìn)按鈕,RemoteController發(fā)送FORWARD 控制碼。SimplePlayerAbilitySlice通過添加RemoteController.RemoteControllerListener來執(zhí)行回調(diào)方法sendControl,再通過implVideoMigration代理對象與對端進(jìn)行通信。
- remoteController.setRemoteControllerCallback(new RemoteController.RemoteControllerListener() {
- @Override
- public void sendControl(int code, int extra) {
- try {
- if (implVideoMigration != null) {
- // 調(diào)用設(shè)備A服務(wù)代理對象的playControl方法通過binder對象調(diào)用設(shè)備B服務(wù)端的playControl方法
- implVideoMigration.playControl(code, extra);
- }
- } catch (RemoteException e) {
- LogUtil.error(TAG, "RemoteException occurs ");
- }
- }
- });
設(shè)備A效果如下圖:
設(shè)備B效果如下圖:
當(dāng)設(shè)備A在RemoteController界面執(zhí)行返回操作時,會隱藏RemoteController,同時設(shè)備A繼續(xù)播放。
- 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) {
- // 遷回視頻時獲取進(jìn)度條進(jìn)度
- progress = implVideoMigration.flyOut();
- }
- } catch (RemoteException e) {
- LogUtil.e(TAG, "RemoteException occurs");
- }
- // 設(shè)備A視頻按照遷回的視頻進(jìn)度繼續(xù)播放
- implPlayer.reload(url, progress);
- }
- });
🕮 說明
以上代碼僅demo演示參考使用,產(chǎn)品化的代碼需要使用國際化。
5. 恭喜你
● 通過使用PageSlider、PageSliderIndicator結(jié)合ListContainer編寫定時滾動及可滑動的頁面。
● HarmonyOS通過DeviceManger獲取分布式網(wǎng)絡(luò)中設(shè)備列表,選中設(shè)備ID之后,再通過IDL跨進(jìn)程通信方式將FA或PA攜帶數(shù)據(jù)跨設(shè)備拉起。
● 整體運(yùn)行效果圖如下:
設(shè)備A視頻跨設(shè)備協(xié)同后效果圖如下:
至此,您已經(jīng)完成HarmonyOS上視頻跨設(shè)備協(xié)同的體驗!
6. 參考
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)

































