HarmonyOS APP投屏能力开发

举报
Jack20 发表于 2026/06/21 11:46:36 2026/06/21
【摘要】 核心要点:深入掌握 HarmonyOS 投屏框架的完整开发链路,从 AVCastPicker 设备选择组件到投屏协议协商,从状态管理到投屏控制,构建稳定可靠的多设备投屏体验。 一、背景与动机你有没有过这样的体验——手机上看电影觉得屏幕太小,想投到电视上看?或者在会议室里,想把手机上的 PPT 投到大屏上给同事演示?这些场景都离不开"投屏"能力。投屏,说白了就是把一个设备上的媒体内容"搬"到另...

核心要点:深入掌握 HarmonyOS 投屏框架的完整开发链路,从 AVCastPicker 设备选择组件到投屏协议协商,从状态管理到投屏控制,构建稳定可靠的多设备投屏体验。


一、背景与动机

你有没有过这样的体验——手机上看电影觉得屏幕太小,想投到电视上看?或者在会议室里,想把手机上的 PPT 投到大屏上给同事演示?这些场景都离不开"投屏"能力。

投屏,说白了就是把一个设备上的媒体内容"搬"到另一个设备上去显示或播放。听起来简单,但背后涉及设备发现、协议协商、媒体传输、状态同步、控制指令等一系列技术环节。而且,不同品牌的电视、不同协议的设备,兼容性问题更是让人头疼。

HarmonyOS 提供了完整的投屏框架,其中 AVCastPicker 是最核心的 UI 组件——它帮你把"发现设备→选择设备→开始投屏"这个流程封装成了一个开箱即用的选择器。但如果你想实现更精细的控制,比如自定义投屏 UI、管理投屏状态、处理各种异常情况,那就需要深入理解整个投屏链路了。


二、核心原理

2.1 投屏架构全景

投屏框架的架构可以分为四个层次:
图片.png

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 使用注意

  1. 必须先创建 AVSession:AVCastPicker 依赖 AVSession,不创建会话就使用 Picker 会导致崩溃
  2. 会话元数据要完整:至少设置 title 和 mediaType,否则远端设备可能无法正确显示
  3. 生命周期管理:页面销毁时必须调用 session.destroy() 释放资源
  4. UI 线程创建:AVCastPicker 必须在 UI 线程创建,不能在子线程

4.3 投屏控制注意事项

  • 控制指令有延迟:远端设备的响应不是即时的,UI 状态更新要考虑网络延迟
  • 不是所有设备都支持所有控制:seek、speed、volume 等能力因设备而异,务必先检测
  • 并发控制:不要同时发送多个控制指令,等上一个完成再发下一个
  • 断线重连:投屏过程中网络断开是常态,需要实现自动重连机制

4.4 兼容性处理要点

  1. 协议降级要有策略:优先使用高质量协议,降级时要有明确的用户提示
  2. 分辨率自适应:不要一上来就发 4K 流,先检测设备支持的最大分辨率
  3. 延迟感知:不同协议延迟差异很大,实时互动场景(如游戏投屏)要选低延迟协议
  4. 功能降级:不支持 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+ 协议
      多设备投屏
      字幕控制

关键知识点回顾

  1. AVCastPicker 是最简入口:几行代码就能实现投屏,但必须先创建 AVSession
  2. 协议选择决定体验:华为私有协议体验最好,DLNA 兼容性最广,Miracast 适合镜像
  3. 状态管理要严谨:投屏状态机要覆盖所有可能的状态转换,异常路径不能遗漏
  4. 控制指令要检测:不是所有设备都支持所有控制,先检测能力再发指令
  5. 兼容性是硬仗:协议降级、分辨率适配、功能降级,缺一不可

一句话总结:投屏的本质是"把媒体搬到更大的屏幕上",但让这个过程稳定、流畅、兼容,才是真正的技术挑战。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。