播放器卡一下,用户就卸载了

做应用,尤其是内容类应用,音视频功能是标配。但音视频开发有个特点:入门容易,精通难。

很多同学第一次写播放器,直接 AVPlayer.play() 一把梭,本地跑得飞起。结果一上生产环境,网络稍微波动一下,播放器就卡死了;用户切到后台,声音突然没了;播完一集,内存泄漏了……

音视频的坑,大部分不在 API 本身,而在状态机和生命周期。这篇就把 AVPlayer 和 AVRecorder 的状态流转、生命周期管理和常见坑讲清楚,代码都是能直接跑的。

https://placeholder-01.jpg/

一、AVPlayer 状态机:别在错误的时候做错误的事

AVPlayer 是一个典型的状态机。它的状态包括:idle、initialized、prepared、playing、paused、completed、stopped、released。

你只能在正确的状态调用正确的方法。 比如,在 idle 状态下调 play(),直接抛异常崩掉;在 prepared 之前调 play(),同样崩溃。

初始化和播放的代码如下:

typescript

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

let avPlayer: media.AVPlayer;

async function initPlayer(url: string) {
  avPlayer = await media.createAVPlayer();
  
  // 1. 注册状态回调
  avPlayer.on('stateChange', (state: string) => {
    console.info('当前状态: ' + state);
    switch (state) {
      case 'initialized':
        // 2. 准备资源
        avPlayer.prepare();
        break;
      case 'prepared':
        // 3. 准备好了,开始播放
        avPlayer.play();
        break;
      case 'playing':
        // 更新 UI
        break;
      case 'error':
        console.error('播放出错');
        break;
    }
  });

  // 4. 设置资源
  avPlayer.url = url;
}

看明白了吗?prepare() 必须在 stateChange 回调里 initialized 之后调用,play() 必须在 prepared 之后调用。 如果你直接 avPlayer.url = url; avPlayer.prepare(); avPlayer.play();,100% 会出错,因为此时状态还没流转到 initialized。

坑来了:网络 URL 要设 url,本地资源要用 fdSrc。

如果你加载的是 rawfile 里的视频,不能直接设 url,要先用 resourceManager 打开文件,获取文件描述符(fd),再赋值给 fdSrc:

typescript

const context = getContext(this);
const fileDescriptor = await context.resourceManager.getRawFd('video.mp4');
avPlayer.fdSrc = { fd: fileDescriptor.fd, offset: fileDescriptor.offset, length: fileDescriptor.length };

二、后台播放与焦点抢占

用户切到后台,你希望音乐继续播放。但这需要申请长时任务,否则系统直接把你的播放器杀了。

在 module.json5 里声明:

json

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "backgroundModes": ["audioPlayback"]
      }
    ]
  }
}

然后在代码里申请长时任务:

typescript

import backgroundTaskManager from '@ohos.resourceschedule.backgroundTaskManager';

let backgroundTaskId: number;

function startBackgroundPlay() {
  const context = getContext(this);
  backgroundTaskManager.startBackgroundTask(context, backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK)
    .then((id) => {
      backgroundTaskId = id;
    });
}

function stopBackgroundPlay() {
  if (backgroundTaskId) {
    backgroundTaskManager.stopBackgroundTask(backgroundTaskId);
  }
}

另一个坑:音频焦点被抢占。

用户正在听你的应用播放音乐,突然来了个电话,或者打开了抖音。如果你不做处理,两个应用的声音会同时播放,用户直接疯掉。

HarmonyOS 提供了音频焦点管理:

typescript

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

const audioManager = audio.getAudioManager();
const interruptMode = audio.InterruptMode.SHARE_MODE;

audioManager.on('audioInterrupt', (event) => {
  if (event.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
    // 系统强制打断,比如来电
    avPlayer.pause();
  } else {
    // 建议打断,比如其他应用申请焦点
    avPlayer.pause();
  }
});

收到打断回调后,必须立即 pause(),否则系统可能会直接杀掉你的进程。

https://placeholder-02.png/

三、AVRecorder 录制:权限与格式的坑

录制比播放更麻烦,因为涉及到麦克风权限和编码格式。

首先,权限:

json

{
  "requestPermissions": [
    { "name": "ohos.permission.MICROPHONE" },
    { "name": "ohos.permission.WRITE_MEDIA" }
  ]
}

这两个都是 user_grant 级别,必须动态申请。千万别在应用启动时就弹麦克风权限,用户会觉得很恐怖。 必须等用户点击“开始录音”按钮时再申请。

录制参数配置:

typescript

const avRecorderConfig: media.AVRecorderConfig = {
  audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
  profile: {
    audioBitrate: 96000,
    audioChannels: 2,
    audioCodec: media.CodecMimeType.AUDIO_AAC,
    audioSampleRate: 48000,
    fileFormat: media.ContainerFormatType.CFT_MPEG_4A
  },
  url: 'fd://' + file.fd
};

avRecorder = await media.createAVRecorder();
await avRecorder.prepare(avRecorderConfig);
await avRecorder.start();

坑来了:录制格式选错,文件打不开。

如果你选了 CFT_MPEG_4A,文件后缀必须是 .m4a。选了 CFT_MPEG_4,后缀是 .mp4。如果你随便写个 .mp3,生成的音频文件在某些播放器上无法播放。格式要和容器严格对应。

另外,录制完成后必须调用 release():

typescript

await avRecorder.stop();
await avRecorder.release();
avRecorder = null;

不调 release,麦克风会被一直占用,其他应用没法录音,用户只能重启手机。

四、音视频常见坑合集

坑一:release() 没调用,内存泄漏。

AVPlayer 和 AVRecorder 都是占用底层硬解码资源的。每次使用完,必须在页面 aboutToDisappear 里调用 release(),并置空对象。否则播放几个视频后,应用直接闪退。

坑二:网络波动导致播放卡死。

AVPlayer 有缓冲机制,但网络极差时会一直 buffering。你需要监听 on('bufferingUpdate'),如果卡顿超过 5 秒,提示用户网络不佳,或者自动降级清晰度。

坑三:视频渲染尺寸不对。

如果你用 XComponent 渲染视频,需要监听 on('videoSizeChange'),获取视频的真实宽高,然后调整 XComponent 的布局,否则视频会被拉伸变形。

https://placeholder-03.png/

五、总结

音视频开发的核心是尊重状态机,管好生命周期。记住三点:

  1. 严格遵循 AVPlayer 的状态流转,别跳状态调用方法。

  2. 后台播放申请长时任务,处理音频焦点抢占。

  3. 用完必须 release(),否则内存泄漏。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐