HarmonyOS APP投屏能力开发
核心要点:深入掌握 HarmonyOS 投屏框架的完整开发链路,从 AVCastPicker 设备选择组件到投屏协议协商,从状态管理到投屏控制,构建稳定可靠的多设备投屏体验。
一、背景与动机
你有没有过这样的体验——手机上看电影觉得屏幕太小,想投到电视上看?或者在会议室里,想把手机上的 PPT 投到大屏上给同事演示?这些场景都离不开"投屏"能力。
投屏,说白了就是把一个设备上的媒体内容"搬"到另一个设备上去显示或播放。听起来简单,但背后涉及设备发现、协议协商、媒体传输、状态同步、控制指令等一系列技术环节。而且,不同品牌的电视、不同协议的设备,兼容性问题更是让人头疼。
HarmonyOS 提供了完整的投屏框架,其中 AVCastPicker 是最核心的 UI 组件——它帮你把"发现设备→选择设备→开始投屏"这个流程封装成了一个开箱即用的选择器。但如果你想实现更精细的控制,比如自定义投屏 UI、管理投屏状态、处理各种异常情况,那就需要深入理解整个投屏链路了。
二、核心原理
2.1 投屏架构全景
投屏框架的架构可以分为四个层次:

2.2 核心概念详解
| 概念 | 说明 |
|---|---|
| AVCastPicker | 系统提供的投屏设备选择 UI 组件,封装了设备发现和选择流程 |
| AVCastSession | 投屏会话,管理一次完整的投屏生命周期 |
| AVCastController | 投屏控制器,提供播放、暂停、跳转等控制能力 |
| CastType | 投屏类型,区分本地投屏和远端投屏 |
| CastState | 投屏状态,包括连接中、已连接、已断开等 |
2.3 投屏协议栈
HarmonyOS 投屏框架支持多种协议,不同协议有不同的特点和适用场景:
flowchart LR
classDef primary fill:#4A90D9,stroke:#2E6BA6,color:#fff,stroke-width:2px
classDef warning fill:#E6A23C,stroke:#CF8C12,color:#fff,stroke-width:2px
classDef error fill:#F56C6C,stroke:#DD3B3B,color:#fff,stroke-width:2px
classDef info fill:#67C23A,stroke:#4AA82A,color:#fff,stroke-width:2px
classDef purple fill:#9B59B6,stroke:#7D3C98,color:#fff,stroke-width:2px
A[协议选择]:::primary --> B{设备能力判断}:::warning
B -->|华为设备| C[华为私有协议]:::info
B -->|通用设备| D[DLNA]:::purple
B -->|镜像投屏| E[Miracast]:::error
C --> F[低延迟 高画质]:::info
D --> G[兼容性好 延迟较高]:::purple
E --> H[实时镜像 画质一般]:::error
三、代码实战
3.1 AVCastPicker 基础用法
AVCastPicker 是最简单的投屏入口,几行代码就能实现设备选择和投屏发起。
// CastPickerBasic.ets
// AVCastPicker 投屏选择器基础用法
import { avCastPicker } from '@kit.MediaKit';
import { avSession } from '@kit.AvSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct CastPickerBasic {
// 投屏会话
private castSession: avSession.AVSession | null = null;
// 投屏状态
@State castState: string = '未连接';
@State isConnected: boolean = false;
aboutToAppear() {
this.initCastSession();
}
// 初始化投屏会话
async initCastSession() {
try {
// 创建 AVSession,作为投屏的媒体源
this.castSession = await avSession.createAVSession(this.getUIContext(), '投屏示例', 'audio');
// 设置媒体元数据
const metadata: avSession.AVMetadata = {
assetId: 'media_001',
title: '示例视频',
artist: 'HarmonyOS',
mediaType: 'VIDEO',
duration: 3600000 // 1小时,单位毫秒
};
this.castSession.setAVMetadata(metadata);
// 设置播放状态
const playbackState: avSession.AVPlaybackState = {
state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
speed: 1.0,
position: { elapsedTime: 0, updateTime: Date.now() },
bufferedTime: 30000
};
this.castSession.setAVPlaybackState(playbackState);
console.info('[投屏] 会话初始化成功');
} catch (error) {
const err = error as BusinessError;
console.error(`[投屏] 会话初始化失败: ${err.code} - ${err.message}`);
}
}
// 投屏状态回调
private onCastStateChange(state: avCastPicker.CastState) {
switch (state) {
case avCastPicker.CastState.STATE_CONNECTING:
this.castState = '连接中...';
this.isConnected = false;
break;
case avCastPicker.CastState.STATE_CONNECTED:
this.castState = '已连接';
this.isConnected = true;
console.info('[投屏] 设备已连接');
break;
case avCastPicker.CastState.STATE_DISCONNECTED:
this.castState = '已断开';
this.isConnected = false;
console.info('[投屏] 设备已断开');
break;
default:
this.castState = '未知状态';
break;
}
}
aboutToDisappear() {
// 释放投屏会话
this.castSession?.destroy();
}
build() {
Column() {
// 标题
Text('投屏示例')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 20 })
// 投屏状态显示
Row() {
Circle({ width: 10, height: 10 })
.fill(this.isConnected ? '#67C23A' : '#909399')
Text(this.castState)
.fontSize(16)
.margin({ left: 8 })
.fontWeight(FontWeight.Medium)
}
.margin({ bottom: 24 })
// AVCastPicker 投屏按钮
// 这是系统提供的标准投屏选择器组件
AVCastPicker({
// 投屏状态变更回调
onCastStateChange: (state: avCastPicker.CastState) => {
this.onCastStateChange(state);
},
// 投屏会话
session: this.castSession!
})
.width(56)
.height(56)
Text('点击上方按钮选择投屏设备')
.fontSize(13)
.fontColor('#999999')
.margin({ top: 12 })
// 媒体信息展示
Column() {
Text('当前播放内容')
.fontSize(14)
.fontColor('#999999')
Text('示例视频 - HarmonyOS')
.fontSize(16)
.fontWeight(FontWeight.Medium)
.margin({ top: 4 })
}
.margin({ top: 32 })
.padding(16)
.borderRadius(12)
.backgroundColor('#F5F7FA')
.width('100%')
}
.width('100%')
.height('100%')
.padding(16)
.alignItems(HorizontalAlign.Center)
}
}
3.2 投屏状态管理与控制
投屏不仅仅是"连上就行",还需要精细的状态管理和播放控制。下面展示一个完整的投屏状态机和控制面板。
// CastStateManager.ets
// 投屏状态管理与控制面板
import { avCastPicker, avCastController } from '@kit.MediaKit';
import { avSession } from '@kit.AvSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 投屏状态枚举
enum CastSessionState {
IDLE = 'idle', // 空闲
DISCOVERING = 'discovering', // 发现设备中
CONNECTING = 'connecting', // 连接中
CONNECTED = 'connected', // 已连接
CASTING = 'casting', // 投屏中
PAUSED = 'paused', // 暂停
DISCONNECTING = 'disconnecting', // 断开中
ERROR = 'error' // 错误
}
@Entry
@Component
struct CastStateManager {
// 投屏状态
@State sessionState: CastSessionState = CastSessionState.IDLE;
// 投屏控制器
private castController: avCastController.AVCastController | null = null;
// 播放进度(毫秒)
@State currentPosition: number = 0;
// 总时长(毫秒)
@State totalDuration: number = 3600000;
// 音量
@State volume: number = 50;
// 播放速度
@State playbackSpeed: number = 1.0;
// 错误信息
@State errorMessage: string = '';
// 初始化投屏控制器
async initCastController() {
try {
this.castController = await avCastController.createAVCastController();
// 注册控制命令回调
this.castController.on('playbackStateChange', (state: avCastController.AVPlaybackState) => {
console.info(`[投屏控制] 播放状态变更: ${state}`);
this.handlePlaybackStateChange(state);
});
// 注册进度变更回调
this.castController.on('positionChange', (position: avCastController.AVPosition) => {
this.currentPosition = position.elapsedTime;
});
console.info('[投屏控制] 控制器初始化成功');
} catch (error) {
const err = error as BusinessError;
console.error(`[投屏控制] 控制器初始化失败: ${err.code} - ${err.message}`);
}
}
// 处理播放状态变更
private handlePlaybackStateChange(state: avCastController.AVPlaybackState) {
switch (state) {
case avCastController.AVPlaybackState.PLAYBACK_STATE_PLAY:
this.sessionState = CastSessionState.CASTING;
break;
case avCastController.AVPlaybackState.PLAYBACK_STATE_PAUSE:
this.sessionState = CastSessionState.PAUSED;
break;
case avCastController.AVPlaybackState.PLAYBACK_STATE_STOP:
this.sessionState = CastSessionState.CONNECTED;
break;
default:
break;
}
}
// 播放控制
async play() {
try {
await this.castController?.play();
console.info('[投屏控制] 播放');
} catch (error) {
this.handleControlError(error, '播放');
}
}
// 暂停控制
async pause() {
try {
await this.castController?.pause();
console.info('[投屏控制] 暂停');
} catch (error) {
this.handleControlError(error, '暂停');
}
}
// 停止控制
async stop() {
try {
await this.castController?.stop();
console.info('[投屏控制] 停止');
} catch (error) {
this.handleControlError(error, '停止');
}
}
// 跳转进度
async seekTo(positionMs: number) {
try {
await this.castController?.seek(positionMs);
this.currentPosition = positionMs;
console.info(`[投屏控制] 跳转到 ${positionMs}ms`);
} catch (error) {
this.handleControlError(error, '跳转');
}
}
// 设置音量
async setVolume(vol: number) {
try {
await this.castController?.setVolume(vol);
this.volume = vol;
console.info(`[投屏控制] 音量设为 ${vol}`);
} catch (error) {
this.handleControlError(error, '设置音量');
}
}
// 设置播放速度
async setSpeed(speed: number) {
try {
await this.castController?.setSpeed(speed);
this.playbackSpeed = speed;
console.info(`[投屏控制] 速度设为 ${speed}x`);
} catch (error) {
this.handleControlError(error, '设置速度');
}
}
// 断开投屏
async disconnect() {
try {
this.sessionState = CastSessionState.DISCONNECTING;
await this.castController?.release();
this.castController = null;
this.sessionState = CastSessionState.IDLE;
this.currentPosition = 0;
console.info('[投屏控制] 已断开投屏');
} catch (error) {
this.handleControlError(error, '断开');
}
}
// 统一错误处理
private handleControlError(error: Error, action: string) {
const err = error as BusinessError;
this.errorMessage = `${action}失败: ${err.message}`;
this.sessionState = CastSessionState.ERROR;
console.error(`[投屏控制] ${action}失败: ${err.code} - ${err.message}`);
}
build() {
Column() {
// 状态指示器
Row() {
Circle({ width: 8, height: 8 })
.fill(this.getStateColor())
Text(this.getStateLabel())
.fontSize(14)
.margin({ left: 6 })
.fontColor(this.getStateColor())
}
.margin({ bottom: 16 })
// 进度条
Column() {
Row() {
Text(this.formatTime(this.currentPosition))
.fontSize(12)
.fontColor('#999999')
Blank()
Text(this.formatTime(this.totalDuration))
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
Slider({
value: this.currentPosition,
min: 0,
max: this.totalDuration,
step: 1000
})
.width('100%')
.trackColor('#E8E8E8')
.selectedColor('#4A90D9')
.onChange((value: number) => {
this.seekTo(value);
})
}
.margin({ bottom: 16 })
// 播放控制按钮组
Row() {
// 后退15秒
Button() {
Text('-15s')
.fontSize(12)
.fontColor(Color.White)
}
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor('#606266')
.onClick(() => this.seekTo(Math.max(0, this.currentPosition - 15000)))
// 播放/暂停
Button() {
Text(this.sessionState === CastSessionState.CASTING ? '⏸' : '▶')
.fontSize(24)
.fontColor(Color.White)
}
.width(64)
.height(64)
.borderRadius(32)
.backgroundColor('#4A90D9')
.onClick(() => {
if (this.sessionState === CastSessionState.CASTING) {
this.pause();
} else {
this.play();
}
})
// 前进15秒
Button() {
Text('+15s')
.fontSize(12)
.fontColor(Color.White)
}
.width(48)
.height(48)
.borderRadius(24)
.backgroundColor('#606266')
.onClick(() => this.seekTo(Math.min(this.totalDuration, this.currentPosition + 15000)))
}
.justifyContent(FlexAlign.SpaceEvenly)
.width('100%')
.margin({ bottom: 16 })
// 音量控制
Row() {
Text('🔊')
.fontSize(16)
Slider({
value: this.volume,
min: 0,
max: 100,
step: 1
})
.layoutWeight(1)
.trackColor('#E8E8E8')
.selectedColor('#67C23A')
.onChange((value: number) => {
this.setVolume(value);
})
Text(`${this.volume}%`)
.fontSize(12)
.fontColor('#666666')
.width(40)
}
.width('100%')
.margin({ bottom: 16 })
// 速度选择
Row() {
Text('速度:')
.fontSize(14)
.margin({ right: 8 })
ForEach([0.5, 1.0, 1.5, 2.0], (speed: number) => {
Button(`${speed}x`)
.height(28)
.fontSize(12)
.onClick(() => this.setSpeed(speed))
.backgroundColor(this.playbackSpeed === speed ? '#4A90D9' : '#E8E8E8')
.fontColor(this.playbackSpeed === speed ? Color.White : '#333333')
.margin({ right: 6 })
}, (speed: number) => speed.toString())
}
.margin({ bottom: 16 })
// 断开按钮
Button('断开投屏')
.width('100%')
.height(44)
.backgroundColor('#F56C6C')
.fontColor(Color.White)
.onClick(() => this.disconnect())
.enabled(this.sessionState !== CastSessionState.IDLE)
// 错误信息
if (this.errorMessage) {
Text(this.errorMessage)
.fontSize(12)
.fontColor('#F56C6C')
.margin({ top: 8 })
}
}
.width('100%')
.height('100%')
.padding(16)
}
// 获取状态颜色
private getStateColor(): string {
switch (this.sessionState) {
case CastSessionState.CASTING: return '#67C23A';
case CastSessionState.PAUSED: return '#E6A23C';
case CastSessionState.CONNECTING:
case CastSessionState.DISCONNECTING: return '#4A90D9';
case CastSessionState.ERROR: return '#F56C6C';
default: return '#909399';
}
}
// 获取状态标签
private getStateLabel(): string {
switch (this.sessionState) {
case CastSessionState.IDLE: return '未连接';
case CastSessionState.DISCOVERING: return '发现设备中...';
case CastSessionState.CONNECTING: return '连接中...';
case CastSessionState.CONNECTED: return '已连接';
case CastSessionState.CASTING: return '投屏中';
case CastSessionState.PAUSED: return '已暂停';
case CastSessionState.DISCONNECTING: return '断开中...';
case CastSessionState.ERROR: return '错误';
default: return '未知';
}
}
// 格式化时间
private formatTime(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
}
3.3 投屏兼容性处理
不同设备支持不同的投屏协议和能力,兼容性处理是投屏开发中最让人头疼的部分。下面展示一个兼容性适配方案。
// CastCompatibility.ets
// 投屏兼容性处理方案
import { avCastPicker, avCastController } from '@kit.MediaKit';
import { avSession } from '@kit.AvSessionKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 设备能力描述
interface DeviceCapability {
deviceId: string;
deviceName: string;
supportedProtocols: string[]; // 支持的投屏协议
maxResolution: string; // 最大分辨率
supportsSeek: boolean; // 是否支持跳转
supportsVolumeControl: boolean; // 是否支持音量控制
supportsSpeedControl: boolean; // 是否支持变速
latencyMs: number; // 延迟(毫秒)
}
// 兼容性降级策略
interface FallbackStrategy {
preferredProtocol: string;
fallbackProtocols: string[];
maxRetries: number;
retryDelayMs: number;
}
@Entry
@Component
struct CastCompatibility {
// 设备能力信息
@State deviceCapability: DeviceCapability | null = null;
// 兼容性检测结果
@State compatibilityReport: string[] = [];
// 当前使用的协议
@State activeProtocol: string = '未连接';
// 降级状态
@State isFallback: boolean = false;
// 投屏质量
@State castQuality: string = '未知';
// 默认降级策略
private defaultStrategy: FallbackStrategy = {
preferredProtocol: 'huawei-cast',
fallbackProtocols: ['dlna', 'miracast'],
maxRetries: 3,
retryDelayMs: 2000
};
// 检测设备兼容性
async checkDeviceCompatibility(deviceId: string): Promise<DeviceCapability> {
const capability: DeviceCapability = {
deviceId: deviceId,
deviceName: '',
supportedProtocols: [],
maxResolution: '1080p',
supportsSeek: true,
supportsVolumeControl: true,
supportsSpeedControl: false,
latencyMs: 100
};
this.compatibilityReport = [];
try {
// 查询设备支持的能力
const deviceInfo = await this.queryDeviceInfo(deviceId);
capability.deviceName = deviceInfo.deviceName;
capability.supportedProtocols = deviceInfo.protocols;
capability.maxResolution = deviceInfo.maxResolution;
// 逐一检测各项能力
await this.checkSeekCapability(capability);
await this.checkVolumeCapability(capability);
await this.checkSpeedCapability(capability);
await this.checkLatency(capability);
this.deviceCapability = capability;
console.info(`[兼容性] 设备 ${deviceInfo.deviceName} 兼容性检测完成`);
} catch (error) {
const err = error as BusinessError;
this.compatibilityReport.push(`⚠ 设备信息查询失败: ${err.message}`);
console.error(`[兼容性] 检测失败: ${err.code} - ${err.message}`);
}
return capability;
}
// 查询设备基本信息
private async queryDeviceInfo(deviceId: string): Promise<{
deviceName: string;
protocols: string[];
maxResolution: string;
}> {
// 模拟查询设备信息
// 实际开发中应通过 avRouter 获取设备详细信息
return {
deviceName: '智能电视',
protocols: ['huawei-cast', 'dlna'],
maxResolution: '4K'
};
}
// 检测跳转能力
private async checkSeekCapability(capability: DeviceCapability) {
try {
// 尝试执行 seek 操作来检测是否支持
capability.supportsSeek = capability.supportedProtocols.includes('huawei-cast') ||
capability.supportedProtocols.includes('dlna');
if (capability.supportsSeek) {
this.compatibilityReport.push('✅ 支持进度跳转');
} else {
this.compatibilityReport.push('❌ 不支持进度跳转(Miracast 镜像模式)');
}
} catch (error) {
capability.supportsSeek = false;
this.compatibilityReport.push('⚠ 进度跳转检测异常,默认不支持');
}
}
// 检测音量控制能力
private async checkVolumeCapability(capability: DeviceCapability) {
capability.supportsVolumeControl = capability.supportedProtocols.includes('huawei-cast');
if (capability.supportsVolumeControl) {
this.compatibilityReport.push('✅ 支持远端音量控制');
} else {
this.compatibilityReport.push('❌ 不支持远端音量控制,需使用设备遥控器');
}
}
// 检测变速播放能力
private async checkSpeedCapability(capability: DeviceCapability) {
capability.supportsSpeedControl = capability.supportedProtocols.includes('huawei-cast');
if (capability.supportsSpeedControl) {
this.compatibilityReport.push('✅ 支持变速播放');
} else {
this.compatibilityReport.push('❌ 不支持变速播放');
}
}
// 检测延迟
private async checkLatency(capability: DeviceCapability) {
if (capability.supportedProtocols.includes('huawei-cast')) {
capability.latencyMs = 50;
this.compatibilityReport.push('✅ 低延迟模式(~50ms)');
} else if (capability.supportedProtocols.includes('dlna')) {
capability.latencyMs = 500;
this.compatibilityReport.push('⚠ 中等延迟(~500ms),不适合实时互动');
} else {
capability.latencyMs = 200;
this.compatibilityReport.push('⚠ 镜像延迟(~200ms),适合演示');
}
}
// 根据兼容性选择最佳协议
selectBestProtocol(capability: DeviceCapability, strategy: FallbackStrategy): string {
// 优先使用首选协议
if (capability.supportedProtocols.includes(strategy.preferredProtocol)) {
this.isFallback = false;
this.castQuality = '高清';
this.activeProtocol = strategy.preferredProtocol;
return strategy.preferredProtocol;
}
// 降级到备选协议
for (const fallback of strategy.fallbackProtocols) {
if (capability.supportedProtocols.includes(fallback)) {
this.isFallback = true;
this.castQuality = fallback === 'dlna' ? '标清' : '镜像';
this.activeProtocol = fallback;
this.compatibilityReport.push(`⚠ 已降级到 ${fallback} 协议`);
return fallback;
}
}
// 无可用协议
this.compatibilityReport.push('❌ 无可用投屏协议');
return '';
}
// 根据设备能力调整投屏参数
adjustCastParams(capability: DeviceCapability): avCastController.AVCastControlParams {
const params: avCastController.AVCastControlParams = {
// 根据最大分辨率调整
videoParams: {
width: capability.maxResolution === '4K' ? 3840 : 1920,
height: capability.maxResolution === '4K' ? 2160 : 1080,
}
};
return params;
}
build() {
Scroll() {
Column() {
// 标题
Text('投屏兼容性检测')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 16 })
// 当前协议信息
Row() {
Text('当前协议:')
.fontSize(14)
.fontColor('#666666')
Text(this.activeProtocol)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.isFallback ? '#E6A23C' : '#67C23A')
.margin({ left: 4 })
if (this.isFallback) {
Text('(降级)')
.fontSize(11)
.fontColor('#E6A23C')
.margin({ left: 4 })
}
}
.margin({ bottom: 8 })
// 投屏质量
Row() {
Text('投屏质量:')
.fontSize(14)
.fontColor('#666666')
Text(this.castQuality)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.margin({ left: 4 })
}
.margin({ bottom: 16 })
// 兼容性报告
Text('兼容性报告')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Column() {
ForEach(this.compatibilityReport, (report: string, index?: number) => {
Text(report)
.fontSize(14)
.width('100%')
.padding({ top: 6, bottom: 6 })
.borderBottomWidth(0.5)
.borderBottomColor('#EEEEEE')
}, (report: string, index?: number) => `${index}`)
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#F5F7FA')
.margin({ bottom: 16 })
// 设备能力详情
if (this.deviceCapability) {
Text('设备能力详情')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.margin({ bottom: 8 })
Column() {
this.CapabilityRow('设备名称', this.deviceCapability.deviceName)
this.CapabilityRow('最大分辨率', this.deviceCapability.maxResolution)
this.CapabilityRow('支持协议', this.deviceCapability.supportedProtocols.join(', '))
this.CapabilityRow('延迟', `${this.deviceCapability.latencyMs}ms`)
this.CapabilityRow('进度跳转', this.deviceCapability.supportsSeek ? '✅' : '❌')
this.CapabilityRow('音量控制', this.deviceCapability.supportsVolumeControl ? '✅' : '❌')
this.CapabilityRow('变速播放', this.deviceCapability.supportsSpeedControl ? '✅' : '❌')
}
.width('100%')
.padding(12)
.borderRadius(8)
.backgroundColor('#FFFFFF')
.border({ width: 1, color: '#E8E8E8' })
}
// 操作按钮
Button('开始兼容性检测')
.width('100%')
.height(44)
.backgroundColor('#4A90D9')
.fontColor(Color.White)
.margin({ top: 16 })
.onClick(() => {
this.checkDeviceCompatibility('demo_device_001');
})
}
.padding(16)
}
.width('100%')
.height('100%')
}
// 能力行组件
@Builder
CapabilityRow(label: string, value: string) {
Row() {
Text(label)
.fontSize(14)
.fontColor('#999999')
.width(80)
Text(value)
.fontSize(14)
.fontWeight(FontWeight.Medium)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 8, bottom: 8 })
.borderBottomWidth(0.5)
.borderBottomColor('#EEEEEE')
}
}
四、踩坑与注意事项
4.1 投屏连接常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接超时 | 设备响应慢或网络不稳定 | 设置 10 秒超时,提供重试机制 |
| 连接后无画面 | 协议协商失败或编解码不支持 | 降级协议或调整分辨率 |
| 频繁断连 | WiFi 信号弱或设备休眠 | 增加心跳检测,唤醒设备 |
| 画面卡顿 | 带宽不足或编码延迟过高 | 降低分辨率/帧率,切换协议 |
4.2 AVCastPicker 使用注意
- 必须先创建 AVSession:AVCastPicker 依赖 AVSession,不创建会话就使用 Picker 会导致崩溃
- 会话元数据要完整:至少设置 title 和 mediaType,否则远端设备可能无法正确显示
- 生命周期管理:页面销毁时必须调用
session.destroy()释放资源 - UI 线程创建:AVCastPicker 必须在 UI 线程创建,不能在子线程
4.3 投屏控制注意事项
- 控制指令有延迟:远端设备的响应不是即时的,UI 状态更新要考虑网络延迟
- 不是所有设备都支持所有控制:seek、speed、volume 等能力因设备而异,务必先检测
- 并发控制:不要同时发送多个控制指令,等上一个完成再发下一个
- 断线重连:投屏过程中网络断开是常态,需要实现自动重连机制
4.4 兼容性处理要点
- 协议降级要有策略:优先使用高质量协议,降级时要有明确的用户提示
- 分辨率自适应:不要一上来就发 4K 流,先检测设备支持的最大分辨率
- 延迟感知:不同协议延迟差异很大,实时互动场景(如游戏投屏)要选低延迟协议
- 功能降级:不支持 seek 的设备,UI 上应该隐藏进度条拖动功能
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| AVCastPicker | 基础设备选择 | 新增自定义样式和过滤能力 |
| 投屏控制 | 基本播放控制 | 新增字幕控制、画面比例调整 |
| 协议支持 | DLNA/Miracast | 新增 Cast+ 协议(更低延迟) |
| 多设备投屏 | 单设备 | 支持同时投屏到多个设备 |
5.2 迁移指南
// HarmonyOS 5 写法
AVCastPicker({
onCastStateChange: (state) => { /* ... */ },
session: this.castSession
})
// HarmonyOS 6 写法(新增自定义能力)
AVCastPicker({
onCastStateChange: (state) => { /* ... */ },
session: this.castSession,
// 新增:自定义过滤条件
filter: {
deviceTypes: [avCastPicker.DeviceType.DEVICE_TYPE_TV],
minProtocolVersion: 2,
supportedFeatures: ['seek', 'volume']
},
// 新增:自定义样式
style: {
iconColor: '#4A90D9',
backgroundColor: '#FFFFFF',
cornerRadius: 28
}
})
5.3 新特性:多设备投屏
HarmonyOS 6 支持同时投屏到多个设备,实现"一源多屏":
// HarmonyOS 6 新增:多设备投屏
const multiCastSession = await avCastController.createMultiCastSession({
maxDevices: 3, // 最多同时投屏3个设备
syncMode: avCastController.SyncMode.SYNC_MODE_MASTER // 主从同步模式
});
// 添加目标设备
await multiCastSession.addDevice(device1);
await multiCastSession.addDevice(device2);
// 同步播放控制
await multiCastSession.play(); // 所有设备同时播放
六、总结
mindmap
root((投屏能力))
AVCastPicker
系统选择器组件
依赖 AVSession
状态回调监听
生命周期管理
投屏协议
华为私有协议
DLNA
Miracast
Cast+(HarmoneyOS 6)
状态管理
连接状态机
播放状态同步
断线重连
错误恢复
投屏控制
播放/暂停/停止
进度跳转
音量控制
变速播放
兼容性
协议降级策略
分辨率自适应
功能降级
延迟感知
HarmonyOS 6
自定义 Picker
Cast+ 协议
多设备投屏
字幕控制
关键知识点回顾:
- AVCastPicker 是最简入口:几行代码就能实现投屏,但必须先创建 AVSession
- 协议选择决定体验:华为私有协议体验最好,DLNA 兼容性最广,Miracast 适合镜像
- 状态管理要严谨:投屏状态机要覆盖所有可能的状态转换,异常路径不能遗漏
- 控制指令要检测:不是所有设备都支持所有控制,先检测能力再发指令
- 兼容性是硬仗:协议降级、分辨率适配、功能降级,缺一不可
一句话总结:投屏的本质是"把媒体搬到更大的屏幕上",但让这个过程稳定、流畅、兼容,才是真正的技术挑战。
- 点赞
- 收藏
- 关注作者
评论(0)