大家好,我是[晚风依旧似温柔],新人一枚,欢迎大家关注~

前言

——鸿蒙多媒体(音视频播放)你真该系统搞一次

说句心里话,现在的用户对“播放体验”几乎是零容忍:

  • 视频一开头就转圈圈?关。
  • 切小窗一卡一卡?卸。
  • 音频后台一会儿就断?差评。

而对我们这种写代码的来说,鸿蒙多媒体这块表面看就两件事:音频播放、视频播放;真开搞的时候才发现:

  • API 一堆:media, avSession, VideoPlayer 组件、流式播放、事件监听……
  • 既要管“能不能播”,还要管“播得顺不顺、清不清、切不切全屏、小窗”。

这篇就按你给的大纲来,一次性把 鸿蒙媒体能力开发:音频 & 视频播放 的主线走一遍,尽量写成那种:

你下次再写播放器页面,脑子里已经有一整套结构,而不是边写边百度。

一、多媒体 API 全景:先认清“有什么”,再决定“用哪个”

鸿蒙多媒体相关的能力,大致可以分成几块(实际模块名字随版本略有调整,但角色很固定):

  • 媒体播放内核

    • 音频播放器:media.createAudioPlayer() / AudioPlayer
    • 视频播放器:media.createAVPlayer() / AVPlayer
  • UI 组件层

    • VideoPlayer ArkUI 组件:帮你把 UI + 播放整合在一起
  • 会话控制层(可选)

    • avSession:控制通知栏播放、耳机控制、系统媒体控制(稍复杂,这里不过多展开)
  • 解码 / 渲染相关:由系统内核处理,我们更多是调参数、喂 URL 或文件

可以简单理解成两条路:

  1. 纯逻辑播放器(AudioPlayer/AVPlayer)

    • 你自己处理 UI、进度条、按钮等
  2. VideoPlayer UI 组件 + 播放器封装

    • 视频播放场景用这个更轻松

这篇我们重点放在:

  • 音频:用 Audio 播放器 + 自定义 UI
  • 视频:靠 VideoPlayer + 必要的控制逻辑

二、VideoPlayer 组件:快速做出一个像样的视频界面

说实话,如果你只是想做个普通的视频播放页,不搞花式弹幕、滤镜、花里胡哨的进度条,那么用 ArkUI 自带的 VideoPlayer 几乎是性价比最高的选择。

2.1 最小可运行示例:本地或网络视频一键播放

import media from '@ohos.multimedia.media';

@Entry
@Component
struct SimpleVideoPage {
  // 媒体 URL:可以是本地 file uri,也可以是 http(s) 流
  @State videoSrc: string = 'https://example.com/demo.mp4';

  build() {
    Column() {
      VideoPlayer({
        src: this.videoSrc,
        controls: true  // 显示系统默认的控制条
      })
      .width('100%')
      .height(240)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

这就已经是一个“能用”的播放器了:

  • 自带播放/暂停按钮
  • 能拖动进度
  • 支持全屏切换(部分版本自带,全屏逻辑也可以自己包)

但只停在这个层级就有点太 Demo 了,我们肯定要更细粒度控制——比如事件监听、播放状态管理、自定义控制条等。

2.2 控制与事件:别只会 .src,得会“听它说话”

真实业务里,通常你需要:

  • 知道当前播放状态(播放中 / 暂停 / 结束 / 出错)
  • 拿到当前进度 / 总时长
  • 响应用户操作(点击自定义按钮触发播放、暂停、跳转等)

通常做法是结合 控制器 + 事件回调,比如:

import media from '@ohos.multimedia.media';
import { VideoPlayerController } from '@ohos.arkui.ui'; // 示例,具体视版本而定

@Entry
@Component
struct VideoWithEvents {
  private controller: VideoPlayerController = new VideoPlayerController();
  @State src: string = 'https://example.com/demo.mp4';
  @State duration: number = 0;
  @State position: number = 0;
  @State isPlaying: boolean = false;

  aboutToAppear() {
    // 可以在这里绑定 controller 的事件(部分版本支持)
  }

  build() {
    Column() {
      VideoPlayer({
        src: this.src,
        controller: this.controller
      })
      .width('100%')
      .height(220)
      .onPrepared((info) => {
        // 媒体准备好,可以获得 duration
        this.duration = info?.duration ?? 0;
      })
      .onPlay(() => {
        this.isPlaying = true;
      })
      .onPause(() => {
        this.isPlaying = false;
      })
      .onFinish(() => {
        this.isPlaying = false;
        this.position = this.duration;
      })
      .onError((err) => {
        console.error('video error: ', JSON.stringify(err));
      })

      // 自定义控制条
      Row() {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => {
            if (this.isPlaying) {
              this.controller.pause();
            } else {
              this.controller.play();
            }
          })

        Slider({
          value: this.position,
          min: 0,
          max: this.duration
        })
        .onChange((value) => {
          this.position = value;
        })
        .onChangeEnd((value) => {
          this.controller.seekTo(value);
        })
        .margin({ left: 16 })
      }
      .margin({ top: 12 })
    }
  }
}

核心思路:

  • 播放状态 → @StateisPlaying, position, duration
  • 播放行为 → controllerplay/pause/seekTo
  • UI 按钮 & 进度条只是“操作者”和“显示器”

三、流媒体播放:点播、直播、M3U8,这些咋搞?

音视频领域,“流媒体”是绕不开的关键词:

  • 点播:比如 https://xxx.com/video/haha.m3u8
  • 直播:RTMP / HLS / DASH 等协议,前端通常是 HLS 地址

鸿蒙播放层对“URL 是什么协议”这一块,基本做的是 透明支持:你只要给出系统支持的 URL,就按普通视频流去播。

3.1 流媒体与本地媒体,对播放器来说区别不大

VideoPlayer/AVPlayer 来说,只要底层 codec & protocol 支持,它就可以:

@State src: string = 'https://example.com/live/stream.m3u8';

VideoPlayer({ src: this.src, ... })

但业务上,你得考虑:

  • 流媒体可能没有固定时长(直播)
  • 网络抖动会引起缓冲 / 卡顿,需要给出友好提示
  • 可能需要手动做“重试 & 重新连接”

3.2 流媒体播放状态:重点关注缓冲 & 错误

对于直播 / HLS 这类,建议重点关注:

  • onBufferedUpdate:缓冲进度(不同版本名字不同,大致含义一致)
  • onError:网络中断、流断开及时重试
  • onInfo:有些播放器会抛出缓冲开始 / 结束信息

伪代码示意:

VideoPlayer({ src: this.src, controller: this.controller })
  .onInfo((info) => {
    if (info.type === 'BUFFERING_START') {
      this.isBuffering = true;
    } else if (info.type === 'BUFFERING_END') {
      this.isBuffering = false;
    }
  })
  .onError((err) => {
    this.isBuffering = false;
    this.errorMsg = '网络异常,正在尝试重连...';
    this.retryPlay();
  })

我们不一定能拿到所有底层细节,但应该在 UI 上至少做到:

  • 卡顿时有 Loading 提示
  • 失败时有重试按钮 & 自动退避重试机制

四、播放控制与事件监听:播放器不是“黑盒”

无论是组件式的 VideoPlayer 还是逻辑层的 AudioPlayer/AVPlayer,你都需要用一套统一的“状态模型”包起来,否则业务逻辑会很散。

4.1 播放状态模型(建议你自己定义一个)

哪怕官方没帮你定义完整枚举,你也可以 在业务层做一份“抽象状态”,比如:

enum PlayerState {
  IDLE = 'idle',         // 未设置源
  PREPARING = 'preparing',
  PLAYING = 'playing',
  PAUSED = 'paused',
  COMPLETED = 'completed',
  ERROR = 'error'
}

然后封装一个简单的播放器管理类:

class VideoPlayerModel {
  state: PlayerState = PlayerState.IDLE;
  duration: number = 0;
  position: number = 0;
  error: string = '';

  attachController(ctrl: VideoPlayerController) {
    ctrl.onPrepared = (info) => {
      this.duration = info?.duration ?? 0;
      this.state = PlayerState.PAUSED; // 准备好但未播放
    };
    ctrl.onPlay = () => this.state = PlayerState.PLAYING;
    ctrl.onPause = () => this.state = PlayerState.PAUSED;
    ctrl.onFinish = () => this.state = PlayerState.COMPLETED;
    ctrl.onError = (err) => {
      this.state = PlayerState.ERROR;
      this.error = JSON.stringify(err);
    };
  }
}

UI 层就只关心 model.statemodel.durationmodel.position,减少直接到处 controller.onXXX 绑事件的混乱感。

4.2 音频播放:列表播放器 + 后台播放的基本套路

音频场景一般会有几个要素:

  • 播放列表(本地 / 在线)
  • 当前播放索引
  • 播放模式(单曲循环 / 列表循环 / 随机)
  • 后台播放 & 通知栏控制(涉及 avSession,这里先不展开太细)

纯音频播放可以用 media.createAudioPlayer()(具体 API 略做简化示意):

import media from '@ohos.multimedia.media';

class AudioPlayerManager {
  private player?: media.AVPlayer;
  private list: Track[] = [];
  private index: number = 0;

  async setPlaylist(list: Track[]) {
    this.list = list;
    this.index = 0;
    await this.prepareCurrent();
  }

  private async prepareCurrent() {
    const track = this.list[this.index];
    if (!this.player) {
      this.player = await media.createAVPlayer(); // 或 createAudioPlayer,视 API 而定
      this.bindEvents();
    }

    this.player.reset();
    this.player.url = track.url;
    await this.player.prepare();
  }

  play() {
    this.player?.play();
  }

  pause() {
    this.player?.pause();
  }

  next() {
    if (this.index < this.list.length - 1) {
      this.index++;
      this.prepareCurrent().then(() => this.play());
    }
  }

  prev() {
    if (this.index > 0) {
      this.index--;
      this.prepareCurrent().then(() => this.play());
    }
  }

  private bindEvents() {
    this.player.on('finish', () => {
      // 自动播下一首
      this.next();
    });
  }
}

UI 层可以用一个全局 AudioPlayerManager + ArkUI 状态绑定,把状态映射到播放条上。

五、全屏 / 小窗播放:别把全屏逻辑散落在一堆 if 里

全屏 / 小窗逻辑,本质是两个问题:

  1. VideoPlayer 在哪里渲染(哪个布局、占多大区域)
  2. 系统状态怎么配合(隐藏导航栏、横屏、沉浸等)

5.1 简单全屏方案:同一页面内布局切换

最简单的实现,全屏其实就是 “同一个页面里占满”,比如:

@Entry
@Component
struct VideoFullscreenDemo {
  @State isFull: boolean = false;

  build() {
    Stack() {
      // 正常布局
      if (!this.isFull) {
        Column() {
          Text('标题区域').fontSize(18).margin(12)
          VideoPlayer({ src: 'https://example.com/demo.mp4' })
            .width('100%')
            .height(220)
          // 其他内容...
        }
      } else {
        // 全屏模式:视频占满
        VideoPlayer({ src: 'https://example.com/demo.mp4' })
          .width('100%')
          .height('100%')
      }

      // 浮动一个退出全屏按钮
      if (this.isFull) {
        Row() {
          Button('退出全屏')
            .onClick(() => this.isFull = false)
        }
        .position({ x: 16, y: 16 })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#000000')
  }
}

实际项目里你会把 VideoPlayer 做成组件,并且让“全屏状态”从外部控制。

更进阶一点,可以考虑:

  • 全屏时锁定横屏
  • 调整系统 UI(状态栏透明 / 隐藏)

5.2 小窗播放(列表页)模式:一屏多个视频

常见:

  • 短视频瀑布流,每个 Item 上都有一个视频预览
  • 只有当前可见的某个 Item 在播放,滚出屏幕后暂停 / 切下一个

这种场景下,做好两件事:

  1. 不同时创建 N 个播放器

    • 共享一个 AVPlayer + 不同 Surface
    • 或限制同时播放的数量(比如仅一个)
  2. 跟踪“当前可见的视频项”

    • 滚动时计算当前处于中心的 Item
    • 切换播放源

这块因为涉及列表、滚动事件、可见性判断,属于另一个话题,可以单开一篇“短视频列表架构”,这次先不展开太深,只提醒一句:

如果你每个 Item 都 new 一个 Player,那不用分析,你的性能一定会很惨。

六、播放缓存策略:“秒开”和“别把用户流量吃死”之间的平衡

播放缓存分两层:

  1. 播放器内部缓冲:播放前后几秒的数据(你配置不多,更多走系统策略)
  2. 业务层离线缓存:提前下载 / 缓存到本地,下次直接本地播

6.1 播放器缓冲:更多是“观察 & 兜底”

你能直接控制的通常有限:

  • 缓冲阈值(有些 API 提供设置)
  • 是否允许边下边播(流媒体默认如此)

更实用的是:

  • 监听缓冲状态,UI 上给明确提示(“当前网络较差,正在缓冲…”)
  • 避免频繁 seek(尤其是直播流),减少卡顿

6.2 业务层缓存:短视频 / 音频列表大杀器

很多应用会做:

  • 向下滑时,预加载接下来 2~3 条视频
  • 经常播放的音频提前缓存一份

基本思路:

  1. 维护一个“缓存管理器”:

    • 负责视频 / 音频文件下载
    • 管理缓存目录 & 大小限制
  2. 播放前先查缓存:

    • 有缓存:直接播本地文件路径
    • 无缓存:用在线 URL 播 & 后台下载

伪代码:

class MediaCacheManager {
  async getPlayableUrl(url: string): Promise<string> {
    const cachedPath = await this.findInCache(url);
    if (cachedPath) return cachedPath;

    this.prefetch(url); // 后台下载,不阻塞首次播放
    return url;         // 先在线播放
  }

  async prefetch(url: string) {
    // 下载并写入缓存目录
  }

  async cleanUpIfNeeded() {
    // 控制缓存总大小
  }
}

播放侧:

const playUrl = await MediaCacheManager.getPlayableUrl(originUrl);
this.src = playUrl;

这样在网络环境好的时候,用户很快就会享受到“第二次播放明显快”的体验。

小结:把音视频当“系统能力 + 业务模型”的组合来做

写到这里,你应该大概有这么一条主线了:

  1. API 层

    • 音频:AudioPlayer/AVPlayer
    • 视频:VideoPlayer 组件 + 控制器
    • 流式播放:跟“播 URL”没本质区别,主要差在容错和 UI 提示
  2. 业务层模型

    • 定义自己的 PlayerState
    • 封装 VideoPlayerModel / AudioPlayerManager
    • 所有 UI 只是去读这个模型的“状态 + 控制方法”
  3. 体验层

    • 全屏 / 小窗:是布局切换问题,而不是“另一个页面”问题
    • 缓存策略:播放器内部缓冲 + 业务层离线缓存
    • 状态提示:缓冲、出错、结束都要有明确反馈

真正稳定好用的媒体体验,从来不是一两个 API 调用出来的,而是你在这几层之间组织得有多清晰——谁负责“能不能播”,谁负责“播得顺不顺”,谁负责“用户看见什么”。

如果觉得有帮助,别忘了点个赞+关注支持一下~
喜欢记得关注,别让好内容被埋没~

Logo

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

更多推荐