概述
系統(tǒng)投播功能讓用戶能夠輕松將手機(jī)上的音視頻投放到其他設(shè)備(如PC/2in1設(shè)備、華為智慧屏)上繼續(xù)播放,實(shí)現(xiàn)跨設(shè)備切換,帶來(lái)流暢的觀影體驗(yàn)。為簡(jiǎn)化開(kāi)發(fā)流程,系統(tǒng)提供了標(biāo)準(zhǔn)化的音視頻投播解決方案,開(kāi)發(fā)者僅需配置資源信息、監(jiān)聽(tīng)投播狀態(tài)并實(shí)現(xiàn)播放控制(如播放、暫停),即可快速集成該功能。
本文將結(jié)合實(shí)際案例,詳細(xì)介紹如何高效利用系統(tǒng)投播組件和接口實(shí)現(xiàn)視頻投播,幫助開(kāi)發(fā)者提升開(kāi)發(fā)效率,包含如下關(guān)鍵步驟:
接入播控中心:播控中心系統(tǒng)提供的播放管理模塊,可以后臺(tái)管理應(yīng)用播放任務(wù),是投播接入的必備條件。
本端控制遠(yuǎn)端設(shè)備狀態(tài):手機(jī)端實(shí)現(xiàn)遙控器功能,直接控制遠(yuǎn)端設(shè)備的播放狀態(tài)、進(jìn)度、音量等。
遠(yuǎn)端視頻狀態(tài)回傳本端:能夠?qū)崟r(shí)同步播放進(jìn)度至手機(jī)端顯示。
視頻資源切換和設(shè)備切換:支持投播過(guò)程中集數(shù)的切換及投播設(shè)備的切換。
用戶體驗(yàn)
體驗(yàn)
用戶體驗(yàn)路徑
本文案例提供本端播放和視頻投播兩種播放模式,體驗(yàn)路徑和交互流程圖如下。用戶可以在本端和遠(yuǎn)端播放視頻,在投播模式下,用戶可以通過(guò)遙控界面實(shí)現(xiàn)快進(jìn)/快退、切換上下集、音量調(diào)節(jié)(支持物理鍵控制)、進(jìn)度條拖動(dòng)跳轉(zhuǎn)、選集切換控制功能,應(yīng)用接入時(shí),可根據(jù)實(shí)際需求參考本文實(shí)現(xiàn),并按照應(yīng)用接入播控自檢表完成基礎(chǔ)功能驗(yàn)證,確保應(yīng)用基礎(chǔ)體驗(yàn)。

實(shí)現(xiàn)原理
名詞解釋

投播功能的實(shí)現(xiàn)基于AVSession媒體會(huì)話和AVCastController投播控制器的協(xié)同工作:系統(tǒng)通過(guò)AVSession建立設(shè)備連接,由AVCastController向Cast+服務(wù)發(fā)送控制指令。開(kāi)發(fā)者需要聚焦兩個(gè)核心環(huán)節(jié)——通過(guò)AVSession實(shí)現(xiàn)監(jiān)聽(tīng)設(shè)備連接,以及使用AVCastController控制遠(yuǎn)端播放并同步狀態(tài),詳見(jiàn)運(yùn)作機(jī)制。

模塊設(shè)計(jì)
建議應(yīng)用封裝三個(gè)模塊:
VideoPlayerController:應(yīng)用封裝的本地視頻控制器,控制本端視頻資源的暫停、播放、進(jìn)度、音量、倍速。
VideoSessionController:應(yīng)用封裝的媒體會(huì)話控制器,本端視頻播放時(shí)用于本應(yīng)用與播控中心的同步、切換設(shè)備發(fā)起投播、結(jié)束投播。
VideoCastController:應(yīng)用封裝的投播視頻控制器,控制遠(yuǎn)端設(shè)備視頻資源的暫停、播放、進(jìn)度、音量、倍速。
完成投播功能,建議參考如下流程接入,其中本端視頻顯示和控制可參考視頻播放組件、使用AVPlayer播放視頻(ArkTS)、使用AVPlayer播放視頻(C/C++)等視頻實(shí)現(xiàn)方案根據(jù)功能訴求自行實(shí)現(xiàn),本文從接入播控中心進(jìn)行介紹。

接入播控中心
投播功能依賴(lài)于播控中心,因此必須接入播控中心才能實(shí)現(xiàn)投播功能。播控中心不僅能夠控制本端設(shè)備的播放,還能控制遠(yuǎn)端設(shè)備的播放。本章節(jié)將簡(jiǎn)要介紹應(yīng)用接入播控中心的開(kāi)發(fā)流程。
媒體會(huì)話初始化
1.avSession.createAVSession()創(chuàng)建avsession,類(lèi)型為VIDEO_SESSION。
2.設(shè)置后臺(tái)長(zhǎng)時(shí)播放任務(wù),確保應(yīng)用退至后臺(tái)后播放不會(huì)停止。
3.videoSession.setLaunchAbility()設(shè)置一個(gè)WantAgent用于拉起會(huì)話的Ability。
4.videoSession.activate()激活videoSession。
letvideoSession =awaitavSession.createAVSession(context,'VIDEO_SESSION','video'); // Set up a background task. BackgroundTaskManager.startContinuousTask(context); constwantAgentInfo: wantAgent.WantAgentInfo= { wants: [ { bundleName: context.abilityInfo.bundleName, abilityName: context.abilityInfo.name } ], operationType: wantAgent.OperationType.START_ABILITIES, requestCode:0, wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG] }; letagent = wantAgent.getWantAgent(wantAgentInfo); videoSession.setLaunchAbility(agent); videoSession.activate(); returnnewVideoSessionController(videoSession);
設(shè)置媒體會(huì)話元數(shù)據(jù)
videoSession.setAVMetadata()上傳元數(shù)據(jù),從而在播控中心界面進(jìn)行展示。如媒體ID(assetId)、標(biāo)題(title)、播控中心顯示的圖片(mediaImage)、媒體時(shí)長(zhǎng)(duration)。
letmetadata: avSession.AVMetadata= {
assetId:`${curSource.index}`,
title: curSource.name,
mediaImage: headPixel,
duration: duration,
filter: avSession.ProtocolType.TYPE_DLNA| avSession.ProtocolType.TYPE_CAST_PLUS_STREAM
};
awaitthis.videoSession.setAVMetadata(metadata);
本應(yīng)用播放狀態(tài)同步到播控中心
當(dāng)設(shè)置元數(shù)據(jù)后,播控中心會(huì)顯示進(jìn)度條并自動(dòng)計(jì)算播放進(jìn)度,但播放狀態(tài)變更(如暫停、播放、進(jìn)度跳轉(zhuǎn))、音量調(diào)節(jié)和倍速設(shè)置等操作不會(huì)自動(dòng)同步到播控中心。開(kāi)發(fā)者需要主動(dòng)監(jiān)聽(tīng)本地的播放狀態(tài)變化(包括進(jìn)度跳轉(zhuǎn)、倍速調(diào)整、音量修改等事件),并主動(dòng)將這些狀態(tài)同步到播控中心,以確保兩端狀態(tài)一致。
以下是videoSession狀態(tài)更新的示例代碼,特別注意的是,在更新進(jìn)度狀態(tài)時(shí),需要傳入當(dāng)前時(shí)間戳updateTime和視頻播放的時(shí)間進(jìn)度elapsedTime。
awaitthis.videoSession.setAVPlaybackState({
state: state ==='playing'? avSession.PlaybackState.PLAYBACK_STATE_PLAY:
avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
});
播控中心控制應(yīng)用播放
當(dāng)用戶在播控中心進(jìn)行操作(如播放、暫停、停止、進(jìn)度跳轉(zhuǎn)、快進(jìn)、快退等)時(shí),這些操作不會(huì)自動(dòng)同步到應(yīng)用端,開(kāi)發(fā)者需要主動(dòng)通過(guò)avCastController.on('controlCommand')監(jiān)聽(tīng)這些事件,并在回調(diào)函數(shù)中主動(dòng)更新應(yīng)用播放器的狀態(tài)以保持同步,例如在收到播放指令時(shí)調(diào)用本地播放器的play()方法,在收到跳轉(zhuǎn)指令時(shí)調(diào)整播放進(jìn)度等,確保播控中心與應(yīng)用端的操作狀態(tài)完全一致。
this.videoSession.on('play',() =>avPlayerController.setAVPlayerPlaying());
this.videoSession.on('pause',() =>avPlayerController.setAVPlayerPause());
說(shuō)明: 這里注冊(cè)的交互監(jiān)聽(tīng)所有on()事件建議在退出播放頁(yè)時(shí)通過(guò)videoSession.off()事件銷(xiāo)毀。
投播基礎(chǔ)功能
為確保投播功能正常使用,應(yīng)用在發(fā)起投播前需要完成播控中心初始化。如未完成此關(guān)鍵步驟,則導(dǎo)致投播功能不可用。
創(chuàng)建投播
在完成創(chuàng)建投播后,遠(yuǎn)端設(shè)備即可正常播放視頻,本端會(huì)停止播放并頁(yè)面跳轉(zhuǎn)。
創(chuàng)建投播時(shí)需要setExtras()告知系統(tǒng)可投播、繪制AVCastPicker、videosession監(jiān)聽(tīng)設(shè)備改變事件,用戶點(diǎn)擊AVCastPicker組件后會(huì)彈出設(shè)備選擇半模態(tài),在選擇設(shè)備后,應(yīng)用需要設(shè)置投播媒體信息,調(diào)用prepare、start啟動(dòng)播放。時(shí)序圖如下,具體實(shí)現(xiàn)見(jiàn)開(kāi)發(fā)步驟:
時(shí)序圖

開(kāi)發(fā)步驟
1.videosession創(chuàng)建后,創(chuàng)建投播前,聲明當(dāng)前應(yīng)用支持投播。
awaitvideoSession.setExtras({
requireAbilityList: ['url-cast']
})
2.繪制AVCastPicker,AVCastPicker是投播組件,點(diǎn)擊后系統(tǒng)會(huì)彈出設(shè)備選擇半模態(tài)。
AVCastPicker({
normalColor: Color.White,
pickerStyle:AVCastPickerStyle.STYLE_PANEL,
sessionType:'video',
// ...
})
3.當(dāng)用戶選擇設(shè)備并設(shè)備切換成功后觸發(fā)videoSession.on('outputDeviceChange')事件,應(yīng)用可選擇停止本地播放并跳轉(zhuǎn)到遙控頁(yè)面(或保持本端繼續(xù)播放),此時(shí)播控中心會(huì)自動(dòng)接管遠(yuǎn)端設(shè)備的播放控制,開(kāi)發(fā)者無(wú)需額外設(shè)置。
videoSession.on('outputDeviceChange',async(connectState: avSession.ConnectionState,
device: avSession.OutputDeviceInfo) => {
hilog.info(0x0000,TAG,`device${JSON.stringify(device)}`);
hilog.info(0x0000,TAG,`connectState${JSON.stringify(connectState)}`);
// The linked device is a remote device.
if(device.devices[0].castCategory=== avSession.AVCastCategory.CATEGORY_REMOTE&&
connectState === avSession.ConnectionState.STATE_CONNECTED) {
// Page jump
this.remoteControlPathStack.replacePath({name:'detail',param:this.currentTime});
this.castingList.push(this.videoType);
awaitthis.releaseAVPlayer();
// The linked device is the local device.
}elseif(device.devices[0].castCategory=== avSession.AVCastCategory.CATEGORY_REMOTE&&
connectState === avSession.ConnectionState.STATE_DISCONNECTED) {
if(this.avCastController) {
awaitthis.avCastController.releaseAVCast();
awaitthis.avSessionController!.stopCasting();
this.avCastController=undefined;
}
}
// ...
})
4.設(shè)置avCastController資源,完成以下三步后遠(yuǎn)端設(shè)備即可投播視頻,以播放網(wǎng)絡(luò)資源為例。
1.構(gòu)建avSession.AVQueueItem。需要傳入assetId(播放列表媒體ID,應(yīng)用自定義)、title(媒體標(biāo)題)、artist(媒體專(zhuān)輯作者)、mediaUri(媒體URI)、mediaType(媒體類(lèi)型)、mediaImage(媒體圖片像素?cái)?shù)據(jù))、duration(媒體播放時(shí)長(zhǎng))。
2.avCastController.prepare(playItem)準(zhǔn)備播放媒體資源,即進(jìn)行播放資源的加載和緩沖。
3.avCastController.start(playItem)啟動(dòng)播放媒體資源。
letplayItem: avSession.AVQueueItem= {
itemId: videoIndex,
description: {
assetId:'VIDEO-'+JSON.stringify(videoIndex),
title:this.videoDataArray[videoIndex].name,
artist:'ExampleArtist',
mediaUri:this.videoDataArray[videoIndex].urlasstring,
mediaType:'VIDEO',
mediaImage: imgPixel,
mediaSize:1000,
startPosition: startPosition,
duration:this.videoDataArray[videoIndex].duration
}
};
awaitthis.avCastController.prepare(playItem);
awaitthis.avCastController.start(playItem);
若需要投播本地資源,需要打開(kāi)沙箱文件,并在fdSrc中傳入文件fd實(shí)現(xiàn)。
try{
letfile =awaitfileIo.open(context.filesDir+'/'+this.videoDataArray[videoIndex].url);
letavFileDescriptor: media.AVFileDescriptor= {fd: file.fd};
letplayItem: avSession.AVQueueItem= {
itemId: videoIndex,
description: {
assetId:'VIDEO-'+JSON.stringify(videoIndex),
title:this.videoDataArray[videoIndex].name,
artist:'ExampleArtist',
mediaType:'VIDEO',
mediaImage: imgPixel,
mediaSize:1000,
fdSrc: avFileDescriptor,
startPosition: startPosition,
duration:this.videoDataArray[videoIndex].duration
}
};
awaitthis.avCastController.prepare(playItem);
awaitthis.avCastController.start(playItem);
設(shè)備切換
設(shè)備切換依賴(lài)于videosession監(jiān)聽(tīng)設(shè)備改變事件,可以通過(guò)stopCasting終止投播切換設(shè)備,也可以通過(guò)avCastPicker.select()進(jìn)行切換。均會(huì)觸發(fā)videoSession.on('outputDeviceChange')事件,當(dāng)切換到遠(yuǎn)端設(shè)備播放,本端應(yīng)該跳轉(zhuǎn)到遙控器界面,當(dāng)切換回本端設(shè)備播放,應(yīng)當(dāng)停止投播并跳轉(zhuǎn)到視頻播放頁(yè)面。應(yīng)用時(shí)序圖如下,具體實(shí)現(xiàn)見(jiàn)開(kāi)發(fā)步驟。
時(shí)序圖

開(kāi)發(fā)步驟
可以直接使用AVCastPicker切換設(shè)備,系統(tǒng)會(huì)自動(dòng)彈出設(shè)備選擇半模態(tài)彈窗,用戶可直接選擇目標(biāo)設(shè)備完成切換。開(kāi)發(fā)者無(wú)需額外處理彈窗邏輯。也可以使用avCastPicker.select() 接口切換設(shè)備。
當(dāng)設(shè)備切換時(shí),videoSession.on
('outputDeviceChange')事件將被觸發(fā),開(kāi)發(fā)者可在回調(diào)中處理設(shè)備切換邏輯:若切換至遠(yuǎn)端設(shè)備則跳轉(zhuǎn)至遙控頁(yè)面,若切回本端設(shè)備則恢復(fù)本地播放,實(shí)現(xiàn)播放控制的無(wú)縫切換。
videoSession.on('outputDeviceChange', async (connectState: avSession.ConnectionState,
device: avSession.OutputDeviceInfo) => {
hilog.info(0x0000, TAG, `device ${JSON.stringify(device)}`);
hilog.info(0x0000, TAG, `connectState ${JSON.stringify(connectState)}`);
// The linked device is a remote device.
if(device.devices[0].castCategory === avSession.AVCastCategory.CATEGORY_REMOTE &&
connectState === avSession.ConnectionState.STATE_CONNECTED) {
// Page jump
this.remoteControlPathStack.replacePath({ name:'detail', param:this.currentTime });
this.castingList.push(this.videoType);
awaitthis.releaseAVPlayer();
// The linked device is the local device.
}elseif(device.devices[0].castCategory === avSession.AVCastCategory.CATEGORY_LOCAL) {
this.remoteControlPathStack.clear();
let videoType =this.castingList[0];
this.castingList = [];
let videoPlayParam = new VideoPlayParam(videoType,0,this.avplayerContinueIndex);
this.videoPlayPathStack.replacePath({ name:'detail', param: videoPlayParam });
if(this.avCastController) {
awaitthis.avCastController.releaseAVCast();
awaitthis.avSessionController!.stopCasting();
this.avCastController = undefined;
}
}
})
遠(yuǎn)端視頻狀態(tài)回傳本端
當(dāng)視頻在遠(yuǎn)端設(shè)備播放時(shí),為了控制遠(yuǎn)端視頻的播放應(yīng)用需要監(jiān)聽(tīng)遠(yuǎn)端視頻播放狀態(tài)并同步顯示本端,通過(guò)遠(yuǎn)端設(shè)備或本端播控中心控制,都會(huì)直接改變遠(yuǎn)端設(shè)備的播放狀態(tài),并觸發(fā)avCastController.on('playbackStateChange')。應(yīng)用時(shí)序圖如下,具體實(shí)現(xiàn)見(jiàn)開(kāi)發(fā)步驟。
時(shí)序圖

開(kāi)發(fā)步驟
當(dāng)需要在本地遙控界面同步顯示遠(yuǎn)端視頻的播放狀態(tài)時(shí),可通過(guò)avCastController.on
('playbackStateChange') 監(jiān)聽(tīng)狀態(tài)變化,并使用過(guò)濾器篩選目標(biāo)狀態(tài)。
建議使用@Track修飾器標(biāo)記這些經(jīng)常改變的狀態(tài)變量,以便頁(yè)面自動(dòng)響應(yīng)數(shù)據(jù)更新。該機(jī)制可統(tǒng)一獲取播放狀態(tài)(如播放/暫停)、音量、總時(shí)長(zhǎng)及倍速等信息,以下代碼以獲取已播放時(shí)長(zhǎng)為例:
@Observed
exportclassVideoCastController{
@Trackstate: avSession.PlaybackState= avSession.PlaybackState.PLAYBACK_STATE_INITIAL;
// ...
/**
* Sets up AV cast playback state change callbacks.
* Handles playback completion, position updates, volume changes and errors.
*/
setAVCastCallback() {
this.avCastController.on('playbackStateChange', ['state'],async(playbackState: avSession.AVPlaybackState) => {
if(playbackState.state) {
this.state= playbackState.state;
}
});
// ...
}
// ...
}
本端控制遠(yuǎn)端設(shè)備狀態(tài)
時(shí)序圖

開(kāi)發(fā)步驟
控制遠(yuǎn)端設(shè)備狀態(tài)可通過(guò)avCastController.sendControlCommand()接口實(shí)現(xiàn),支持多種播放控制命令,包括:暫停、停止、下一首、上一首、快進(jìn)、快退、跳轉(zhuǎn)、音量調(diào)節(jié)和倍速設(shè)置。只需修改command字段即可切換不同功能,具體命令與功能的對(duì)應(yīng)關(guān)系請(qǐng)參考AVCastControlCommandType。
publicasyncsetAVCastPlay(){
letavCommand: avSession.AVCastControlCommand = { command:'play'};
awaitthis.avCastController.sendControlCommand(avCommand);
}
在控制跳轉(zhuǎn)、音量調(diào)節(jié)和倍速設(shè)置時(shí),需要傳入時(shí)間(單位ms)、音量、倍速參數(shù)。
publicasyncsetAVCastSeek(timeMS:number) { letavCommand: avSession.AVCastControlCommand= {command:'seek',parameter: timeMS }; awaitthis.avCastController.sendControlCommand(avCommand); } publicasyncsetAVCastVolume(volume:number) { letavCommand: avSession.AVCastControlCommand= {command:'setVolume',parameter: volume }; awaitthis.avCastController.sendControlCommand(avCommand); } publicasyncsetAVCastSpeed(speed: media.PlaybackSpeed) { letavCommand: avSession.AVCastControlCommand= {command:'setSpeed',parameter: speed }; awaitthis.avCastController.sendControlCommand(avCommand); }
資源切換
在完成本集播放/用戶觸發(fā)集數(shù)切換時(shí)不需要斷開(kāi)連接,重新設(shè)置資源即可。
1.構(gòu)建avSession.AVQueueItem。
2.avCastController.prepare(playItem)。
3.avCastController.start(playItem)。
letplayItem: avSession.AVQueueItem= {
itemId: videoIndex,
description: {
assetId:'VIDEO-'+JSON.stringify(videoIndex),
title:this.videoDataArray[videoIndex].name,
subtitle:'video',
mediaUri:this.videoDataArray[videoIndex].urlasstring,
mediaType:'VIDEO',
mediaImage: imgPixel,
startPosition: startPosition,
duration:this.videoDataArray[videoIndex].duration
}
};
awaitthis.avCastController.prepare(playItem);
awaitthis.avCastController.start(playItem);
擴(kuò)展功能
懸浮球快捷控制
建議應(yīng)用集成懸浮球快捷控制功能,便于用戶快速返回投播頁(yè)面進(jìn)行操作控制,實(shí)現(xiàn)效果如圖:
可以通過(guò)為頁(yè)面設(shè)置浮層實(shí)現(xiàn)。
.overlay(this.OverlayNode(), {
align: Alignment.BottomEnd,
offset: {x: -24,
y: -136}
})
@Builder
OverlayNode(){
// ...
}
手機(jī)物理音量鍵同步遠(yuǎn)端
音量同步需要通過(guò)遙控器頁(yè)面的焦點(diǎn)管理和按鍵監(jiān)聽(tīng)實(shí)現(xiàn),具體流程為:當(dāng)遙控器頁(yè)面獲焦時(shí),監(jiān)聽(tīng)音量加減按鍵事件,在事件回調(diào)中調(diào)用音量調(diào)節(jié)函數(shù)并同步更新播控中心狀態(tài)。典型實(shí)現(xiàn)示例如下:
letupOptions: inputConsumer.KeyPressedConfig= {
key:KeyCode.KEYCODE_VOLUME_UP,
action:1,
isRepeat:true,
}
inputConsumer.on('keyPressed', upOptions,async()=> {
if(this.avCastPlayerController) {
console.log('currentVolume'+JSON.stringify(this.currentVolume));
letvolume =this.currentVolume+10;
awaitthis.avCastPlayerController.setAVCastVolume(volume);
}
})
letdownOptions: inputConsumer.KeyPressedConfig= {
key:KeyCode.KEYCODE_VOLUME_DOWN,
action:1,
isRepeat:true,
}
inputConsumer.on('keyPressed', downOptions,async()=> {
if(this.avCastPlayerController) {
letvolume =this.currentVolume-10;
if(volume 0){
? ? ??await?this.avCastPlayerController.setAVCastVolume(0);
? ? }
? ??await?this.avCastPlayerController.setAVCastVolume(volume);
? }
})
-
視頻
+關(guān)注
關(guān)注
6文章
1996瀏覽量
74525 -
華為
+關(guān)注
關(guān)注
217文章
35618瀏覽量
259764 -
HarmonyOS
+關(guān)注
關(guān)注
80文章
2144瀏覽量
35214
原文標(biāo)題:HarmonyOS應(yīng)用視頻投播解決方案
文章出處:【微信號(hào):HarmonyOS_Dev,微信公眾號(hào):HarmonyOS開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
HDTV的完整音視頻解決方案
視頻語(yǔ)音解決方案
[求助]視頻轉(zhuǎn)換器解決方案
杰士安校園網(wǎng)絡(luò)視頻監(jiān)控解決方案
如何構(gòu)建更好的視頻橋接解決方案
【視頻】解決方案第4期:極小硬件方案介紹
Altera的視頻和圖像處理解決方案
HarmonyOS測(cè)試技術(shù)與實(shí)戰(zhàn)-分布式應(yīng)用測(cè)試解決方案

HarmonyOS應(yīng)用視頻投播解決方案
評(píng)論