【HarmonyOS- 媒体技术 -AVPlayer】手把手教你用 AVPlayer 实现流媒体播放 (ArkTS 详解)

  • 2025-11-03
    北京
  • 本文字数:12949 字

    阅读完需:约 42 分钟

 2025 年最新实战指南|从零搭建稳定、流畅的直播/点播播放器


一句话总结

在 HarmonyOS 中,使用 AVPlayer 播放流媒体,不是“能播就行”,而是要“稳、准、快、可控”。本文带你掌握从创建到释放的全链路操作,覆盖 HLS/DASH/FLV 等主流协议,支持码率切换、轨道选择、自动重试、缓冲监控等高阶能力。


一、前置准备:权限 & 环境配置

1. 添加网络权限(必须!)

在 module.json5 中添加:

{  "reqPermissions": [    {      "name""ohos.permission.INTERNET"    }  ]}
复制代码

⚠️ 否则访问任何网络资源都会失败!


2. 引入 MediaKit 模块

import { media } from '@kit.MediaKit';
复制代码

 

推荐使用 @kit.MediaKit,它是 HarmonyOS 官方提供的多媒体核心库。


二、标准播放流程(必看!)

顺序不能乱!否则可能收不到事件、无法播放

async avSetupStreamingMediaVideo() {    if (this.context == undefined) return;    // 创建avPlayer实例对象。    this.avPlayer = await media.createAVPlayer();    // 创建状态机变化回调函数。    await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {      this.percent = avPlayer.width / avPlayer.height;  // 计算并保存视频的宽高比      this.setVideoWH();  // 调用方法更新视频显示区域的宽高      this.durationTime = this.getDurationTime();  // 获取视频总时长      setInterval(() => { // 更新当前时间。        if (!this.isSwiping) {          this.currentTime = this.getCurrentTime();        }      }, SET_INTERVAL);    });    // 设置播放资源。    this.avPlayer.url = "http://media.iyuns.top:1000/http/720p_1m.mp4";   //开始播放    avPlay(): void {      if (this.avPlayer) {        try {          this.avPlayer.play();        } catch (e) {          console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);        }      }    }
复制代码


三、核心监听事件详解(缺一不可)


示例:监听播放器状态变化和监听播放时间

// 状态机变化回调函数。    this.avPlayer.on('stateChange'async (state, reason) => {      if (this.avPlayer == null) {        console.info(`${this.tag}: avPlayer has not init on state change`);        return;      }   // 时间上报监听函数。    this.avPlayer.on('timeUpdate'(time: number) => {      this.currentTime = time;    });
复制代码


四、主流协议支持一览表

✅ 所有协议均支持 setSource() 直接接入,无需额外封装。


五、高阶功能实战(让你的播放器“聪明”起来)

 1. 流媒体缓冲状态

当下载速率低于片源的码率时,会出现卡顿。此时,播放器检测到缓冲区数据不足,会先缓冲一些数据再播放,避免连续卡顿。一次卡顿对应的缓冲事件上报过程为:BUFFERING_START-> BUFFERING_PERCENT 0 -> ... -> BUFFERING_PERCENT 100 -> BUFFERING_END。CACHED_DURATION 在卡顿过程和播放过程中都会持续上报,直至下载至资源末尾。

import { media } from '@kit.MediaKit';// 类成员定义avPlayerprivate avPlayer: media.AVPlayer | null = null;// 创建avPlayer实例对象。this.avPlayer = await media.createAVPlayer();// 监听当前bufferingUpdate缓冲状态。this.avPlayer.on('bufferingUpdate'(infoType : media.BufferingInfoType, value : number) => {  console.info(`AVPlayer bufferingUpdate, infoType is ${infoType}, value is ${value}.`);})
复制代码

 

适用于直播、弱网环境下保障连续播放。


2. HLS 多码率切换(自定义清晰度)

当前流媒体 HLS 协议流支持多码率播放,默认情况下,播放器会根据网络下载速度选择合适的码率。

通过on('availableBitrates')监听当前 HLS 协议流可用的码率。如果监听的码率列表长度为 0,则不支持设置指定码率。

mport { media } from '@kit.MediaKit';// 类成员定义avPlayerprivate avPlayer: media.AVPlayer | null = null;// 创建avPlayer实例对象。this.avPlayer = await media.createAVPlayer();// 监听当前HLS协议流可用的码率。this.avPlayer.on('availableBitrates'(bitrates: Array<number>) => {  console.info('availableBitrates called, and availableBitrates length is: ' + bitrates.length);})
复制代码

通过setBitrate接口设置播放码率。若用户设置的码率不在可用码率中,播放器将选择最小且最接近的码率。该接口只能在 prepared/playing/paused/completed 状态下调用,可通过监听bitrateDone事件确认是否生效。

mport { media } from '@kit.MediaKit';// 类成员定义avPlayerprivate avPlayer: media.AVPlayer | null = null;// 创建avPlayer实例对象。this.avPlayer = await media.createAVPlayer();// 监听当前HLS协议流可用的码率。this.avPlayer.on('availableBitrates'(bitrates: Array<number>) => {  console.info('availableBitrates called, and availableBitrates length is: ' + bitrates.length);})
复制代码

 

可配合 UI 提供“清晰度选择”按钮。


3. DASH 起播策略设置(首帧更快加载)

为了保证在弱网环境下的播放体验,AVPlayer 将默认选择最低的视频分辨率开始播放,随后依据网络状况自动调整。开发者可以根据具体需求,自定义 DASH 视频的起播策略,包括设定视频的宽度、高度以及色彩格式等参数。

// 自定义起播分辨率:1920×1080import { media } from '@kit.MediaKit';let mediaSource : media.MediaSource = media.createMediaSourceWithUrl("http://test.cn/dash/aaa.mpd",  {"User-Agent" : "User-Agent-Value"});let playbackStrategy : media.PlaybackStrategy = {preferredWidth1920preferredHeight1080};this.avPlayer.setMediaSource(mediaSource, playbackStrategy);
复制代码

 弱网环境下优先加载低码率,提升首帧速度。


4. DASH 音视频轨道切换(手动选清晰度/语言)

DASH 流媒体资源包含多路不同分辨率、码率、采样率、编码格式的音频、视频及字幕资源。默认情况下,AVPlayer 会依据网络状况自动切换不同码率的视频轨道。开发者可根据需求选择指定的音视频轨道播放,此时自适应码率切换策略将失效。

 

 调用getTrackDescription获取所有音视频轨道列表。开发者可根据实际需求,基于MediaDescription各字段信息,确定目标轨道索引。

// 以获取1080p视频轨道索引为例。import { media } from '@kit.MediaKit';import { BusinessError } from '@kit.BasicServicesKit';public videoTrackIndex: number = 0;// 类成员定义avPlayerprivate avPlayer: media.AVPlayer | null = null;// 创建avPlayer实例对象。this.avPlayer = await media.createAVPlayer();this.avPlayer.getTrackDescription((error: BusinessError, arrList: Array<media.MediaDescription>) => {  if (arrList != null) {    for (let i = 0; i < arrList.length; i++) {      let propertyIndex: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_TRACK_INDEX];      let propertyType: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_TRACK_TYPE];      let propertyWidth: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_WIDTH];      let propertyHeight: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_HEIGHT];      if (propertyType == media.MediaType.MEDIA_TYPE_VID && propertyWidth == 1920 && propertyHeight == 1080) {        this.videoTrackIndex = parseInt(propertyIndex?.toString()); // 获取1080p视频轨道索引。      }    }  } else {    console.error(`getTrackDescription fail, error:${error}`);  }});
复制代码

在音视频播放过程中调用selectTrack选择对应的音视频轨道,或者调用deselectTrack取消选择的音视频轨道。

import { media } from '@kit.MediaKit';public videoTrackIndex: number = 0;    // 类成员定义avPlayerprivate avPlayer: media.AVPlayer | null = null;// 创建avPlayer实例对象。this.avPlayer = await media.createAVPlayer();// 切换至目标视频轨道。this.avPlayer.selectTrack(this.videoTrackIndex);// 取消选择目标视频轨道。// this.avPlayer.deselectTrack(this.videoTrackIndex);
复制代码

 

适合教育类、影视类 App,让用户自由选择画质/字幕/语音。


六、常见坑位 & 解决方案(避雷手册)


七、一个简单的开发实例

import { media } from '@kit.MediaKit';import { emitter } from '@kit.BasicServicesKit';import { display } from '@kit.ArkUI';const TIME_ONE = 60000// 1分钟的毫秒数。const TIME_TWO = 1000;  // 1秒的毫秒数。const SET_INTERVAL = 1000// 每秒更新一次当前播放时间。const SPEED_ZERO: number = 0// 对应1.00x。const SPEED_ONE: number = 1;  // 对应1.25x。const SPEED_TWO: number = 2;  // 对应1.75x。const SPEED_THREE: number = 3// 对应2.00x。const PROPORTION: number = 0.99;let innerEventFalse: emitter.InnerEvent = {  eventId: 1,  priority: emitter.EventPriority.HIGH};let innerEventTrue: emitter.InnerEvent = {  eventId: 2,  priority: emitter.EventPriority.HIGH};let innerEventWH: emitter.InnerEvent = {  eventId: 3,  priority: emitter.EventPriority.HIGH};@Entry@Componentstruct Index {  private avPlayer: media.AVPlayer | null = null;  private context: Context | undefined = undefined;  public videoTrackIndex: number = 0;  public bitrate: number = 0;  @State durationTime: number = 0;  @State currentTime: number = 0;  @State percent: number = 0;  @State isSwiping: boolean = false;  @State tag: string = 'StreamingMedia';  private surfaceId: string = '';  @State speedSelect: number = -1;  public intervalID: number = -1;  @State windowWidth: number = 300;  @State windowHeight: number = 300;  @State surfaceW: number | null = null;  @State surfaceH: number | null = null;  @State isPaused: boolean = true;  @State XComponentFlag: boolean = false;  getDurationTime(): number {    return this.durationTime;  }  getCurrentTime(): number {    return this.currentTime;  }  timeConvert(time: number): string {    let min: number = Math.floor(time / TIME_ONE);    let second: string = ((time % TIME_ONE) / TIME_TWO).toFixed(0);    // return `${min}:${(+second < TIME_THREE ? '0' : '') + second}`;    second = second.padStart(2'0');    return `${min}:${second}`;  }  async msleepAsync(ms: number): Promise<boolean> {    return new Promise((resolve, reject) => {      setTimeout(() => {        resolve(true)      }, ms)    })  }  async avSetupStreamingMediaVideo() {    if (this.context == undefined) return;    // 创建avPlayer实例对象。    this.avPlayer = await media.createAVPlayer();    // 创建状态机变化回调函数。    await this.setAVPlayerCallback((avPlayer: media.AVPlayer) => {      this.percent = avPlayer.width / avPlayer.height;      this.setVideoWH();      this.durationTime = this.getDurationTime();      setInterval(() => { // 更新当前时间。        if (!this.isSwiping) {          this.currentTime = this.getCurrentTime();        }      }, SET_INTERVAL);    });    // 情况一:HTTP视频播放。    this.avPlayer.url = "http://media.iyuns.top:1000/http/720p_1m.mp4";    // 情况二:HLS视频播放。    // this.avPlayer.url = "http://media.iyuns.top:1000/720-270-480.m3u8";    // 情况三:DASH视频播放。    // this.avPlayer.url = "http://media.iyuns.top:1000/dash/720p/720-1/720-1.mpd";    // 情况四:通过setMediaSource设置自定义头域及播放优选参数实现初始播放参数设置,以流媒体HTTP点播为例。    /*    let mediaSource : media.MediaSource = media.createMediaSourceWithUrl("http://media.iyuns.top:1000/http/720p_1m.mp4", {"":""});    // 设置播放策略,设置为缓冲区数据为20s。    let playbackStrategy : media.PlaybackStrategy = {preferredBufferDuration20};    // 为avPlayer设置媒体来源和播放策略。    this.avPlayer.setMediaSource(mediaSource, playbackStrategy);    * */    // 情况五:HLS切码率。    /*    this.avPlayer.url = "https://upftimae.dailyworkout.cn/videos/course/c800f81a209b5ee7891f1128ed301db/4/master.m3u8";    let bitrate: number = 0;    // 监听当前HLS协议流可用的码率。    this.avPlayer.on('availableBitrates'(bitrates: Array<number>) => {      console.info('availableBitrates called, and availableBitrates length is: ' + bitrates.length);      this.bitrate = bitrates[0]; // 保存需要切换的码率。    })    // 监听码率设置是否生效。    this.avPlayer.on('bitrateDone'(bitrate: number) => {      console.info('bitrateDone called, and bitrate value is: ' + bitrate);    })    * */    // 情况六:DASH切换音视频轨道。    /*    this.avPlayer.url = "http://poster-inland.hwcloudtest.cn/AiMaxEngine/ProductionEnvVideo/DASH_SDR_MultiAudio_MultiSubtitle_yinHeHuWeiDui3/DASH_SDR_MultiAudio_MultiSubtitle_yinHeHuWeiDui3.mpd";    //    this.avPlayer.getTrackDescription((error: BusinessError, arrList: Array<media.MediaDescription>) => {      if (arrList != null) {        for (let i = 0; i < arrList.length; i++) {          let propertyIndex: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_TRACK_INDEX];          let propertyType: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_TRACK_TYPE];          let propertyWidth: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_WIDTH];          let propertyHeight: Object = arrList[i][media.MediaDescriptionKey.MD_KEY_HEIGHT];          if (propertyType == media.MediaType.MEDIA_TYPE_VID && propertyWidth == 1920 && propertyHeight == 1080) {            this.videoTrackIndex = parseInt(propertyIndex.toString()); // 获取1080p视频轨道索引。          }        }      } else {        console.error(`getTrackDescription fail, error:${error}`);      }    });    * */  }  // HLS切换码率。  changeBitrate(bitrate: number) {    if (this.avPlayer == null) {      return;    }    // 设置播放码率。    try {      this.avPlayer.setBitrate(bitrate);    } catch (error) {      console.error(`${this.tag}: setBitrate failed, error message is = ${JSON.stringify(error.message)}`);    }  }  // DASH切换音视频轨道。  changeTrack(track: number) {    if (this.avPlayer == null) {      return;    }    // 切换至目标视频轨道。    try {      this.avPlayer.selectTrack(track);    } catch (error) {      console.error(`${this.tag}: selectTrack failed, error message is = ${JSON.stringify(error.message)}`);    }    // 取消选择目标视频轨道。    /*    try {      this.avPlayer.deselectTrack(track);    } catch (error) {      console.error(`${this.tag}: deselectTrack failed, error message is = ${JSON.stringify(error.message)}`);    }    * */  }  avPlay(): void {    if (this.avPlayer) {      try {        this.avPlayer.play();      } catch (e) {        console.error(`${this.tag}: avPlay = ${JSON.stringify(e)}`);      }    }  }  avPause(): void {    if (this.avPlayer) {      try {        this.avPlayer.pause();        console.info(`${this.tag}: avPause==`);      } catch (e) {        console.error(`${this.tag}: avPause== ${JSON.stringify(e)}`);      }    }  }  async avSeek(seekTime: numbermode: SliderChangeMode): Promise<void> {    if (this.avPlayer) {      try {        console.info(`${this.tag}: videoSeek  seekTime== ${seekTime}`);        this.avPlayer.seek(seekTime, 2);        this.currentTime = seekTime;      } catch (e) {        console.error(`${this.tag}: videoSeek== ${JSON.stringify(e)}`);      }    }  }  avSetSpeed(speed: number): void {    if (this.avPlayer) {      try {        this.avPlayer.setSpeed(speed);        console.info(`${this.tag}: avSetSpeed enum ${speed}`);      } catch (e) {        console.error(`${this.tag}: avSetSpeed == ${JSON.stringify(e)}`);      }    }  }  // 注册avplayer回调函数。  async setAVPlayerCallback(callback: (avPlayer: media.AVPlayer) => void, vType?: number): Promise<void> {    // seek操作结果回调函数。    if (this.avPlayer == null) {      console.error(`${this.tag}: avPlayer has not init!`);      return;    }    this.avPlayer.on('seekDone'(seekDoneTime) => {      console.info(`${this.tag}: setAVPlayerCallback AVPlayer seek succeeded, seek time is ${seekDoneTime}`);    });    this.avPlayer.on('speedDone'(speed) => {      console.info(`${this.tag}: setAVPlayerCallback AVPlayer speedDone, speed is ${speed}`);    });    // error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程。    this.avPlayer.on('error'(err) => {      console.error(`${this.tag}: setAVPlayerCallback Invoke avPlayer failed ${JSON.stringify(err)}`);      if (this.avPlayer == null) {        console.error(`${this.tag}: avPlayer has not init on error`);        return;      }      this.avPlayer.reset();    });    // 状态机变化回调函数。    this.avPlayer.on('stateChange'async (state, reason) => {      if (this.avPlayer == null) {        console.info(`${this.tag}: avPlayer has not init on state change`);        return;      }      switch (state) {        case 'idle'// 成功调用reset接口后触发该状态机上报。          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state idle called.`);          break;        case 'initialized'// avplayer 设置播放源后触发该状态上报。          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state initialized called.`);          if (this.surfaceId) {            this.avPlayer.surfaceId = this.surfaceId; // 设置显示画面,当播放的资源为纯音频时无需设置。            console.info(`${this.tag}: setAVPlayerCallback this.avPlayer.surfaceId = ${this.avPlayer.surfaceId}`);            this.avPlayer.prepare();          }          break;        case 'prepared'// prepare调用成功后上报该状态机。          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state prepared called.`);          this.avPlayer.on('bufferingUpdate'(infoType: media.BufferingInfoType, value: number) => {            console.info(`${this.tag}: bufferingUpdate called, infoType value: ${infoType}, value:${value}}`);          })          this.durationTime = this.avPlayer.duration;          this.currentTime = this.avPlayer.currentTime;          this.avPlayer.play(); // 调用播放接口开始播放。          console.info(`${this.tag}:            setAVPlayerCallback speedSelect: ${this.speedSelect}, duration: ${this.durationTime}`);          if (this.speedSelect != -1) {            switch (this.speedSelect) {              case SPEED_ZERO:                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_00_X);                break;              case SPEED_ONE:                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_25_X);                break;              case SPEED_TWO:                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_1_75_X);                break;              case SPEED_THREE:                this.avSetSpeed(media.PlaybackSpeed.SPEED_FORWARD_2_00_X);                break;            }          }          callback(this.avPlayer);          break;        case 'playing'// play成功调用后触发该状态机上报。          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state playing called.`);          if (this.intervalID != -1) {            clearInterval(this.intervalID)          }          this.intervalID = setInterval(() => { // 更新当前时间。            AppStorage.setOrCreate('durationTime'this.durationTime);            AppStorage.setOrCreate('currentTime'this.currentTime);          }, 100);          let eventDataTrue: emitter.EventData = {            data: {              'flag'true            }          };          let innerEventTrue: emitter.InnerEvent = {            eventId: 2,            priority: emitter.EventPriority.HIGH          };          emitter.emit(innerEventTrue, eventDataTrue);          break;        case 'completed'// 播放结束后触发该状态机上报。          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state completed called.`);          let eventDataFalse: emitter.EventData = {            data: {              'flag'false            }          };          let innerEvent: emitter.InnerEvent = {            eventId: 1,            priority: emitter.EventPriority.HIGH          };          emitter.emit(innerEvent, eventDataFalse);          if (this.intervalID != -1) {            clearInterval(this.intervalID)          }          this.avPlayer.off('bufferingUpdate')          AppStorage.setOrCreate('currentTime'this.durationTime);          break;        case 'released':          console.info(`${this.tag}: setAVPlayerCallback released called.`);          break        case 'stopped':          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state stopped called.`);          break        case 'error':          console.error(`${this.tag}: setAVPlayerCallback AVPlayer state error called.`);          break        case 'paused':          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state paused called.`);          break        default:          console.info(`${this.tag}: setAVPlayerCallback AVPlayer state unknown called.`);          break;      }    });    // 时间上报监听函数。    this.avPlayer.on('timeUpdate'(time: number) => {      this.currentTime = time;    });  }  aboutToAppear() {    this.windowWidth = display.getDefaultDisplaySync().width;    this.windowHeight = display.getDefaultDisplaySync().height;    if (this.percent >= 1) { // 横向视频。      this.surfaceW = Math.round(this.windowWidth * PROPORTION);      this.surfaceH = Math.round(this.surfaceW / this.percent);    } else { // 纵向视频。      this.surfaceH = Math.round(this.windowHeight * PROPORTION);      this.surfaceW = Math.round(this.surfaceH * this.percent);    }    this.isPaused = true;    this.context = this.getUIContext().getHostContext();  }  aboutToDisappear() {    if (this.avPlayer == null) {      console.info(`${this.tag}: avPlayer has not init aboutToDisappear`);      return;    }    this.avPlayer.release((err) => {      if (err == null) {        console.info(`${this.tag}: videoRelease release success`);      } else {        console.error(`${this.tag}: videoRelease release failed, error message is = ${JSON.stringify(err.message)}`);      }    });    emitter.off(innerEventFalse.eventId);  }  onPageHide() {    this.avPause();    this.isPaused = false;  }  onPageShow() {    emitter.on(innerEventTrue, (res: emitter.EventData) => {      if (res.data) {        this.isPaused = res.data.flag;        this.XComponentFlag = res.data.flag;      }    });    emitter.on(innerEventFalse, (res: emitter.EventData) => {      if (res.data) {        this.isPaused = res.data.flag;      }    });    emitter.on(innerEventWH, (res: emitter.EventData) => {      if (res.data) {        this.windowWidth = res.data.width;        this.windowHeight = res.data.height;        this.setVideoWH();      }    });  }  setVideoWH(): void {    if (this.percent >= 1) { // 横向视频。      this.surfaceW = Math.round(this.windowWidth * PROPORTION);      this.surfaceH = Math.round(this.surfaceW / this.percent);    } else { // 纵向视频。      this.surfaceH = Math.round(this.windowHeight * PROPORTION);      this.surfaceW = Math.round(this.surfaceH * this.percent);    }  }  @Builder  CoverXComponent() {    // ...  }  build() {    // ...  }}
复制代码


八、立即行动,开启你的音视频播放开发之旅!

 点击了解完整开发示例与 API 文档HarmonyOS AVPlayer 官方文档


加入 HarmonyOS 社区,共创未来!

我们诚邀广大开发者一起参与 HarmonyOS 技术生态建设,共建更开放、更智能的未来世界!

 加入开发者社区,获取最新资讯和技术支持HarmonyOS 官方社区


如果你觉得这篇指南有用,欢迎点赞、收藏、分享给更多开发者! 让 AVPlayer 成为你开发路上的得力助手,开启你的音视频播放新纪元!