在鸿蒙(HarmonyOS)应用开发中,媒体投影(投播)能力允许开发者将应用内的音视频资源无缝流转至远端设备播放。依托系统级的 AVSession Kit 和 AVCastPicker 组件,开发者无需关注底层的设备发现、连接与认证过程,即可快速构建跨设备的媒体播控体验。

一、 核心架构与基本概念

鸿蒙的投播机制基于分布式媒体会话(AVSession)体系,主要包含以下核心组件:

  1. 媒体会话(AVSession):作为音视频管控服务,负责统一管理应用内的音视频行为。在投播场景下,应用通过 AVSession 设置投播能力、获取投播控制器,并监听远端设备的连接与状态变化。
  2. 投播组件(AVCastPicker):系统级的 UI 组件,作为设备切换和投播能力的统一入口。应用将其嵌入界面后,系统会自动完成设备的发现、连接和认证流程。
  3. 投播控制器(AVCastController):在设备成功连接后由系统返回。应用通过该控制器向远端设备发送播放、暂停、调节音量等播控指令,并监听远端播放状态的变更。
// AvSessionManager.ets
import { avSession } from '@kit.AVSessionKit';

export class AvSessionManager {
    private session: avSession.AVSession | null = null;

    // 1. 创建并激活媒体会话
    public async createSession(context: Context) {
        try {
            // 创建视频类型的媒体会话
            this.session = await avSession.createAVSession(context, 'video_cast', 'video');
            await this.session.activate();
            
            // 核心:声明当前应用支持投播能力
            await this.session.setExtras({
                requireAbilityList: ['url-cast']
            });
            console.info('AVSession 创建并激活成功');
        } catch (err) {
            console.error('AVSession 创建失败:', err);
        }
    }

    // 2. 获取 Session 实例(供外部使用)
    public getSession(): avSession.AVSession | null {
        return this.session;
    }
}

二、 投播运作机制与交互流程

  1. 设备发现与连接:用户在应用内点击 AVCastPicker 组件,系统自动搜索并展示支持投播的远端设备。用户选择后,系统自动建立连接。
  2. 进入远端投播:监听到设备连接成功后,应用应停止本地播放器(避免双端同时发声),并通过 AVSession 获取 AVCastController。此时建议将应用界面切换为“遥控器”模式。
  3. 双向播控与状态同步
    • 本端控制:用户在手机端或播控中心操作时,指令通过 AVCastController 下发,系统同步更新远端播放器状态。
    • 远端控制:用户直接在远端设备(如智慧屏)上操作时,状态变更会触发回调,本端应用可实时监听并刷新 UI。
// CastPlayerPage.ets
import { avSession } from '@kit.AVSessionKit';
import { AvSessionManager } from './AvSessionManager';

@Entry
@Component
struct CastPlayerPage {
    private sessionManager = new AvSessionManager();
    @State isCasting: boolean = false;

    aboutToAppear() {
        // 初始化会话
        this.sessionManager.createSession(getContext(this));
        const session = this.sessionManager.getSession();
        
        // 监听远端设备连接状态变化
        session?.on('outputDeviceChange', (connectState, device) => {
            const currentDevice = device?.devices?.[0];
            if (currentDevice?.castCategory === avSession.AVCastCategory.CATEGORY_REMOTE) {
                if (connectState === avSession.ConnectionState.STATE_CONNECTED) {
                    // 设备连接成功,进入投播状态
                    this.isCasting = true;
                    // TODO: 停止本地播放,切换为遥控器模式
                } else if (connectState === avSession.ConnectionState.STATE_DISCONNECTED) {
                    // 设备断开,恢复本地播放
                    this.isCasting = false;
                }
            }
        });
    }

    build() {
        Column() {
            // 挂载系统级投播组件,点击后自动弹出设备选择半模态
            avSession.AVCastPicker({
                pickerStyle: avSession.AVCastPickerStyle.STYLE_PANEL,
                sessionType: 'video'
            })
            .width('100%')
            .height(60)
            
            Text(this.isCasting ? '正在投播中...' : '本地播放模式')
                .fontSize(18)
                .margin({ top: 20 })
        }
        .width('100%')
        .height('100%')
        .justifyContent(FlexAlign.Center)
    }
}

三、 开发约束与前置条件

实现投播功能需严格满足以下系统与环境要求:

  1. 设备与系统版本:本端设备需为 HarmonyOS 5.0.0 及以上版本的手机或平板;远端设备需为 HarmonyOS 5.0.0 及以上的 2in1 设备、HarmonyOS 3.1 及以上版本的华为智慧屏,或支持标准 DLNA 协议的设备。
  2. 网络与权限:双端设备必须同时打开蓝牙和 Wi-Fi,并可访问网络。若涉及读取媒体库文件,还需申请 ohos.permission.READ_IMAGEVIDEO 受限权限。
  3. 后台长时任务:为防止应用在投播后被系统冻结或清理,必须申请后台长时任务(Background Task)以维持投播会话。
// CastController.ets
import { avSession } from '@kit.AVSessionKit';

export class CastController {
    private avCastController: avSession.AVCastController | null = null;

    // 1. 设备连接成功后获取投播控制器
    public async initController(session: avSession.AVSession) {
        try {
            this.avCastController = await session.getAVCastController();
            // 监听远端播放状态变化
            this.avCastController.on('playbackStateChange', 'all', (state) => {
                console.info(`远端播放状态变更: ${state.state}`);
                // TODO: 同步更新本端 UI 状态
            });
        } catch (err) {
            console.error('获取投播控制器失败:', err);
        }
    }

    // 2. 向远端设备发送播放指令
    public async startRemotePlay(mediaUri: string, title: string) {
        if (!this.avCastController) return;
        
        // 构建投播媒体队列项
        const playItem: avSession.AVQueueItem = {
            itemId: 'video_001',
            description: {
                assetId: 'VIDEO-001',
                title: title,
                mediaUri: mediaUri,
                mediaType: 'VIDEO',
                duration: 3600000 // 毫秒
            }
        };

        // 准备资源并启动远端播放
        await this.avCastController.prepare(playItem);
        await this.avCastController.start(playItem);
    }

    // 3. 发送暂停/播放等播控指令
    public async sendPlayPauseCommand() {
        const command: avSession.AVCastControlCommand = {
            command: avSession.CastControlCommand.COMMAND_PLAY_PAUSE
        };
        await this.avCastController?.sendControlCommand(command);
    }
}

四、 性能优化

  1. 基础播控前置适配:在接入投播组件前,应用必须先适配媒体播控中心的基础播控业务(如配置媒体元数据、响应暂停/播放命令等),否则投播功能无法生效。
  2. 不支持投播内容的处理:当应用切换上下集时,若下一集内容不支持投播,可通过设置 filter 参数为 0,控制系统播控中心隐藏投播设备列表,避免用户误触。
  3. DRM 加密资源支持:若投播的是在线 DRM 视频资源,应用需注册 DRM 许可证请求回调函数(keyRequest),在获取许可证后调用 processMediaKeyResponse 接口向远端提供秘钥,方可正常播放。
// DrmCastSupport.ets
import { avSession } from '@kit.AVSessionKit';

export class DrmCastSupport {
    // 针对 DRM 加密视频的投播支持
    public static setupDrmCallback(castController: avSession.AVCastController) {
        // 注册 DRM 秘钥请求回调
        castController.on('keyRequest', async (keyRequest) => {
            console.info('收到 DRM 秘钥请求, assetId:', keyRequest.assetId);
            try {
                // TODO: 调用业务后端接口获取 DRM 许可证秘钥
                const licenseKey = await fetchDrmLicense(keyRequest.assetId);
                
                // 将秘钥提供给远端设备进行解密播放
                const response = new Uint8Array(licenseKey);
                await castController.processMediaKeyResponse(keyRequest.assetId, response);
            } catch (err) {
                console.error('DRM 秘钥获取失败:', err);
            }
        });
    }
}

五、 媒体投影实战:AVSession 初始化与 AVCastPicker 挂载

在鸿蒙 ArkTS 开发中,实现音视频投播的核心在于正确配置媒体会话并接入系统级投播组件。

  1. 创建并激活媒体会话
    通过 avSession.createAVSession 创建会话,并调用 activate() 激活。同时,必须通过 setExtras 声明当前应用支持投播能力(如设置 requireAbilityList: ['url-cast']),这是触发投播的关键前置条件。
  2. 配置媒体元数据
    调用 setAVMetadata 设置当前播放内容的标题、作者、封面及 DRM 类型等信息。未配置元数据会导致投播组件无法显示或投播后远端黑屏。
  3. 挂载 AVCastPicker 组件
    在 UI 层引入 AVCastPicker 组件。用户点击后,系统会自动拉起设备选择弹框,完成发现、连接和认证,应用无需干预底层流程。
// AvSessionCastSetup.ets
import { avSession } from '@kit.AVSessionKit';

export class AvSessionCastSetup {
    private videoSession: avSession.AVSession | null = null;

    // 1. 创建会话、激活并声明投播能力
    public async initSession(context: Context): Promise<void> {
        this.videoSession = await avSession.createAVSession(context, 'video_cast', 'video');
        await this.videoSession.activate();
        
        // 核心:声明支持投播能力
        await this.videoSession.setExtras({
            requireAbilityList: ['url-cast']
        });
    }

    // 2. 配置媒体元数据(未配置会导致投播组件无法显示或远端黑屏)
    public async setMediaMetadata(title: string, artist: string, coverUri: string) {
        const metadata: avSession.AVMetadata = {
            title: title,
            artist: artist,
            mediaImage: coverUri, // 实际开发中需传入 PixelMap 或有效 URI
            duration: 3600000
        };
        await this.videoSession?.setAVMetadata(metadata);
    }

    // 获取 Session 供 UI 层绑定 AVCastPicker
    public getSession(): avSession.AVSession | null {
        return this.videoSession;
    }
}

六、 进阶场景:AVCastController 播控指令与状态同步

当设备连接成功后,应用需通过投播控制器实现双向播控。

  1. 获取投播控制器
    监听 outputDeviceChange 事件,当 connectState 为已连接且设备类别为 CATEGORY_REMOTE 时,调用 getAVCastController 获取控制器实例。
  2. 发送播控指令
    通过 avCastController.sendControlCommand 发送播放、暂停、上一首/下一首等指令。同时可调用 prepare 和 start 接口主动控制远端资源的加载与播放。
  3. 监听远端状态回传
    注册 playbackStateChange 和 mediaItemChange 回调,实时同步远端的播放进度、播放状态及当前媒体信息,确保本端 UI 与远端保持一致。
// AvSessionCastSetup.ets
import { avSession } from '@kit.AVSessionKit';

export class AvSessionCastSetup {
    private videoSession: avSession.AVSession | null = null;

    // 1. 创建会话、激活并声明投播能力
    public async initSession(context: Context): Promise<void> {
        this.videoSession = await avSession.createAVSession(context, 'video_cast', 'video');
        await this.videoSession.activate();
        
        // 核心:声明支持投播能力
        await this.videoSession.setExtras({
            requireAbilityList: ['url-cast']
        });
    }

    // 2. 配置媒体元数据(未配置会导致投播组件无法显示或远端黑屏)
    public async setMediaMetadata(title: string, artist: string, coverUri: string) {
        const metadata: avSession.AVMetadata = {
            title: title,
            artist: artist,
            mediaImage: coverUri, // 实际开发中需传入 PixelMap 或有效 URI
            duration: 3600000
        };
        await this.videoSession?.setAVMetadata(metadata);
    }

    // 获取 Session 供 UI 层绑定 AVCastPicker
    public getSession(): avSession.AVSession | null {
        return this.videoSession;
    }
}

在实际落地媒体投影功能时,需特别注意以下工程规范:

  1. PCM 与 Cast+ 协议差异
    PCM 投播模式类似蓝牙连接,无 playbackStateChange 回调,远端状态直接跟随本地;而 Cast+ Stream 模式和 DLNA 协议则有独立的状态回调,需据此同步状态。
  2. 直播投播的特殊处理
    直播场景下不应设置播放初始位置(startPosition)或应设为 0,否则可能导致投播失败。同时,带 Referer 防盗链的网络视频目前不支持投播。
  3. 扩展屏投播模式
    对于需要双屏协作的场景,可通过 getAllCastDisplays() 获取支持扩展屏的设备,在虚拟扩展屏上启动独立的 UIAbility 绘制投屏内容,实现主屏与扩展屏的差异化显示。
// CastEngineeringUtils.ets
import { avSession } from '@kit.AVSessionKit';

export class CastEngineeringUtils {
    // 1. 直播投播特殊处理:startPosition 必须设为 0
    public static getLiveStreamPlayItem(liveUrl: string): avSession.AVQueueItem {
        return {
            itemId: 'live_001',
            description: {
                assetId: 'LIVE-001',
                title: 'Live Broadcast',
                mediaUri: liveUrl,
                mediaType: 'VIDEO',
                duration: 0, // 直播无固定时长
                startPosition: 0 // 核心:不可设置初始位置,否则投播失败
            }
        };
    }

    // 2. 扩展屏投播模式:获取支持扩展屏的设备列表
    public static async getExtendedDisplays(session: avSession.AVSession) {
        try {
            // 获取所有支持扩展屏的远端设备
            const displays = await session.getAllCastDisplays();
            if (displays && displays.length > 0) {
                console.info(`发现 ${displays.length} 个支持扩展屏的设备`);
                // TODO: 在虚拟扩展屏上启动独立的 UIAbility 绘制差异化内容
            }
        } catch (err) {
            console.error('获取扩展屏设备失败:', err);
        }
    }

    // 3. 协议差异提示:PCM 模式无独立状态回调
    public static handleProtocolDifferences(protocolType: number) {
        if (protocolType === avSession.ProtocolType.TYPE_PCM) {
            // PCM 模式远端状态直接跟随本地,无需监听 playbackStateChange
            console.info('当前为 PCM 投播模式,状态自动同步');
        } else {
            // Cast+ Stream 或 DLNA 协议,需依赖回调同步
            console.info('当前为 Cast+/DLNA 协议,需监听状态回调');
        }
    }
}

 

 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐