你以为多媒体就是“能播就行”?那为啥一加相机/录音/转码就开始闪退和卡顿呢?

🏆本文收录于「滚雪球学SpringBoot」专栏(全网一个名),手把手带你零基础入门Spring Boot,从入门到就业,助你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8
🤔 开篇
你有没有发现一个“残酷但真实”的现象:多媒体做得好的 App,用户会说“哇好顺”;多媒体做得糟的 App,用户会说“哎你这也太卡/太吵/太闪退了吧”😤——而你,往往就夹在中间:一边写业务一边当“音视频修理工”🧰。
来,今天咱们把 鸿蒙第 18 章:媒体与多媒体开发狠狠干透:播放(AVPlayer)🎬 / 相机&相册📸 / 录音🎙️ / 视频处理(转码)🧪。我会尽量用“像同事手把手带你”的口吻讲,顺便把那些最爱搞心态的坑也提前钉死🪤。
🧭📌 目录(先把战场画出来🗺️)
- 🎞️ 18.1 媒体播放(AVPlayer):从“能播”到“播得稳、播得省”
- 📸 18.2 相机调用(Camera Kit):预览 / 拍照流程与释放顺序
- 🖼️ 18.3 相册调用(PhotoViewPicker):不乱要权限,也能安全选图
- 🎙️ 18.4 音频录制(AVRecorder):状态机别违章,后台录制别嘴硬
- 🧪 18.5 视频处理(AVTranscoder):压缩/转码的正确打开方式
- 🧯 18.6 常见翻车现场清单:我替你踩过了😭
- ❓ 小确认:你更想做“系统能力调用”还是“自定义相机/播放器 UI”?😄
🎞️ 18.1 媒体播放(AVPlayer):从“能播”到“播得稳、播得省” 😎🎬
先给你一个“我亲测好用”的结论:
AVPlayer 别写成一次性脚本,要写成“生命周期友好型”播放器。因为官方就明说了:播放在 prepared/playing/paused/completed 等状态时引擎在工作,会占用较多内存;不用时要 reset() 或 release() 回收资源,不然你就等着被内存/卡顿教育吧🥲。
✅ 你真正需要的播放流程(别少步骤)
官方的端到端流程很清晰:创建 AVPlayer → 设置资源 & 窗口(SurfaceID)→ prepare → play/pause/seek/stop → reset(可换源)→ release(销毁)。同时建议监听 stateChange/error/timeUpdate 等事件,避免在错误状态硬操作导致异常行为。
🧩 示例:用 XComponent 承载画面 + AVPlayer 播放(骨架版,够你落地)
说明:
surfaceId需要从 XComponent 拿(你项目里获取方式可能封装不同),核心思路是:先有 surfaceId,再把它塞给播放器。官方也明确:显示画面需要设置 SurfaceID,通常来自 XComponent。
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct PlayerDemo {
@State playing: boolean = false
private avPlayer?: media.AVPlayer
private surfaceId: string = '' // 这里假设你已经从XComponent拿到了
async aboutToAppear() {
this.avPlayer = await media.createAVPlayer();
this.bindEvents(this.avPlayer);
// 1) 设置资源(本地/网络都行;网络需要申请 INTERNET 权限):contentReference[oaicite:3]{index=3}
this.avPlayer.url = 'https://example.com/demo.mp4';
// 2) 设置窗口(画面渲染 surface)
this.avPlayer.surfaceId = this.surfaceId;
// 3) 准备
await this.avPlayer.prepare();
}
private bindEvents(p: media.AVPlayer) {
p.on('stateChange', (state) => {
console.info(`🎬 stateChange: ${JSON.stringify(state)}`);
});
p.on('error', (err: BusinessError) => {
console.error(`💥 player error: ${err.code}, ${err.message}`);
});
p.on('timeUpdate', (t: number) => {
// 进度条可用
});
p.on('startRenderFrame', () => {
// 首帧回调:常用来移除封面,衔接更丝滑:contentReference[oaicite:4]{index=4}
});
}
async togglePlay() {
if (!this.avPlayer) return;
if (this.playing) {
await this.avPlayer.pause();
} else {
await this.avPlayer.play();
}
this.playing = !this.playing;
}
async aboutToDisappear() {
// 不用的时候别嘴硬,赶紧释放,省内存!:contentReference[oaicite:5]{index=5}
await this.avPlayer?.release();
this.avPlayer = undefined;
}
build() {
Column({ space: 12 }) {
// XComponent 用来显示画面(你工程里可能是自定义封装)
// XComponent({ id: 'playerSurface', type: 'surface', libraryname: '' })
// .onLoad((id) => { this.surfaceId = getSurfaceIdSomehow(id) })
Button(this.playing ? '⏸️ 暂停' : '▶️ 播放')
.onClick(async () => await this.togglePlay())
}.padding(16)
}
}
🧠 播放器“写得像人”小技巧(真的能少掉一堆 bug😤)
- 状态机敬畏症:别在错误状态、未 prepare 状态乱
seek/play,官方也提醒这会导致异常或未定义行为 - 长时任务/媒体会话:要做后台/熄屏播放,官方建议接入 AVSession 并申请长时任务,否则系统可能中断播放
- 封面切换:用
startRenderFrame做“首帧到来才撤封面”的衔接,会比你瞎猜延时优雅太多
📸 18.2 相机/拍照(Camera Kit):预览→拍照→释放,顺序错了就等着哭 😭📷
相机这块我先把“门槛”说清楚:
拍摄前要申请 CAMERA;录像还要 MICROPHONE;要读写媒体文件优先用 Picker / 安全控件保存,别上来就申请受限权限——官方在相机权限指导里讲得非常直白。([华为开发者][3])
✅ 最小权限心法(别把自己送去上架审核挨揍🥲)
- 拍照:
ohos.permission.CAMERA - 录像含音:再加
ohos.permission.MICROPHONE - 读媒体:优先 媒体库 Picker
- 存媒体:优先 安全控件保存媒体资源
👀 预览:XComponent 提供 surfaceId,然后 createPreviewOutput
官方预览指导提到:通过 createPreviewOutput 创建预览输出流,其中参数之一就是你从 XComponent 获取到的 surfaceId;并且预览/录像分辨率宽高比要一致(别把画面拉成“胖头鱼”😅)。
📷 拍照:完整流程要“开-拍-停-释放”,别拍完就把会话砍了
官方拍照实现方案特别强调:拍照结束后再 stop/close/release,避免拍照未结束就释放会话。并给出了典型释放顺序:stop session → close input → release preview/photo output → release session。
🧩 示例:拍照流程骨架(把调用顺序写对就赢一半)
import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';
async function takePhoto(surfaceId: string) {
// 1) 获取 CameraManager,拿能力(省略:选择 cameraId、创建 cameraInput)
const cameraManager = camera.getCameraManager(getContext(this) as any);
const cameraIds = cameraManager.getSupportedCameras();
const cameraId = cameraIds[0];
const cameraInput = cameraManager.createCameraInput(cameraId);
await cameraInput.open();
const capability = cameraManager.getSupportedOutputCapability(cameraId);
const previewProfile = capability.previewProfiles[0];
// 2) 预览输出(surfaceId 来自 XComponent):contentReference[oaicite:16]{index=16}
const previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId);
// 3) 拍照输出
const photoProfile = capability.photoProfiles[0];
const photoOutput = cameraManager.createPhotoOutput(photoProfile);
// 4) 创建会话 + 添加输出
const session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO);
session.beginConfig();
session.addInput(cameraInput);
session.addOutput(previewOutput);
session.addOutput(photoOutput);
session.commitConfig();
await session.start();
// 5) 拍照
const setting: camera.PhotoCaptureSetting = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
rotation: camera.ImageRotation.ROTATION_0
};
await new Promise<void>((resolve, reject) => {
photoOutput.capture(setting, (err: BusinessError) => {
if (err) reject(err);
else resolve();
});
});
// 6) 释放顺序别乱:官方建议拍照结束后再释放:contentReference[oaicite:17]{index=17}
await session.stop();
await cameraInput.close();
await previewOutput.release();
await photoOutput.release();
await session.release();
}
🖼️ 18.3 相册调用:PhotoViewPicker(不乱要权限,也能选图)😄🧺
选相册这块,我最喜欢的点是:Picker 的体验像“官方帮你把权限和安全都包了”。
PhotoViewPicker 的 API 参考里明确写了:select 支持选择图片/视频,返回的 PhotoSelectResult.photoUris 具备“永久授权”,你可以后续用 photoAccessHelper.getAssets 去使用这些资源。
✅ 示例:只让用户选图片(最多 5 张),拿到 URI
import photoAccessHelper from '@ohos.file.photoAccessHelper';
import { BusinessError } from '@kit.BasicServicesKit';
async function pickImages() {
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 5;
const picker = new photoAccessHelper.PhotoViewPicker();
try {
const result = await picker.select(options);
console.info(`🖼️ 选到的URI: ${JSON.stringify(result.photoUris)}`);
// 官方说明:photoUris 具备永久授权,可用于后续 getAssets 使用:contentReference[oaicite:19]{index=19}
return result.photoUris;
} catch (e) {
const err = e as BusinessError;
console.error(`😵 选图失败: ${err.code}, ${err.message}`);
return [];
}
}
🧠 选图“像个人”的产品体验小技巧
- 用户取消别当错误:给个轻提示就行(别弹“失败”把用户吓一跳😅)
- 拿到 URI 后别立刻做重 IO:先显示缩略图/占位,再异步加载原图
- 如果你要上传:建议先拷贝到应用缓存目录再处理(避免直接拿媒体库资源反复读写)
🎙️ 18.4 音频录制(AVRecorder):状态机别违章,后台录制别嘴硬 😤🎧
录音这块,官方文档把“规矩”写得很严:
- 用麦克风要申请
ohos.permission.MICROPHONE - 录制要严格遵循状态机:比如只能在 started 状态 pause,只能在 paused 状态 resume
- 要持续/后台录制需要申请长时任务,避免挂起导致录制被干掉
✅ 示例:录音“开始→暂停→恢复→停止”(骨架版)
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
class AudioRecorderService {
private r?: media.AVRecorder;
async init() {
this.r = await media.createAVRecorder();
this.r.on('stateChange', (s) => console.info(`🎙️ state=${JSON.stringify(s)}`));
this.r.on('error', (e: BusinessError) => console.error(`💥 rec err=${e.code}, ${e.message}`));
}
async start(outputFd: number) {
if (!this.r) await this.init();
// prepare 配置(实际参数按你的编码/采样率/容器格式设置)
await this.r!.prepare({
audioEncoder: media.AudioEncoder.AAC,
audioSourceType: media.AudioSourceType.MIC,
profile: media.AVRecorderProfile.AAC_LC,
url: `fd://${outputFd}` // 也可以用文件路径/FD,按你的业务来
});
await this.r!.start();
}
async pause() {
// 状态机:started 才能 pause :contentReference[oaicite:23]{index=23}
await this.r?.pause();
}
async resume() {
// 状态机:paused 才能 resume :contentReference[oaicite:24]{index=24}
await this.r?.resume();
}
async stopAndRelease() {
await this.r?.stop();
await this.r?.release();
this.r = undefined;
}
}
小提醒(很现实😅):录音权限被用户拒绝时,别硬开录音;给一个“去设置开启权限”的引导才像正常 App。
🧪 18.5 视频处理(AVTranscoder):压缩/转码不是玄学,是工程活儿 🧰😎
转码这块我经常用来干两件事:
- 压缩:拍一段 4K,用户手机存储直接哀嚎……你得给他变小点😮💨
- 兼容:把奇怪格式转成常用格式,提升可播放性
官方说明:AVTranscoder 可实现视频转码,从 API 12 起在手机、平板、2in1 等设备上作为基础能力提供;并可用 canIUse("SystemCapability.Multimedia.Media.AVTranscoder") 判断是否支持。
更关键的是(很多人会忽略):OpenHarmony 的 AVTranscoder 接口文档里明确写了——创建实例后必须设置 fdSrc 和 fdDst,再调用 prepare(config) 开始参数配置;而且同一个 fd 不要并发给多个媒体组件使用,避免竞争导致数据异常。
✅ 示例:转码骨架(fdSrc → fdDst,prepare → start)
import { media } from '@kit.MediaKit';
import { BusinessError } from '@kit.BasicServicesKit';
async function transcodeVideo(fdSrc: media.AVFileDescriptor, fdDst: number) {
// 1) 能力判断(不支持就别硬转😅):contentReference[oaicite:27]{index=27}
if (!canIUse('SystemCapability.Multimedia.Media.AVTranscoder')) {
throw new Error('当前设备不支持 AVTranscoder');
}
const t = await media.createAVTranscoder();
// 2) 设置输入输出(创建后必须设置):contentReference[oaicite:28]{index=28}
t.fdSrc = fdSrc;
t.fdDst = fdDst;
t.on('error', (e: BusinessError) => console.error(`💥 transcode err=${e.code}, ${e.message}`));
t.on('complete', () => console.info('✅ 转码完成啦!'));
// 3) prepare:设置转码参数(分辨率/码率/编码格式等,按你的场景填)
await t.prepare({
// 示例字段按版本可能略有差异;核心是“用 config 描述目标输出”
// 比如:videoCodec / audioCodec / bitrate / width / height / fps...
} as any);
// 4) start / pause / resume / release(按业务需要)
await t.start();
// await t.pause();
// await t.resume();
// 最后记得 release(别让资源赖着不走)😤
// await t.release();
}
友情吐槽😆:转码最“阴”的 bug 往往不是 API 用错,而是你把 同一个 fd 同时塞给了 AVPlayer/AVTranscoder/别的组件,结果数据竞争,转出来的视频“看着像抽象派”。OpenHarmony 文档明确提醒过这种竞争风险。
🧯 18.6 常见翻车现场清单(提前贴你桌上📌)😭
- 🎬 AVPlayer 不 release:用久了卡顿、内存飙升(官方都提醒要 reset/release)
- 📸 拍照没结束就释放会话:拍照失败/崩溃,官方强调要拍完再 stop/release
- 🖼️ 相册乱要读写权限:其实 Picker 就能选(PhotoViewPicker 返回的 URI 还有长期授权)
- 🎙️ AVRecorder 状态机乱调用:started 才 pause、paused 才 resume,别违章
- 🧪 AVTranscoder fd 复用并发:同一 fd 给多个组件并发读写会出竞争,文档有明确警告
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」专栏(全网一个名),bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板、技术文章Markdown文档等海量资料。
✨️ Who am I?
我是bug菌(全网一个名),CSDN | 掘金 | InfoQ | 51CTO | 华为云 | 阿里云 | 腾讯云 等社区博客专家,C站博客之星Top30,华为云多年度十佳博主/价值贡献奖,掘金多年度人气作者Top40,掘金等各大社区平台签约作者,51CTO年度博主Top12,掘金/InfoQ/51CTO等社区优质创作者;全网粉丝合计 30w+;更多精彩福利点击这里;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试真题、4000G PDF电子书籍、简历模板等海量资料,你想要的我都有,关键是你不来拿。

-End-
- 点赞
- 收藏
- 关注作者
评论(0)