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

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

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

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

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

開發(fā)步驟
1.videosession創(chuàng)建后,創(chuàng)建投播前,聲明當前應(yīng)用支持投播。
awaitvideoSession.setExtras({
requireAbilityList: ['url-cast']
})
2.繪制AVCastPicker,AVCastPicker是投播組件,點擊后系統(tǒng)會彈出設(shè)備選擇半模態(tài)。
AVCastPicker({
normalColor: Color.White,
pickerStyle:AVCastPickerStyle.STYLE_PANEL,
sessionType:'video',
// ...
})
3.當用戶選擇設(shè)備并設(shè)備切換成功后觸發(fā)videoSession.on('outputDeviceChange')事件,應(yīng)用可選擇停止本地播放并跳轉(zhuǎn)到遙控頁面(或保持本端繼續(xù)播放),此時播控中心會自動接管遠端設(shè)備的播放控制,開發(fā)者無需額外設(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資源,完成以下三步后遠端設(shè)備即可投播視頻,以播放網(wǎng)絡(luò)資源為例。
1.構(gòu)建avSession.AVQueueItem。需要傳入assetId(播放列表媒體ID,應(yīng)用自定義)、title(媒體標題)、artist(媒體專輯作者)、mediaUri(媒體URI)、mediaType(媒體類型)、mediaImage(媒體圖片像素數(shù)據(jù))、duration(媒體播放時長)。
2.avCastController.prepare(playItem)準備播放媒體資源,即進行播放資源的加載和緩沖。
3.avCastController.start(playItem)啟動播放媒體資源。
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);
若需要投播本地資源,需要打開沙箱文件,并在fdSrc中傳入文件fd實現(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è)備切換依賴于videosession監(jiān)聽設(shè)備改變事件,可以通過stopCasting終止投播切換設(shè)備,也可以通過avCastPicker.select()進行切換。均會觸發(fā)videoSession.on('outputDeviceChange')事件,當切換到遠端設(shè)備播放,本端應(yīng)該跳轉(zhuǎn)到遙控器界面,當切換回本端設(shè)備播放,應(yīng)當停止投播并跳轉(zhuǎn)到視頻播放頁面。應(yīng)用時序圖如下,具體實現(xiàn)見開發(fā)步驟。
時序圖

開發(fā)步驟
可以直接使用AVCastPicker切換設(shè)備,系統(tǒng)會自動彈出設(shè)備選擇半模態(tài)彈窗,用戶可直接選擇目標設(shè)備完成切換。開發(fā)者無需額外處理彈窗邏輯。也可以使用avCastPicker.select() 接口切換設(shè)備。
當設(shè)備切換時,videoSession.on
('outputDeviceChange')事件將被觸發(fā),開發(fā)者可在回調(diào)中處理設(shè)備切換邏輯:若切換至遠端設(shè)備則跳轉(zhuǎn)至遙控頁面,若切回本端設(shè)備則恢復本地播放,實現(xiàn)播放控制的無縫切換。
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;
}
}
})
遠端視頻狀態(tài)回傳本端
當視頻在遠端設(shè)備播放時,為了控制遠端視頻的播放應(yīng)用需要監(jiān)聽遠端視頻播放狀態(tài)并同步顯示本端,通過遠端設(shè)備或本端播控中心控制,都會直接改變遠端設(shè)備的播放狀態(tài),并觸發(fā)avCastController.on('playbackStateChange')。應(yīng)用時序圖如下,具體實現(xiàn)見開發(fā)步驟。
時序圖

開發(fā)步驟
當需要在本地遙控界面同步顯示遠端視頻的播放狀態(tài)時,可通過avCastController.on
('playbackStateChange') 監(jiān)聽狀態(tài)變化,并使用過濾器篩選目標狀態(tài)。
建議使用@Track修飾器標記這些經(jīng)常改變的狀態(tài)變量,以便頁面自動響應(yīng)數(shù)據(jù)更新。該機制可統(tǒng)一獲取播放狀態(tài)(如播放/暫停)、音量、總時長及倍速等信息,以下代碼以獲取已播放時長為例:
@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;
}
});
// ...
}
// ...
}
本端控制遠端設(shè)備狀態(tài)
時序圖

開發(fā)步驟
控制遠端設(shè)備狀態(tài)可通過avCastController.sendControlCommand()接口實現(xiàn),支持多種播放控制命令,包括:暫停、停止、下一首、上一首、快進、快退、跳轉(zhuǎn)、音量調(diào)節(jié)和倍速設(shè)置。只需修改command字段即可切換不同功能,具體命令與功能的對應(yīng)關(guān)系請參考AVCastControlCommandType。
publicasyncsetAVCastPlay(){
letavCommand: avSession.AVCastControlCommand = { command:'play'};
awaitthis.avCastController.sendControlCommand(avCommand);
}
在控制跳轉(zhuǎn)、音量調(diào)節(jié)和倍速設(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è)置資源即可。
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);
擴展功能
懸浮球快捷控制
建議應(yīng)用集成懸浮球快捷控制功能,便于用戶快速返回投播頁面進行操作控制,實現(xiàn)效果如圖:
可以通過為頁面設(shè)置浮層實現(xiàn)。
.overlay(this.OverlayNode(), {
align: Alignment.BottomEnd,
offset: {x: -24,
y: -136}
})
@Builder
OverlayNode(){
// ...
}
手機物理音量鍵同步遠端
音量同步需要通過遙控器頁面的焦點管理和按鍵監(jiān)聽實現(xiàn),具體流程為:當遙控器頁面獲焦時,監(jiān)聽音量加減按鍵事件,在事件回調(diào)中調(diào)用音量調(diào)節(jié)函數(shù)并同步更新播控中心狀態(tài)。典型實現(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瀏覽量
74537 -
華為
+關(guān)注
關(guān)注
217文章
35620瀏覽量
259842 -
HarmonyOS
+關(guān)注
關(guān)注
80文章
2144瀏覽量
35256
原文標題:HarmonyOS應(yīng)用視頻投播解決方案
文章出處:【微信號:HarmonyOS_Dev,微信公眾號:HarmonyOS開發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
HDTV的完整音視頻解決方案
視頻語音解決方案
[求助]視頻轉(zhuǎn)換器解決方案
杰士安校園網(wǎng)絡(luò)視頻監(jiān)控解決方案
如何構(gòu)建更好的視頻橋接解決方案
【視頻】解決方案第4期:極小硬件方案介紹
Altera的視頻和圖像處理解決方案
HarmonyOS測試技術(shù)與實戰(zhàn)-分布式應(yīng)用測試解決方案

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