在鸿蒙(HarmonyOS)开发中,AVPlayer 是功能最完善的音视频播放引擎,集成了流媒体和本地资源解析、媒体解封装、解码和渲染功能。无论是播放音频还是视频,都遵循一套严格的状态机流转机制。

以下是使用 AVPlayer 播放音视频的完整流程与核心指南:

一、 核心状态机流转

AVPlayer 的执行逻辑强依赖于其内部状态(state)。如果在错误的状态下调用方法,系统会抛出异常。核心状态流转如下:

  1. idle(空闲):创建实例后的初始状态。
  2. initialized(已初始化):设置资源(URL/FD)后的状态。
  3. prepared(准备就绪):调用 prepare() 后的状态,此时可获取时长、设置音量等。
  4. playing(播放中):调用 play() 后的状态。
  5. paused(暂停):调用 pause() 后的状态。
  6. completed(完成):播放自然结束后的状态。
  7. released(已释放):调用 release() 销毁实例后的状态。
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct AVPlayerStateDemo {
  // 声明 AVPlayer 实例
  private avPlayer: media.AVPlayer | null = null;
  // 用于 UI 显示当前状态
  @State currentState: string = 'Idle';

  aboutToAppear() {
    this.initAVPlayer();
  }

  // 1. 创建实例并绑定状态机监听
  private async initAVPlayer() {
    try {
      this.avPlayer = await media.createAVPlayer();
      
      // 核心:监听状态变化(必须在设置资源前完成)
      this.avPlayer.on('stateChange', (state: string) => {
        console.info(`AVPlayer state changed to: ${state}`);
        this.currentState = state;
        
        switch (state) {
          case 'idle':
            // 初始状态,准备设置资源
            this.setSource();
            break;
          case 'initialized':
            // 资源设置成功,进入准备阶段
            this.avPlayer?.prepare();
            break;
          case 'prepared':
            // 准备就绪,可以获取时长、音量等,并执行播放
            console.info(`Duration: ${this.avPlayer?.duration}ms`);
            // 此处不自动播放,留给按钮控制
            break;
          case 'playing':
            // 正在播放中
            break;
          case 'paused':
            // 暂停状态
            break;
          case 'completed':
            // 播放自然结束
            console.info('Playback completed');
            break;
          case 'stopped':
            // 停止状态,可重新 prepare 或 release
            break;
          case 'released':
            // 资源已释放
            break;
        }
      });

      // 监听错误事件
      this.avPlayer.on('error', (err: BusinessError) => {
        console.error(`AVPlayer Error: Code=${err.code}, Message=${err.message}`);
        // 发生错误时通常需要重置或释放资源
        this.avPlayer?.reset();
      });

    } catch (error) {
      console.error(`Create AVPlayer failed: ${(error as BusinessError).message}`);
    }
  }

  // 2. 设置播放资源(以网络 URL 为例,需 INTERNET 权限)
  private setSource() {
    if (this.avPlayer) {
      // 设置 URL 后,状态机会自动流转到 initialized
      this.avPlayer.url = 'https://example.com/sample_audio.mp3'; 
    }
  }

  // 3. 基础播控方法(严格遵循状态机规则)
  private togglePlayPause() {
    if (!this.avPlayer) return;
    
    // 只有在 prepared, playing, paused, completed 状态下才能调用 play
    if (['prepared', 'paused', 'completed'].includes(this.currentState)) {
      this.avPlayer.play();
    } 
    // 只有在 playing 状态下才能调用 pause
    else if (this.currentState === 'playing') {
      this.avPlayer.pause();
    }
  }

  // 4. 切换资源或重置
  private resetPlayer() {
    // 在 prepared, playing, paused, completed 状态下可调用 reset
    // 调用后状态机会回到 idle
    this.avPlayer?.reset();
  }

  // 5. 销毁资源(页面退出时必须调用)
  aboutToDisappear() {
    if (this.avPlayer) {
      this.avPlayer.release();
      this.avPlayer = null;
    }
  }

  build() {
    Column({ space: 20 }) {
      Text(`当前状态: ${this.currentState}`)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Button('播放 / 暂停')
        .onClick(() => this.togglePlayPause())
        .enabled(['prepared', 'playing', 'paused', 'completed'].includes(this.currentState))

      Button('重置 (Reset)')
        .onClick(() => this.resetPlayer())
        .enabled(['prepared', 'playing', 'paused', 'completed', 'stopped'].includes(this.currentState))
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

二、 完整开发步骤

1. 创建实例与注册监听

首先创建 AVPlayer 实例,并务必在设置资源前注册状态和错误监听,以免错过状态变化事件。

import { media } from '@kit.MediaKit';

// 创建 AVPlayer 实例
let avPlayer: media.AVPlayer = await media.createAVPlayer();

// 监听状态变化(核心)
avPlayer.on('stateChange', (state: string) => {
  switch (state) {
    case 'initialized': 
      avPlayer.prepare(); // 资源设置成功后,进入准备阶段
      break;
    case 'prepared': 
      avPlayer.play();    // 准备就绪后,开始播放
      break;
  }
});

// 监听错误信息
avPlayer.on('error', (err) => {
  console.error(`播放器错误: ${err.message}`);
});
2. 设置播放资源

根据资源类型选择不同的设置方式:

  • 网络资源:直接设置 url 属性(需申请 ohos.permission.INTERNET 权限)。
  • 本地 Rawfile 资源:通过 resourceManager.getRawFd() 获取文件描述符,赋值给 fdSrc 属性。
  • 沙箱文件:通过 fs.open() 获取沙箱路径的文件描述符,赋值给 fdSrc 或使用 fd:// 协议赋值给 url
3. 视频专属:设置显示窗口

如果播放的是视频,必须获取显示画面的 surfaceId 并绑定给 AVPlayer

  • 在 UI 层使用 XComponent 组件。
  • 在 XComponent 的 onLoad 回调中,通过 getXComponentSurfaceId() 获取 surfaceId
  • 将 surfaceId 赋值给 avPlayer.surfaceId
4. 播放控制与参数设置

在 preparedplaying 或 paused 状态下,可以执行播控操作和参数调节:

  • 基础播控play()(播放)、pause()(暂停)、stop()(停止)。
  • 进度跳转seek(timeMs),配合 on('seekDone') 监听跳转完成。
  • 参数调节setVolume()(音量)、setSpeed()(倍速)、setMediaMuted()(静音)。
5. 资源释放与重置
  • 切换视频/音频:当需要播放下一个资源时,调用 reset() 方法,播放器会回到 idle 状态,允许重新设置资源。
  • 退出页面/销毁:当不再需要播放器时,必须调用 release() 销毁实例,进入 released 状态,以回收系统内存资源。
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct AVPlayerVideoDemo {
  private avPlayer: media.AVPlayer | null = null;
  private xComponentController: XComponentController = new XComponentController();
  
  // 用于暂存 SurfaceId
  private surfaceId: string = '';
  // 视频资源 URL(需 INTERNET 权限)
  private videoUrl: string = 'https://www.w3schools.com/html/mov_bbb.mp4';

  @State isPlaying: boolean = false;
  @State currentTime: number = 0;
  @State duration: number = 0;

  aboutToAppear() {
    this.initAVPlayer();
  }

  private async initAVPlayer() {
    try {
      this.avPlayer = await media.createAVPlayer();

      // 1. 注册状态监听
      this.avPlayer.on('stateChange', (state: string) => {
        switch (state) {
          case 'initialized':
            // 【3. 视频专属:设置显示窗口】
            // ⚠️ 注意:surfaceId 必须在 initialized 状态下设置,不能在 idle 状态设置!
            if (this.avPlayer && this.surfaceId) {
              this.avPlayer.surfaceId = this.surfaceId;
              this.avPlayer.prepare();
            }
            break;
          case 'prepared':
            // 【4. 播放控制】准备就绪后,可获取时长并播放
            this.duration = this.avPlayer!.duration / 1000;
            this.avPlayer!.play();
            this.isPlaying = true;
            break;
          case 'playing':
            this.isPlaying = true;
            break;
          case 'paused':
            this.isPlaying = false;
            break;
          case 'completed':
            this.isPlaying = false;
            break;
        }
      });

      // 监听播放进度
      this.avPlayer.on('timeUpdate', (time: number) => {
        this.currentTime = time / 1000;
      });

      // 监听跳转完成
      this.avPlayer.on('seekDone', (seekDoneTime: number) => {
        console.info(`Seek completed at: ${seekDoneTime}ms`);
      });

      // 监听错误
      this.avPlayer.on('error', (err: BusinessError) => {
        console.error(`AVPlayer Error: ${err.message}`);
        this.avPlayer?.reset();
      });

    } catch (error) {
      console.error(`Create AVPlayer failed: ${(error as BusinessError).message}`);
    }
  }

  // 2. 设置播放资源
  private setSource() {
    if (this.avPlayer) {
      // 网络资源:直接设置 url
      this.avPlayer.url = this.videoUrl;
    }
  }

  // 【4. 播放控制与参数设置】
  private togglePlayPause() {
    if (!this.avPlayer) return;
    if (this.isPlaying) {
      this.avPlayer.pause();
    } else {
      this.avPlayer.play();
    }
  }

  private seekTo(timeMs: number) {
    this.avPlayer?.seek(timeMs, media.SeekMode.SEEK_PREV_SYNC);
  }

  // 【5. 资源释放与重置】
  private resetPlayer() {
    this.avPlayer?.reset(); // 回到 idle 状态,允许重新设置资源
  }

  aboutToDisappear() {
    // 退出页面时,必须调用 release 销毁实例
    this.avPlayer?.release();
    this.avPlayer = null;
  }

  build() {
    Column({ space: 20 }) {
      // 【3. 视频专属:XComponent 渲染画布】
      XComponent({
        id: 'videoSurface',
        type: XComponentType.SURFACE,
        controller: this.xComponentController
      })
        .width('100%')
        .height(250)
        .onLoad(() => {
          // 在 onLoad 回调中获取 surfaceId
          this.surfaceId = this.xComponentController.getXComponentSurfaceId();
          // 获取到 SurfaceId 后,开始设置资源
          this.setSource();
        })

      Text(`${this.currentTime.toFixed(1)}s / ${this.duration.toFixed(1)}s`)
        .fontSize(16)

      Row({ space: 20 }) {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => this.togglePlayPause())
        
        Button('快进 5s')
          .onClick(() => this.seekTo((this.currentTime + 5) * 1000))

        Button('重置')
          .onClick(() => this.resetPlayer())
      }
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

三、 进阶开发建议

  1. 后台播放与播控中心:如需实现熄屏播放或接入系统播控中心,需要接入 AVSession(媒体会话)并申请长时任务(LongTimeTask)。
  2. 音频焦点管理:播放音频时可能会被系统通话或其他应用打断。建议监听 audioInterrupt 事件,在焦点丢失时暂停播放,恢复时继续。
  3. 流媒体缓冲:播放 HLS/DASH 等网络流媒体时,可通过 on('bufferingUpdate') 监听缓冲进度,在 UI 上展示 Loading 状态。
  4. 首帧渲染:视频播放时,可通过 on('startRenderFrame') 监听首帧渲染完成事件,此时可移除视频封面图,实现平滑过渡。

1、 后台播放与播控中心(AVSession + 长时任务)

import { mediaSession } from '@kit.MediaKit';
import { backgroundTaskManager } from '@kit.BasicServicesKit';

// 1. 创建并激活媒体会话
private avSession: mediaSession.AVSession | null = null;

private initAVSession() {
  this.avSession = mediaSession.createAVSession(getContext(), 'MusicSession', 'audio');
  
  // 设置元数据(歌曲信息、封面等)
  this.avSession.setAVMetadata({
    title: '夜曲',
    artist: '周杰伦',
    album: '十一月的萧邦',
    // 封面图需转换为 PixelMap 传入
  });

  // 监听系统播控中心的指令(如锁屏界面的播放/暂停按钮)
  this.avSession.on('play', () => this.avPlayer?.play());
  this.avSession.on('pause', () => this.avPlayer?.pause());
}

// 2. 开启长时任务(防止后台被杀)
private async startBackgroundTask() {
  try {
    await backgroundTaskManager.startBackgroundRunning(getContext(), 'AudioPlayback', {
      backgroundModes: [backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK]
    });
  } catch (error) {
    console.error('Start background task failed:', error);
  }
}

// ⚠️ 在 AVPlayer 的 timeUpdate 回调中,实时同步进度给播控中心:
// this.avSession.setAVPlaybackState({ state: mediaSession.AVPlaybackState.PLAYBACK_STATE_PLAYING, position: timeMs });

2、 音频焦点管理(Audio Interrupt)

// 在 AVPlayer 初始化后,注册音频焦点打断监听
this.avPlayer.on('audioInterrupt', (event: media.AudioInterruptEvent) => {
  console.info(`Audio Interrupt Event: ${event.eventType}, Hint: ${event.hintType}`);

  switch (event.hintType) {
    case media.InterruptHint.INTERRUPT_HINT_PAUSE:
      // 临时失去焦点(如来电),系统建议暂停
      this.avPlayer?.pause();
      break;
    case media.InterruptHint.INTERRUPT_HINT_RESUME:
      // 焦点恢复,可继续播放
      this.avPlayer?.play();
      break;
    case media.InterruptHint.INTERRUPT_HINT_STOP:
      // 永久失去焦点(如打开了其他音乐App),系统建议停止
      this.avPlayer?.stop();
      break;
    case media.InterruptHint.INTERRUPT_HINT_DUCK:
      // 音量压低(如导航播报),可在此处手动降低音量
      this.avPlayer?.setVolume(0.2);
      break;
    case media.InterruptHint.INTERRUPT_HINT_UNDUCK:
      // 音量恢复
      this.avPlayer?.setVolume(1.0);
      break;
  }
});

3、 流媒体缓冲状态监听

// 声明一个状态变量用于控制 UI Loading 动画
@State isBuffering: boolean = false;

// 在初始化时注册缓冲进度监听
this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
  // value 为缓冲进度百分比(0-100)
  if (infoType === media.BufferingInfoType.BUFFERING_PERCENT) {
    if (value < 100) {
      this.isBuffering = true; // 显示 Loading 动画
    } else {
      this.isBuffering = false; // 隐藏 Loading 动画
    }
  }
});

// 在 UI 层根据状态展示 Loading
if (this.isBuffering) {
  LoadingProgress().width(50).height(50)
}

4、 首帧渲染与平滑过渡(视频专属)

// 声明状态变量控制封面图的显隐
@State showCover: boolean = true;

// 在初始化时注册首帧渲染监听
this.avPlayer.on('startRenderFrame', () => {
  console.info('Video first frame rendered!');
  // 触发 UI 更新,隐藏封面图
  this.showCover = false;
});

// UI 层:使用 Stack 将封面图与视频画布叠放
Stack() {
  // 视频渲染画布
  XComponent({ id: 'videoSurface', type: XComponentType.SURFACE, controller: this.xComponentController })
    .width('100%')
    .height('100%')

  // 视频封面图
  if (this.showCover) {
    Image($r('app.media.video_cover'))
      .width('100%')
      .height('100%')
      .objectFit(ImageFit.Cover)
      .transition(TransitionEffect.OPACITY.animation({ duration: 300 })) // 添加淡出动画
  }
}

5、 进阶播控与多端协同体验

  1. 精准进度跳转与倍速控制:在拖动进度条或切换倍速时,由于底层解码需要时间,操作并非瞬间完成。务必通过 on('seekDone') 和 on('speedDone') 事件监听操作结果,再更新 UI 上的时间标签或倍速按钮状态,避免界面与实际播放进度不同步。
  2. 音视频输出设备路由切换:当用户插入有线耳机、连接蓝牙耳机或断开设备时,音频输出通道会发生变化。应用可通过 on('audioOutputDeviceChangeWithInfo') 监听输出设备变更事件,从而动态调整 UI 提示(如“已切换至蓝牙耳机”),提升用户体验。
  3. 视频画面自适应(视频专属):在视频播放过程中,当切换不同分辨率的视频源时,视频宽高比可能会发生改变。通过监听 on('videoSizeChange') 事件,可以实时获取最新的视频宽高信息,进而动态调整 XComponent 窗口的大小或缩放模式(如等比例缩放、全屏拉伸),避免画面变形或留黑边。

6、 网络流媒体与性能优化

  1. HLS 自适应码率切换:针对 HLS 协议的网络流媒体,播放器支持动态调整清晰度。可通过 on('availableBitrates') 监听当前可用的码率列表,并在需要时调用 setBitrate() 切换清晰度,配合 on('bitrateDone') 确认切换成功,实现流畅的画质无缝切换。
  2. 缓冲状态可视化:在弱网环境下播放在线资源时,通过 on('bufferingUpdate') 实时获取缓冲百分比和缓存进度。结合该事件,可以在 UI 层动态展示 Loading 动画或缓冲进度条,缓解用户的等待焦虑。
  3. 首帧渲染无缝衔接(视频专属):为了提升视频起播的视觉体验,可以利用 on('startRenderFrame') 事件。当首帧视频画面真正渲染到屏幕上时触发该事件,此时再移除视频封面图,能够实现封面与视频画面的完美、平滑过渡,消除起播时的黑屏或闪烁感。

7、 异常兜底与生命周期管理

  1. 全局错误恢复机制:网络波动或服务异常是常态。除了基础的 on('error') 监听外,建议封装统一的错误处理逻辑。当捕获到错误时,根据错误码进行分类处理(如提示用户检查网络、重试播放等),并调用 reset() 将播放器重置为 idle 状态,防止在 error 状态下执行其他操作导致应用崩溃。
  2. 严格的内存回收策略AVPlayer 在 preparedplayingpaused 等工作状态下会占用大量系统运行内存。当用户离开播放页面、应用切入后台或暂时不需要播放时,务必及时调用 reset()(用于切换资源)或 release()(用于彻底销毁),避免内存泄漏导致应用被系统强杀。
  3. 后台防杀与播控中心集成:若需实现音乐或播客的后台/熄屏播放,单纯依赖 AVPlayer 是不够的。必须接入 AVSession(媒体会话)将播放信息同步给系统,并申请长时任务(LongTimeTask),防止应用在后台被系统强制挂起或中断。
Logo

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

更多推荐