播控特性简介
使用媒体播控,可以简单高效地将音视频投放到其他HarmonyOS设备上播放,如在手机上播放的音视频,可以投到2in1设备上继续播放。
HarmonyOS提供了统一的应用内音视频投播功能设计,通过使用系统提供的投播组件和接口,应用只需要设置对应的资源信息、监听投播中的相关状态,以及应用主动控制的行为(如:播放、暂停)。其他动作包括图标切换、设备的发现、连接、认证等,均由系统完成。
基本概念
-
媒体会话(AVSession)
音视频管控服务,用于对系统中所有音视频行为进行统一的管理。
本地播放时,应用需要向媒体会话提供播放的媒体信息(如正在播放的歌曲、歌曲的播放状态等),并接收和响应播控中心发出的控制命令(如暂停、下一首等)。具体请参考本地媒体会话。
投播时,通过AVSession,应用可以进行投播能力的设置和查询,并创建投播控制器。
应用可以在启动内容显示(比如:视频播放)时,获取支持投播的扩展屏设备并注册监听,当存在扩展屏时,可在扩展屏上全屏绘制要投播的内容。
-
投播组件(AVCastPicker)
系统级的投播组件,可嵌入应用界面的UI组件。当用户点击该组件后,系统将进行设备发现、连接、认证等流程,应用仅需要通过接口获取投播中相关的回调信息。
-
投播控制器(AVCastController)
在投播后,由应用发起的用于控制远端播放的接口,包括播放、暂停、调节音量、设置播放模式、设置播放速度等能力。
-
后台长时任务
应用实现后台播放,需申请后台长时任务,避免应用在投播后被系统后台清理或冻结。具体参考长时任务。
运作机制

-
发现和连接设备
用户在应用界面上点击AVCastPicker组件,触发系统发现可用于投播的设备。用户在设备列表中选择对应设备后,系统连接对应设备。应用无需关注设备的发现连接过程,仅需关注设备在远端是否可用。
应用需要接入AVSession,才可以使用系统提供的统一投播能力,由系统进行设备发现和管理。
-
进入远端投播
应用通过AVSession监听设备的连接情况,监听到设备已连接后,可通过AVSession获取一个AVCastController对象用于发送控制命令(如播放、暂停、下一首等)。
应用在进入远端投播时,应停止本地的播放器,避免本端和远端设备同时播放的情况。同时,建议应用重新绘制应用界面,比如界面变更为一个遥控器,来控制远端播放。
-
在本端控制播放
在本端(包括应用内和播控中心)控制播放时,控制命令将通过AVCastController发送,系统将完成数据传输和信息同步,然后更新远端系统预置播放器的状态。
-
在远端控制播放
用户同样可以在远端直接控制播放,会直接修改远端播放器的状态。
-
远端播放器状态回调
当远端播放器状态变更后,会触发回调,将状态信息返回到本端。应用可以通过AVCastController监听到远端播放器的状态变化。
投播组件开发指导
通过本开发指导,完成一次音视频跨设备投播。
约束与限制
需同时满足以下条件,才能使用该功能:
-
设备限制
本端设备:HarmonyOS NEXT Developer Preview0及以上版本的手机设备
远端设备:HarmonyOS NEXT Developer Preview0及以上版本的2in1设备/华为智慧屏HarmonyOS3.1及以上版本
-
使用限制
- 双端设备打开蓝牙和WIFI,并可访问网络。
接口说明
在开发具体功能前,请先查阅参考文档,获取详细的接口说明。
- AVCastPicker:投播组件,提供设备发现认证连接的统一入口。
- AVCastController:投播控制器,用于投播场景下,完成播放控制、远端播放状态监听等操作。
说明
AVCastController由系统获取并返回,在设备连接成功后获取,在设备断开后不能继续使用,否则会抛出异常。
支持在线DRM视频资源投播能力,需注册DRM许可证请求回调函数,获取许可证后,调用处理许可证响应函数。

开发步骤
-
创建播放器,并创建AVSession。
通过AVSessionManager创建并激活媒体会话。
示例中的context的获取方式请参见获取UIAbility的上下文信息。
import { avSession } from '@kit.AVSessionKit'; // 导入AVSession模块
// 声明全局的session对象,此写法是加在class类外的声明,如果需要在class类内申明全局变量,需要去掉 export let
export let session: avSession.AVSession;
// 创建session
async createSession(context: Context) {
session = await avSession.createAVSession(context, 'video_test', 'video'); // 'audio'代表音频应用,'video'代表视频应用
await session.activate();
// 需要将应用加入支持投播的应用名单中,才能成功投播。
session.setExtras({
requireAbilityList: ['url-cast'],
});
console.info(`Session created. sessionId: ${session.sessionId}`);
}
- 设置媒体资源信息。
说明
需要在AVCastPicker中仅显示支持DRM资源投播的设备时,应在AVMetaData设置明确的drmSchemes。
// 与session声明不在同一文件时,需要import
import { session } from './xxx'; // session声明的文件
public setAVMetadata(playInfo: avSession.AVMediaDescription): Promise<void> {
const metadata: avSession.AVMetadata = {
assetId: playInfo.assetId, // 需要配置实际id
title: playInfo.title, // 播放媒体资源的标题
subtitle: playInfo.subtitle,// 播放媒体资源的副标题
// 发现Cast+ Stream 和 DLNA协议设备,TYPE_CAST_PLUS_STREAM为默认必选。
filter: avSession.ProtocolType.TYPE_CAST_PLUS_STREAM|avSession.ProtocolType.TYPE_DLNA,
mediaImage: playInfo.mediaImage,
artist: playInfo.artist,
// 如果是DRM资源,配置支持的DRM uuid 用于设备过滤。非DRM资源不配置。
drmSchemes: ['3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c']
};
return session.setAVMetadata(metadata);
}
- 在需要投播的播放界面创建投播组件AVCastPicker。
import { AVCastPicker } from '@kit.AVSessionKit';;
// 创建组件,并设置大小
build() {
Row() {
Column() {
AVCastPicker()
.size({height:'100%',width:'100%'})
}
}
}
-
设置AVSession的信息,注册AVSession的回调。用于感知投播连接。
说明
下面代码展示设备连接成功后的相应的处理
- 连接成功后通过session获取AVCastController,用于后期的投播控制;
- 如需要推送DRM在线资源,根据远端设备支持的DRM能力,从服务端获取对应的资源;
- 推送DRM资源后,应注册监听许可证请求事件,从服务器端获取许可证后,通过处理许可证响应函数提供给远端。
import { BusinessError } from '@kit.BasicServicesKit';
import { avSession } from '@kit.AVSessionKit';
import { session } from './xxx'; // session声明的文件
castController: avSession.AVCastController | undefined = undefined;
getAVCastController() {
// 监听设备连接状态的变化
session.on('outputDeviceChange', async (connectState: avSession.ConnectionState,
device: avSession.OutputDeviceInfo) => {
let currentDevice: avSession.DeviceInfo = device?.devices?.[0];
if (currentDevice.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE && connectState === avSession.ConnectionState.STATE_CONNECTED) { // 设备连接成功
console.info(`Device connected: ${device}`);
this.castController = await session.getAVCastController();
console.info('Succeeded in getting a cast controller');
// 查询当前播放的状态
let avPlaybackState = await this.castController?.getAVPlaybackState();
console.info(`Succeeded in AVPlaybackState resource obtained: ${avPlaybackState}`);
// 监听播放状态的变化
this.castController?.on('playbackStateChange', 'all', (state: avSession.AVPlaybackState) => {
console.info(`Succeeded in Playback state changed: ${state}`);
});
if (currentDevice.supportedProtocols === avSession.ProtocolType.TYPE_CAST_PLUS_STREAM) {
// 此设备支持cast+投播协议
} else if (currentDevice.supportedProtocols === avSession.ProtocolType.TYPE_DLNA) {
// 此设备支持DLNA投播协议
}
// 此设备支持chinaDRM,监听许可证请求事件,也可在发起DRM资源投播前监听。
if (currentDevice.supportedDrmCapabilities?.includes('3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c')) {
this.castController?.on('keyRequest', this.keyRequestCallback);
}
}
})
}
// 处理DRM许可证请求事件
private keyRequestCallback: avSession.KeyRequestCallback = async (assetId: string, requestData: Uint8Array) => {
// 根据assetId获取对应的DRM url
let drmUrl: string = 'http://license.xxx.xxx.com:8080/drmproxy/getLicense';
// 从服务器获取许可证,具体实现可参考附录。
let licenseResponseData = await this.getLicense(drmUrl, requestData);
try {
// 处理DRM许可证响应
await this.castController?.processMediaKeyResponse(assetId, licenseResponseData);
} catch (error) {
console.error(`Failed to process the response corresponding to the media key. Error: ${error}`);
}
}
-
使用AVCastController进行资源播放。
说明
下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
- 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源,参考获取应用文件路径。应用沙箱的介绍及如何向应用沙箱推送文件,请参考文件管理。
- 如果通过FilePicker使用用户文件,请参考选择用户文件。
- 如果使用网络播放路径,需申请相关权限:ohos.permission.INTERNET。
- 如果是DRM资源,需配置drmSchemes字段。
playItem() {
// 设置播放参数,开始播放
let playItem : avSession.AVQueueItem = {
itemId: 0,
description: {
assetId: 'VIDEO-1',
title: 'ExampleTitle',
artist: 'ExampleArtist',
mediaUri: 'https://xxx.xxx.com/example.mp4',
mediaType: 'VIDEO',
mediaSize: 1000,
startPosition: 0,
duration: 100000,
albumCoverUri: 'https://www.example.jpeg',
albumTitle: '《ExampleAlbum》',
appName: 'ExampleApp',
// DRM资源,需要配置支持的DRM类型, 以chinaDRM为例。
drmScheme: '3d5e6d35-9b9a-41e8-b843-dd3c6e72c42c',
}
};
// 准备播放,这个不会触发真正的播放,会进行加载和缓冲
this.castController?.prepare(playItem, () => {
console.info('Preparation done');
});
// 启动播放
this.castController?.start(playItem, () => {
console.info('Playback started');
});
}
- 使用AVCastController,监听控制命令和进行播放控制。
playControl() {
// 记录从avsession获取的远端控制器
// 下发播放命令
let avCommand: avSession.AVCastControlCommand = {command:'play'};
this.castController?.sendControlCommand(avCommand);
// 下发暂停命令
avCommand = {command:'pause'};
this.castController?.sendControlCommand(avCommand);
// 监听上下一首切换
this.castController?.on('playPrevious', () => {
console.info('PlayPrevious done');
});
this.castController?.on('playNext', () => {
console.info('PlayNext done');
});
}
-
申请投播长时任务,避免应用在投播进入后台时被系统冻结,导致无法持续投播。
说明
在申请长时任务时,需要在module.json5文件中:
- 配置长时任务权限ohos.permission.KEEP_BACKGROUND_RUNNING。
- 为需要使用长时任务的UIAbility声明相应的后台模式类型:MULTI_DEVICE_CONNECTION。
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
import { wantAgent } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
let context: Context = getContext(this);
function startContinuousTask() {
let wantAgentInfo: wantAgent.WantAgentInfo = {
// 点击通知后,将要执行的动作列表
wants: [
{
bundleName: "com.example.myapplication",
abilityName: "EntryAbility",
}
],
// 点击通知后,动作类型
operationType: wantAgent.OperationType.START_ABILITY,
// 使用者自定义的一个私有值
requestCode: 0,
// 点击通知后,动作执行属性
wantAgentFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
};
// 通过wantAgent模块的getWantAgent方法获取WantAgent对象
try {
wantAgent.getWantAgent(wantAgentInfo).then((wantAgentObj) => {
try {
backgroundTaskManager.startBackgroundRunning(context,
backgroundTaskManager.BackgroundMode.MULTI_DEVICE_CONNECTION, wantAgentObj).then(() => {
console.info('Succeeded in requesting to start running in background');
}).catch((error: BusinessError) => {
console.error(`Failed to request to start running in background. Code: ${error.code}, message: ${error.message}`);
});
} catch (error) {
console.error(`Failed to request to start running in background. Error: ${error}`);
}
});
} catch (error) {
console.error(`Failed to get WantAgent. Error: ${error}`);
}
}
-
处理音频焦点。请参考多音频并发处理。
在应用进入投播后,当前应用需要取消注册焦点处理事件,以免被其他应用的焦点申请而影响。
-
结束投播。
当远端设备断开的时候,应用会收到事件,系统会自动断开连接。
应用也可以使用断开投播的接口,主动进行投播连接的断开。
async release() {
// 一般来说,应用退出时,而不希望继续投播,可以主动结束
await session.stopCasting();
}
附录
从服务器获取许可证
开发者需要根据实际的资源和服务地址获取DRM许可证,以下示例代码仅作为参考。
import { http } from '@kit.NetworkKit';
// 获取DRM许可证, 仅做参考,需要结合实际资源和服务地址进行获取。
async getLicense(drmUrl: string, requestData: Uint8Array): Promise<Uint8Array | undefined> {
let licenseRequestStr: string = this.byteToString(requestData);
let licenseResponseStr: string = 'defaultStr';
let httpRequest = http.createHttp();
try {
let response: http.HttpResponse = await httpRequest.request(drmUrl, {
method: http.RequestMethod.POST,
header: {
'Content-Type': 'application/json',
'Accept-Encoding': 'gzip, deflate',
},
extraData: licenseRequestStr,
expectDataType: http.HttpDataType.STRING,
});
if (response?.responseCode == http.ResponseCode.OK) {
if (typeof response.result == 'string') {
licenseResponseStr = response.result;
}
}
httpRequest.destroy();
} catch (error) {
console.error(`Failed to request Http. Error: ${error}`);
return undefined;
}
return this.stringToByte(licenseResponseStr);
}
/**
* Uint8Array to string
* @param arr Uint8Array
* @returns string
*/
byteToString(arr: Uint8Array): string {
let str: string = ''
let _arr: Uint8Array = arr
for (let i = 0; i < _arr.length; i++) {
// 将数值转为二进制字符串
let binaryStr: string = _arr[i].toString(2)
let matchArray = binaryStr.match(new RegExp('/^1+?(?=0)/'))
if (matchArray && binaryStr.length == 8) {
let bytesLength: number = matchArray[0].length
let store: string = _arr[i].toString(2).slice(7 - bytesLength)
for (let j = 1; j < bytesLength; j++) {
store += _arr[j + i].toString(2).slice(2)
}
str += String.fromCharCode(Number.parseInt(store, 2))
i += bytesLength - 1
} else {
str += String.fromCharCode(_arr[i])
}
}
return str
}
/**
* string 转 Uint8Array
* @param str string
* @returns Uint8Array
*/
stringToByte(str: string): Uint8Array {
let bytes: number[] = new Array()
let unicode: number
for (let i = 0; i < str.length; i++) {
unicode = str.charCodeAt(i)
if (unicode >= 0x010000 && unicode <= 0x10FFFF) {
bytes.push(((unicode >> 18) & 0x07) | 0xf0)
bytes.push(((unicode >> 12) & 0x3F) | 0x80)
bytes.push(((unicode >> 6) & 0x3F) | 0x80)
bytes.push((unicode & 0x3F) | 0x80)
} else if (unicode >= 0x000800 && unicode <= 0x00FFF) {
bytes.push(((unicode >> 12) & 0x07) | 0xf0)
bytes.push(((unicode >> 6) & 0x3F) | 0x80)
bytes.push((unicode & 0x3F) | 0x80)
} else if (unicode >= 0x000800 && unicode <= 0x0007FF) {
bytes.push(((unicode >> 6) & 0x3F) | 0x80)
bytes.push((unicode & 0x3F) | 0x80)
} else {
bytes.push(unicode & 0xFF)
}
}
return new Uint8Array(bytes);
}
扩展屏投播开发指导
通过本节开发指导,可在系统镜像投屏后,获取投屏设备信息,实现扩展屏模式的投播,实现双屏协作的能力。
运作机制

-
虚拟扩展屏
是在系统投屏启动过程中建立的,依据双端协商的投屏视频流的分辨率创建,支持1080P 及以上分辨率。默认镜像主屏内容,当虚拟扩展屏上有UIAbility绘制时,会投屏该屏内容。
-
UIAbility A(本机内容)
在本端主屏上显示的内容。假定UIAbility A 与 UIAbility B 属于同一应用,UIAbility A可以控制UIAbility B,实现双屏联动。
-
UIAbility B(投屏内容)
在虚拟扩展屏上绘制的内容,考虑到远端投屏用户体验,UIAbility B 应铺满全屏。从安全角度考虑,在启动UIAbility B 时,系统会校验主屏前台UIAbility是否归属同一应用,如果校验失败会禁止其在虚拟扩展屏启动。
约束与限制
需同时满足以下条件,才能使用该功能:
-
设备限制
本端设备:HarmonyOS NEXT Developer Beta1及以上版本的手机设备
远端设备:华为智慧屏HarmonyOS2.0及以上版本
-
使用限制
需要系统发起无线/有线投屏后才可通过接口获取有效的扩展投屏设备。
接口说明
在开发具体功能前,请先查阅参考文档,获取详细的接口说明。

开发步骤
-
UIAibility A创建AVSession, 获取可用扩展屏投播设备并注册监听。
说明
获取的屏幕信息CastDisplayInfo中包含屏幕ID,屏幕名称、状态以及分辨率宽度、高度基础属性,其中屏幕id 值同于Display的id,如需要获取更详细的信息可参考Display获取设备信息说明。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { avSession } from '@kit.AVSessionKit'; // 导入AVSession模块
import { BusinessError } from '@kit.BasicServicesKit';
export default class AbilityA extends UIAbility{
private session: avSession.AVSession | undefined = undefined;
private extCastDisplayInfo: avSession.CastDisplayInfo | undefined = undefined;
// 注册监听可投屏设备变化事件
private onCastDisplayChangedCallback = (castDisplayInfo: avSession.CastDisplayInfo) => {
// 新增扩展屏,进入扩展屏显示
if (this.extCastDisplayInfo === undefined && castDisplayInfo.state === avSession.CastDisplayState.STATE_ON) {
console.info('Succeeded in opening the cast display');
this.extCastDisplayInfo = castDisplayInfo;
this.startExternalDisplay();
} else if (this.extCastDisplayInfo?.id == castDisplayInfo.id) {
this.extCastDisplayInfo = castDisplayInfo;
// 扩展屏不可用,退出扩展屏显示
if (castDisplayInfo.state === avSession.CastDisplayState.STATE_OFF){
console.info('Succeeded in closing the cast display');
this.stopExternalDisplay();
this.extCastDisplayInfo = undefined;
}
}
};
// 创建AVSession, 获取可用扩展屏投播设备并注册监听
initAVSession(context: Context) {
avSession.createAVSession(context, 'CastDisplay', 'video').then((session: avSession.AVSession) => {
this.session = session;
this.session?.on('castDisplayChange', this.onCastDisplayChangedCallback);
// 获取当前系统可用的扩展屏显示设备
session.getAllCastDisplays().then((infoArr: avSession.CastDisplayInfo[]) => {
// 有多个扩展屏时可以提供用户选择,也可使用其中任一个作为扩展屏使用。
if (infoArr.length > 0) {
this.extCastDisplayInfo = infoArr[0];
this.startExternalDisplay();
}
}).catch((err: BusinessError<void>) => {
console.error(`Failed to get all CastDisplay. Code: ${err.code}, message: ${err.message}`);
});
});
}
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
super.onCreate(want, launchParam);
this.initAVSession(this.context);
}
onDestroy() {
this.stopExternalDisplay();
// 去注册监听
this.session?.off('castDisplayChange');
}
}
- 在UIAblityA中构建扩展屏启动和退出能力。
// 扩展屏启动UIAbilityB
startExternalDisplay() {
if (this.extCastDisplayInfo !== undefined &&
this.extCastDisplayInfo.id !== 0 &&
this.extCastDisplayInfo.state === avSession.CastDisplayState.STATE_ON) {
let id = this.extCastDisplayInfo?.id;
console.info(`Succeeded in starting ability and the id of display is ${id}`);
this.context.startAbility({
bundleName: 'com.example.myapplication', // 应用自有包名
abilityName: 'AbilityB'
}, {
displayId: id // 扩展屏ID
});
AppStorage.setOrCreate('CastDisplayState', 1);
}
}
// 停止使用扩展屏
stopExternalDisplay() {
AppStorage.setOrCreate('CastDisplayState', 0);
// 更新本页面显示。
}
- UIAbility B 扩展屏显示内容绘制,需响应退出处理。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class AbilityB extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
windowStage.getMainWindowSync().setWindowLayoutFullScreen(true); // 设置为全屏
windowStage.loadContent('pages/CastPage', (err: BusinessError) => {
if (err.code) {
console.error(`Failed to load the content. Code: ${err.code}, message: ${err.message}`);
return;
}
console.info('Succeeded in loading the content. ');
});
}
}
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';
@Entry
@Component
struct CastPage {
// 监测到CastDisplayState变化后,当设备断开时,销毁本页内容。
@StorageLink('CastDisplayState') @Watch('onDestroyExtend') private displayState: number = 1;
private onDestroyExtend() {
if (this.displayState === 1) return;
let context = (getContext(this) as common.UIAbilityContext)
context.terminateSelf().then(() => {
console.info('CastPage finished');
}).catch((err: BusinessError) => {
console.error(`Failed to destroying CastPage. Code: ${err.code}, message: ${err.message}`);
});
}
//...
}
在案例应用中,开发者可以参考以下实现,完成接入播控中心、投播等功能。
案例介绍
华为视频接入播控中心和投播实践
华为视频在进入影片详情页播放时,支持在控制中心查看当前播放的视频信息,并操作视频完成快进、快退、拖动进度、播放暂停、下一集、调节音量等,方便用户通过控制中心来操作当前播放的视频。
当用户希望通过大屏播放当前华为视频的影片时,可以在华为视频内或播控中心内进行投播,将影片投播到同一网络下的华为智慧屏等大屏设备进行播放,并通过播控中心方便地完成播放暂停、快进快退、下一集等操作。
写在最后
●如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
●点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
●关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
●更多鸿蒙最新技术知识点,请移步前往小编:https://gitee.com/


1万+

被折叠的 条评论
为什么被折叠?



