鸿蒙学习实战之路-AVPlayer 视频播放完全指南

最近好多朋友问我:“西兰花啊,我想在鸿蒙应用里加个视频播放功能,用 Video 组件总觉得差点意思,想搞点高级的咋弄啊?” 害,这问题可问对人了!今天我就手把手带你用AVPlayer实现专业级的视频播放功能,从"选播放器"到"放电影",全程不踩坑~


🥦 先唠唠播放视频有哪两种方案

在鸿蒙里播放视频,就像选看电影的设备,有两种选择:

方案 特点 适用场景
AVPlayer 功能完善
支持流媒体和本地资源
可自定义播放控制
扩展性强
专业视频播放器、直播应用、需要高级功能的场景
Video 组件 简单易用
几行代码就能实现
基础功能完备
适合快速开发
简单视频播放、新闻应用、基础视频展示

今天咱们重点讲AVPlayer,就像教你用专业家庭影院看电影,而不是用手机看短视频~

🔍 AVPlayer 能做啥?

AVPlayer 就像专业的家庭影院系统,能帮你实现:

  • 播放本地视频文件(mp4、mkv 等)
  • 播放网络视频流(直播、在线视频)
  • 控制播放(播放/暂停/跳转/停止)
  • 调整音量、倍速、缩放模式
  • 监听播放状态、缓冲进度、视频尺寸变化
  • 处理音频焦点、设备切换等高级功能

播放的全流程就像在家看电影:

  1. 打开家庭影院(创建 AVPlayer 实例)
  2. 连好电视(设置播放窗口)
  3. 放光盘(设置播放资源)
  4. 调整音效(设置播放参数)
  5. 开始播放(播控操作)
  6. 看完关机(释放资源)

看看官方给的状态变化图,是不是超清晰?


🥦 西兰花警告

在开始之前,先说说开发建议和注意事项:

必看的开发建议

  • 要实现后台/熄屏播放,需要接入AVSession(媒体会话)申请长时任务,避免被系统打断
  • 要处理音频冲突,建议监听音频打断事件,别让用户看着看着突然没声音了
  • 要播放网络视频,必须申请ohos.permission.INTERNET权限(就像看网络电影要连 Wi-Fi)
  • 要监听设备变化,用on('audioOutputDeviceChangeWithInfo')监听音频输出设备变化

状态管理要注意

当播放处于prepared/playing/paused/completed状态时,播放引擎处于工作状态,会占用大量内存。不用的时候一定要调用reset()release()释放资源,就像看完电影要关机一样!


第一步:打开家庭影院(创建 AVPlayer 实例)

要播放视频得先有播放器,要实现视频播放得先创建 AVPlayer 实例:

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

// 创建AVPlayer实例(打开家庭影院)
let 家庭影院 = await media.createAVPlayer();

就这么简单!现在家庭影院处于idle状态,还没开始工作~


第二步:连好电视(设置播放窗口)

家庭影院打开了,得连好电视才能显示画面。AVPlayer 需要一个SurfaceID来显示视频,这就像电视的 HDMI 接口:

// 通过XComponent组件获取SurfaceID(找到电视的HDMI接口)
let 电视接口: string = "";

if (家庭影院 == null || 电视接口 === "") {
  return;
}

// 连接到电视(设置SurfaceID)
家庭影院.surfaceId = 电视接口;

🥦 西兰花小贴士

  • SurfaceID 需要从XComponent组件获取,参考getXComponentSurfaceId文档
  • 这一步是视频播放的关键,没有 SurfaceID 就像电视没接电源,看不到画面!

第三步:接好音响和电源(设置监听事件)

家庭影院和电视连好了,得接好音响和电源才能用。AVPlayer 的监听事件就是"音响"和"电源",帮咱们监听播放状态变化:

import { BusinessError } from "@kit.BasicServicesKit";
import { audio } from "@kit.AudioKit";

// 监听播放状态变化(电源指示灯)
家庭影院.on(
  "stateChange",
  async (state: string, reason: media.StateChangeReason) => {
    // 状态变化时的业务逻辑
    console.info(`播放状态变了:${state},原因:${reason}`);
  }
);

// 监听错误信息(故障提示灯)
家庭影院.on("error", (error: BusinessError) => {
  console.error(`播放出错了:${error.code}${error.message}`);
});

// 监听时长更新(电影总时长)
家庭影院.on("durationUpdate", (duration: number) => {
  console.info(`视频总时长:${duration}`);
});

// 监听当前播放时间(进度条)
家庭影院.on("timeUpdate", (time: number) => {
  console.info(`当前播放到:${time}`);
});

// 监听跳转完成(快进/快退完成)
家庭影院.on("seekDone", (seekDoneTime: number) => {
  console.info(`跳转完成,现在播放到:${seekDoneTime}`);
});

// 监听倍速设置完成(调速完成)
家庭影院.on("speedDone", (speed: number) => {
  console.info(`倍速设置完成:${speed}x`);
});

// 监听音量变化(音量调节完成)
家庭影院.on("volumeChange", (vol: number) => {
  console.info(`音量调节完成:${vol}`);
});

// 监听缓冲进度(网络播放缓冲)
家庭影院.on(
  "bufferingUpdate",
  (infoType: media.BufferingInfoType, value: number) => {
    console.info(`缓冲信息:${infoType},进度:${value}%`);
  }
);

// 监听首帧渲染(电影开始画面)
家庭影院.on("startRenderFrame", () => {
  console.info("视频首帧已渲染,可以移除封面了~");
});

// 监听视频尺寸变化(电影画面大小变化)
家庭影院.on("videoSizeChange", (width: number, height: number) => {
  console.info(`视频尺寸变化:${width}x${height}`);
});

// 监听音频焦点变化(防止被其他应用打断)
家庭影院.on("audioInterrupt", (info: audio.InterruptEvent) => {
  console.info(`音频焦点变化:${JSON.stringify(info)}`);
});

// 流媒体专用监听(HLS协议)
家庭影院.on("bitrateDone", (bitrate: number) => {
  console.info(`比特率设置完成:${bitrate}`);
});

家庭影院.on("availableBitrates", (bitrates: Array<number>) => {
  console.info(`可用比特率:${bitrates}`);
});

🥦 西兰花警告

  • stateChangeerror是必须监听的事件,就像家庭影院必须接电源一样!
  • 这些监听必须在idle状态下、设置资源前完成,否则可能收不到状态变化~

第四步:放光盘(设置播放资源)

家庭影院接好线了,现在该放光盘了!设置 AVPlayer 的url属性就能指定要播放的视频资源:

// 设置播放资源(放光盘)
let 电影地址 = "https://example.com/movie.mp4"; // 替换成你的视频地址

if (家庭影院 == null) {
  return;
}

家庭影院.url = 电影地址;

设置完资源后,家庭影院就进入了initialized状态,准备就绪!

🥦 关于资源路径的小知识

资源类型 注意事项
本地资源 必须用应用沙箱路径,参考获取应用文件路径
网络资源 必须申请ohos.permission.INTERNET权限
HAP 资源 ResourceManager.getRawFd打开文件描述符
格式要求 必须是 AVPlayer支持的播放格式

第五步:预热(准备播放)

光盘放好了,现在该预热家庭影院了!调用prepare()方法准备播放:

// 准备播放(预热家庭影院)
家庭影院.prepare((err: BusinessError) => {
  if (err) {
    console.error("准备失败:" + err.message);
  } else {
    console.info("准备成功!可以开始播放了~");
    // 准备成功后可以获取时长、调整音量、设置缩放模式
  }
});

准备成功后,家庭影院进入prepared状态,可以获取视频时长并调整播放参数了!


第六步:开始播放(播控操作)

预热完成,终于可以开始看电影了!咱们来实现播放、暂停、跳转、停止等操作:

import { BusinessError } from "@kit.BasicServicesKit";

// 播放视频
家庭影院.play().then(
  () => {
    console.info("电影开始播放!");
  },
  (err: BusinessError) => {
    console.error("播放失败:" + err.message);
  }
);

// 暂停播放
家庭影院.pause((err: BusinessError) => {
  if (err) {
    console.error("暂停失败:" + err.message);
  } else {
    console.info("暂停播放,去拿点零食~");
  }
});

// 跳转播放(快进/快退)
let 目标时间: number = 60; // 跳转到1分钟处
家庭影院.seek(目标时间, media.SeekMode.SEEK_PREV_SYNC);

// 停止播放
家庭影院.stop((err: BusinessError) => {
  if (err) {
    console.error("停止失败:" + err.message);
  } else {
    console.info("停止播放,电影看完了~");
  }
});

就像操作家里的家庭影院一样简单!


第七步:换光盘(更换资源)

想看另一部电影?可以调用reset()方法重置播放器,然后更换资源:

// 重置播放器(取出光盘)
家庭影院.reset((err: BusinessError) => {
  if (err) {
    console.error("重置失败:" + err.message);
  } else {
    console.info("重置成功!可以换光盘了~");
  }
});

// 更换资源(放新光盘)
let 新电影地址 = "https://example.com/new_movie.mp4";
if (家庭影院 == null) {
  return;
}
家庭影院.url = 新电影地址;

重置后,家庭影院回到idle状态,可以重新设置资源并播放~


第八步:关机(释放资源)

电影看完了,别忘了关机节约电量!调用release()方法释放 AVPlayer 资源:

// 释放资源(关机)
家庭影院.release((err: BusinessError) => {
  if (err) {
    console.error("释放资源失败:" + err.message);
  } else {
    console.info("资源释放成功!家庭影院已关机~");
  }
});

释放后,家庭影院进入released状态,彻底退出播放流程~


🥦 完整示例:实现一个专业视频播放器

光说不练假把式,咱们来整个完整的示例!

1. 准备资源

先新建工程,下载示例工程,把以下资源复制到对应目录:

AVPlayerArkTSVideo
├── entry/src/main/ets/
│   └── pages/
│       └── Index.ets (播放界面)
└── entry/src/main/resources/
    ├── base/
    │   ├── element/
    │   │   ├── color.json
    │   │   ├── float.json
    │   │   └── string.json
    │   └── media/
    │       ├── ic_video_play.svg  (播放键)
    │       └── ic_video_pause.svg (暂停键)
    └── rawfile/
        └── test1.mp4 (视频资源)

2. 实现播放界面

Index.ets中实现播放界面:

import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { audio } from '@kit.AudioKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct Index {
  private 家庭影院: media.AVPlayer | null = null;
  private 电视接口: string = '';
  private isPlaying: boolean = false;
  private currentTime: number = 0;
  private duration: number = 0;
  private progress: number = 0;

  // 页面加载时初始化
  aboutToAppear() {
    this.initPlayer();
  }

  // 初始化播放器
  async initPlayer() {
    try {
      // 创建播放器实例
      this.家庭影院 = await media.createAVPlayer();

      // 设置监听
      this.setPlayerListeners();

      // 设置本地资源
      let context = getContext(this) as common.UIAbilityContext;
      let resourceManager = context.resourceManager;
      let fd = await resourceManager.getRawFd('test1.mp4');

      if (this.家庭影院) {
        this.家庭影院.fdSrc = {
          fd: fd.fd,
          offset: 0,
          length: fd.length
        };
      }
    } catch (error) {
      console.error('初始化播放器失败:' + JSON.stringify(error));
    }
  }

  // 设置播放器监听
  setPlayerListeners() {
    if (!this.家庭影院) return;

    // 监听状态变化
    this.家庭影院.on('stateChange', async (state: string) => {
      console.info(`状态变化:${state}`);
      if (state === 'playing') {
        this.isPlaying = true;
      } else if (state === 'paused' || state === 'completed') {
        this.isPlaying = false;
      }
    });

    // 监听错误
    this.家庭影院.on('error', (error: BusinessError) => {
      console.error(`播放错误:${error.code}${error.message}`);
      promptAction.showToast({ message: '播放失败' });
    });

    // 监听时长更新
    this.家庭影院.on('durationUpdate', (duration: number) => {
      this.duration = duration;
    });

    // 监听当前时间
    this.家庭影院.on('timeUpdate', (time: number) => {
      this.currentTime = time;
      this.progress = this.duration > 0 ? (time / this.duration) * 100 : 0;
    });

    // 监听首帧渲染
    this.家庭影院.on('startRenderFrame', () => {
      console.info('视频首帧已渲染!');
    });

    // 监听视频尺寸变化
    this.家庭影院.on('videoSizeChange', (width: number, height: number) => {
      console.info(`视频尺寸:${width}x${height}`);
    });
  }

  // 准备播放器
  async preparePlayer() {
    if (!this.家庭影院) return;

    // 设置播放窗口
    if (this.电视接口 === '') {
      console.error('还没连电视呢!');
      return;
    }
    this.家庭影院.surfaceId = this.电视接口;

    return new Promise<void>((resolve, reject) => {
      this.家庭影院?.prepare((err: BusinessError) => {
        if (err) {
          console.error('准备失败:' + err.message);
          reject(err);
        } else {
          console.info('准备成功');
          resolve();
        }
      });
    });
  }

  // 获取SurfaceID
  getSurfaceId(e: XComponentSurfaceStatus) {
    if (e.surfaceId !== undefined) {
      this.电视接口 = e.surfaceId;
      // 获取到SurfaceID后准备播放
      this.preparePlayer();
    }
  }

  // 播放/暂停切换
  async togglePlay() {
    if (!this.家庭影院) return;

    try {
      if (this.isPlaying) {
        await this.家庭影院.pause();
      } else {
        await this.家庭影院.play();
      }
    } catch (error) {
      console.error('播放/暂停失败:' + JSON.stringify(error));
    }
  }

  // 页面卸载时释放资源
  aboutToDisappear() {
    if (this.家庭影院) {
      this.家庭影院.release();
      this.家庭影院 = null;
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('专业视频播放器')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin(20);

      // 视频播放窗口(XComponent)
      XComponent({
        id: 'videoPlayer',
        type: 'surface',
        controller: new XComponentController()
      })
        .width('80%')
        .height(300)
        .onSurfaceCreated(this.getSurfaceId.bind(this));

      // 进度条
      Slider({
        value: this.progress,
        min: 0,
        max: 100,
        style: SliderStyle.InSet
      })
        .width('80%')
        .onChange((value: number) => {
          // 拖动进度条跳转
          if (this.家庭影院 && this.duration > 0) {
            let seekTime = (value / 100) * this.duration;
            this.家庭影院.seek(seekTime, media.SeekMode.SEEK_PREV_SYNC);
          }
        });

      // 时间显示
      Row() {
        Text(this.formatTime(this.currentTime))
          .fontSize(14);
        Text('/')
          .fontSize(14);
        Text(this.formatTime(this.duration))
          .fontSize(14);
      }

      // 播放按钮
      Button({
        type: ButtonType.Circle,
        stateEffect: true
      }) {
        Image(this.isPlaying ? $r('app.media.ic_video_pause') : $r('app.media.ic_video_play'))
          .width(30)
          .height(30)
          .objectFit(ImageFit.Contain);
      }
      .width(80)
      .height(80)
      .backgroundColor('#1890ff')
      .onClick(() => {
        this.togglePlay();
      });
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center);
  }

  // 格式化时间
  formatTime(seconds: number): string {
    let min = Math.floor(seconds / 60);
    let sec = Math.floor(seconds % 60);
    return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
  }
}

3. 编译运行

编译工程并运行,就能看到一个专业的视频播放器了!点击播放按钮就能播放rawfile里的视频~


总结一下

今天咱们学会了:

  1. 两种播放方案对比:AVPlayer vs Video 组件
  2. AVPlayer 的核心功能:专业级视频播放能力
  3. 完整播放流程:从创建实例到释放资源
  4. 关键技术点:SurfaceID 获取、状态监听、资源设置
  5. 完整示例:实现了一个专业的视频播放器

是不是超简单?AVPlayer 的使用其实就像操作家里的家庭影院,跟着步骤来就能搞定~


📚 推荐资料

我是盐焗西兰花,
不教理论,只给你能跑的代码和避坑指南。
下期见!🥦

Logo

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

更多推荐