HarmonyOS 多媒体开发实战:音频播放、视频播放与相机拍照
HarmonyOS 多媒体开发实战:音频播放、视频播放与相机拍照## 一、前言多媒体功能是现代应用中不可或缺的组成部分。HarmonyOS 提供了强大的多媒体开发套件(@kit.MediaKit),涵盖音频播放录制、视频播放和相机拍照等功能。本文将以 HarmonyOS 5.0.0(API 12)为基础,通过完整可运行的代码示例,详细讲解三大核心多媒体功能的开发方法。## 二、权限配置在 module.json5 中配置权限:{
“module”: {
“requestPermissions”: [
{ “name”: “ohos.permission.INTERNET” },
{ “name”: “ohos.permission.MICROPHONE” },
{ “name”: “ohos.permission.CAMERA” },
{ “name”: “ohos.permission.WRITE_MEDIA” },
{ “name”: “ohos.permission.READ_MEDIA” }
]
}
}
三、音频播放### 3.1 基础音频播放器import { media } from ‘@kit.MediaKit’;
@Entry
@Component
struct AudioPlayer {
@State status: string = ‘空闲’;
@State currentTime: number = 0;
@State duration: number = 0;
@State isPlaying: boolean = false;
private avPlayer?: media.AVPlayer;
async initPlayer() {
try {
this.avPlayer = await media.createAVPlayer();
const context = getContext(this);
const fd = await context.resourceManager.getRawFd(‘test_audio.mp3’);
this.avPlayer.fdSrc = { fd: fd.fd, offset: fd.offset, length: fd.length };
this.avPlayer.on('stateChange', (state: media.AVPlayerState) => {
switch (state) {
case 'prepared':
this.status = '准备就绪';
this.duration = this.avPlayer?.duration ?? 0;
break;
case 'playing':
this.status = '播放中';
this.isPlaying = true;
break;
case 'paused':
this.status = '已暂停';
this.isPlaying = false;
break;
case 'completed':
this.status = '播放完成';
this.isPlaying = false;
break;
}
});
this.avPlayer.on('timeUpdate', (time: number) => { this.currentTime = time });
await this.avPlayer.prepare();
} catch (error) { this.status = `初始化失败: ${error}` }
}
async play() { this.avPlayer && await this.avPlayer.play() }
async pause() { this.avPlayer && await this.avPlayer.pause() }
async stop() { this.avPlayer && await this.avPlayer.stop() }
async seekTo(timeMs: number) {
this.avPlayer && await this.avPlayer.seek(timeMs, media.SeekMode.SEEK_PREV_SYNC)
}
aboutToDisappear() { this.avPlayer?.release() }
formatTime(ms: number): string {
const total = Math.floor(ms / 1000);
return ${Math.floor(total/60).toString().padStart(2,'0')}:${(total%60).toString().padStart(2,'0')};
}
build() {
Column({ space: 20 }) {
Text(‘🎵 音频播放器’).fontSize(28).fontWeight(FontWeight.Bold)
Text(this.status).fontSize(16).fontColor('#007AFF')
.padding(8).backgroundColor('#E3F2FD').borderRadius(8)
Column({ space: 8 }) {
Slider({ value: this.currentTime, min: 0,
max: this.duration > 0 ? this.duration : 100, step: 1000 })
.width('100%').blockColor('#007AFF').trackColor('#E0E0E0')
.onChange((value: number) => { this.seekTo(value) })
Row() {
Text(this.formatTime(this.currentTime)).fontSize(12).fontColor('#888')
Blank()
Text(this.formatTime(this.duration)).fontSize(12).fontColor('#888')
}.width('100%')
}.width('100%')
Row({ space: 24 }) {
Button(this.isPlaying ? '⏸ 暂停' : '▶️ 播放')
.fontSize(18).backgroundColor(this.isPlaying ? '#FF9800' : '#4CAF50')
.onClick(() => { this.isPlaying ? this.pause() : this.play() })
Button('⏹ 停止').fontSize(18).backgroundColor('#FF4444')
.onClick(() => { this.stop() })
}
}.width('100%').height('100%').justifyContent(FlexAlign.Center).padding(20)
}
async aboutToAppear() { await this.initPlayer() }
}
四、视频播放### 4.1 Video 组件@Entry
@Component
struct VideoPlayer {
@State isPlaying: boolean = false;
@State isMuted: boolean = false;
@State videoSrc: string | Resource = ‘’;
private videoController: VideoController = new VideoController();
aboutToAppear() { this.videoSrc = $rawfile(‘sample_video.mp4’) }
build() {
Column({ space: 16 }) {
Text(‘🎬 视频播放器’).fontSize(28).fontWeight(FontWeight.Bold)
Video({ src: this.videoSrc, controller: this.videoController })
.width('100%').height(250).controls(true).loop(true)
.objectFit(ImageFit.Contain)
.onStart(() => { this.isPlaying = true })
.onPause(() => { this.isPlaying = false })
.onFinish(() => { this.isPlaying = false })
.onError((error) => { console.error(`视频错误: ${error}`) })
Row({ space: 16 }) {
Button(this.isPlaying ? '⏸ 暂停' : '▶️ 播放').fontSize(14)
.onClick(() => { this.isPlaying ? this.videoController.pause() : this.videoController.start() })
Button('⏪ -10s').fontSize(14)
.onClick(() => { this.videoController.setCurrentTime(Math.max(0, this.videoController.getCurrentTime()-10)) })
Button('⏩ +10s').fontSize(14)
.onClick(() => { this.videoController.setCurrentTime(this.videoController.getCurrentTime()+10) })
Button(this.isMuted ? '🔇' : '🔊').fontSize(14)
.onClick(() => { this.isMuted = !this.isMuted; this.videoController.setVolume(this.isMuted ? 0 : 1) })
}.width('100%').justifyContent(FlexAlign.SpaceEvenly)
Row({ space: 8 }) {
Button('本地视频').fontSize(12).onClick(() => { this.videoSrc = $rawfile('sample_video.mp4') })
Button('网络视频').fontSize(12)
.onClick(() => { this.videoSrc = 'https://media.w3.org/2010/05/sintel/trailer.mp4' })
}
}.width('100%').height('100%').padding(16)
}
}
五、相机拍照### 5.1 CameraPicker(推荐)import { cameraPicker } from ‘@kit.CameraKit’;
@Entry
@Component
struct CameraPickerDemo {
@State photoUri: string = ‘’;
@State statusText: string = ‘点击按钮拍照’;
async takePhoto() {
try {
const picker = new cameraPicker.CameraPicker(getContext(this));
const pickerProfile: cameraPicker.PickerProfile = {
mediaType: cameraPicker.PickerMediaType.PHOTO,
saveUri: undefined
};
this.statusText = ‘正在打开相机…’;
const result = await picker.pick(pickerProfile);
if (result && result.resultUri) {
this.photoUri = result.resultUri;
this.statusText = '拍照成功!';
} else {
this.statusText = '拍照已取消';
}
} catch (error) {
this.statusText = `拍照失败: ${JSON.stringify(error)}`;
}
}
build() {
Column({ space: 20 }) {
Text(‘📷 相机拍照’).fontSize(28).fontWeight(FontWeight.Bold)
if (this.photoUri) {
Image(this.photoUri).width(300).height(300).objectFit(ImageFit.Cover)
.borderRadius(12).border({ width: 2, color: '#E0E0E0' })
} else {
Column() {
Rectangle().width(300).height(300).fill('#F5F5F5').borderRadius(12)
Text('暂无照片').fontSize(16).fontColor('#AAA')
}
}
Text(this.statusText).fontSize(14).fontColor('#666')
Button('📸 拍照').fontSize(18).width(200).height(48).borderRadius(24)
.onClick(() => { this.takePhoto() })
}.width('100%').height('100%').justifyContent(FlexAlign.Center).padding(20)
}
}
六、音频录制import { media } from ‘@kit.MediaKit’;
@Entry
@Component
struct AudioRecorder {
@State isRecording: boolean = false;
@State recordTime: number = 0;
@State statusText: string = ‘点击开始录音’;
@State recordFilePath: string = ‘’;
private audioRecorder?: media.AudioRecorder;
private timerId?: number;
async startRecording() {
try {
this.audioRecorder = await media.createAudioRecorder();
const audioConfig: media.AudioRecorderConfig = {
audioEncoder: media.AudioEncoder.AAC_LC,
audioEncodeBitRate: 128000,
audioSampleRate: 44100,
numberOfChannels: 2,
fileFormat: media.ContainerFormatType.CFT_MPEG_4A,
uri: this.getRecordFilePath()
};
await this.audioRecorder.start(audioConfig);
this.isRecording = true;
this.statusText = '录音中...';
this.recordTime = 0;
this.timerId = setInterval(() => { this.recordTime++ }, 1000);
} catch (error) { this.statusText = `录音失败: ${error}` }
}
async stopRecording() {
if (!this.audioRecorder) return;
try {
await this.audioRecorder.stop();
this.audioRecorder.release();
this.isRecording = false;
this.statusText = ‘录音完成’;
if (this.timerId) { clearInterval(this.timerId); this.timerId = undefined }
} catch (error) { this.statusText = 停止录音失败: ${error} }
}
getRecordFilePath(): string {
const filePath = ${getContext(this).filesDir}/recording_${Date.now()}.m4a;
this.recordFilePath = filePath;
return filePath;
}
formatTime(seconds: number): string {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return ${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')};
}
build() {
Column({ space: 24 }) {
Text(‘🎙 录音机’).fontSize(28).fontWeight(FontWeight.Bold)
Text(this.formatTime(this.recordTime)).fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(this.isRecording ? '#FF4444' : '#666')
.fontFamily('monospace')
Text(this.statusText).fontSize(16).fontColor('#888')
if (this.isRecording) {
Row({ space: 4 }) {
ForEach([1,2,3,4,5], (item: number) => {
Rectangle().width(4).height(20+Math.random()*30).fill('#FF4444').borderRadius(2)
})
}.width(40).height(50)
}
Button(this.isRecording ? '⏹ 停止录音' : '🔴 开始录音')
.fontSize(18).width(200).height(48).borderRadius(24)
.backgroundColor(this.isRecording ? '#FF4444' : '#007AFF')
.onClick(() => { this.isRecording ? this.stopRecording() : this.startRecording() })
if (this.recordFilePath && !this.isRecording) {
Text(`文件保存: ${this.recordFilePath}`).fontSize(11).fontColor('#AAA')
.maxLines(2).textOverflow({ overflow: TextOverflow.Ellipsis })
}
}.width('100%').height('100%').justifyContent(FlexAlign.Center).padding(20)
}
}
七、完整实战:媒体画廊enum MediaType { PHOTO, VIDEO, AUDIO }
interface MediaRecord {
id: number; type: MediaType; uri: string; timestamp: string; title: string;
}
@Entry
@Component
struct MediaGallery {
@State records: MediaRecord[] = [];
@State selectedTab: number = 0;
aboutToAppear() {
this.records = [
{ id: 1, type: MediaType.PHOTO, uri: ‘’, timestamp: ‘2026-06-07 10:30’, title: ‘风景照’ },
{ id: 2, type: MediaType.VIDEO, uri: ‘’, timestamp: ‘2026-06-07 11:00’, title: ‘开发教程’ },
{ id: 3, type: MediaType.AUDIO, uri: ‘’, timestamp: ‘2026-06-07 12:00’, title: ‘会议录音’ }
];
}
getTypeIcon(type: MediaType): string {
switch (type) {
case MediaType.PHOTO: return ‘📷’;
case MediaType.VIDEO: return ‘🎬’;
case MediaType.AUDIO: return ‘🎵’;
}
}
getFilteredRecords(): MediaRecord[] {
return this.records.filter(r => r.type === this.selectedTab);
}
build() {
Column({ space: 12 }) {
Text(‘📂 媒体库’).fontSize(28).fontWeight(FontWeight.Bold)
.padding({ top: 16, left: 16, right: 16 })
Row() {
Button(`📷 照片 (${this.records.filter(r=>r.type===MediaType.PHOTO).length})`)
.fontSize(14).backgroundColor(this.selectedTab===MediaType.PHOTO?'#007AFF':'#F0F0F0')
.fontColor(this.selectedTab===MediaType.PHOTO?'#FFF':'#333').layoutWeight(1)
Button(`🎬 视频 (${this.records.filter(r=>r.type===MediaType.VIDEO).length})`)
.fontSize(14).backgroundColor(this.selectedTab===MediaType.VIDEO?'#007AFF':'#F0F0F0')
.fontColor(this.selectedTab===MediaType.VIDEO?'#FFF':'#333').layoutWeight(1)
Button(`🎵 音频 (${this.records.filter(r=>r.type===MediaType.AUDIO).length})`)
.fontSize(14).backgroundColor(this.selectedTab===MediaType.AUDIO?'#007AFF':'#F0F0F0')
.fontColor(this.selectedTab===MediaType.AUDIO?'#FFF':'#333').layoutWeight(1)
}.width('100%').padding({ left: 16, right: 16 })
if (this.getFilteredRecords().length === 0) {
Column({ space: 12 }) {
Text('📭').fontSize(48)
Text('暂无记录').fontSize(16).fontColor('#888')
}.width('100%').layoutWeight(1).justifyContent(FlexAlign.Center)
} else {
List({ space: 8 }) {
ForEach(this.getFilteredRecords(), (record: MediaRecord) => {
ListItem() {
Row({ space: 12 }) {
Text(this.getTypeIcon(record.type)).fontSize(32)
Column({ space: 4 }) {
Text(record.title).fontSize(16).fontWeight(FontWeight.Medium)
Text(record.timestamp).fontSize(12).fontColor('#888')
}.layoutWeight(1).alignItems(HorizontalAlign.Start)
Button('查看').fontSize(12).backgroundColor('#007AFF')
}.width('100%').padding(12).backgroundColor('#FAFAFA').borderRadius(8)
}
}, (record: MediaRecord) => record.id.toString())
}.layoutWeight(1).width('100%').padding({ left: 16, right: 16 })
}
}.width('100%').height('100%')
}
}
八、常见问题与最佳实践| 问题 | 解答 ||------|------|| AVPlayer 状态机 | initialized → prepared → playing/paused → completed/stopped → released || 视频无法播放? | 检查格式(MP4/H.264),H.265 需要硬件支持 || 相机权限被拒? | 调用 requestPermissionsFromUser 动态申请 || 录音文件太大? | 调整 audioEncodeBitRate(默认 128000bps) || 保存到相册? | photoAccessHelper.createAsset |## 九、总结| 功能 | 核心 API | 最佳场景 ||------|----------|---------|| 音频播放 | media.createAVPlayer() | 音乐、播客 || 视频播放 | Video 组件 | 播放器、短视频 || 相机拍照 | CameraPicker / Camera Kit | 拍照、扫码 || 音频录制 | media.createAudioRecorder() | 录音、语音笔记 |开发建议:- 优先使用 Video 组件,简单场景无需自定义- CameraPicker 比自定义相机更稳定- 播放完成后记得 release AVPlayer- 音频录制用 AAC_LC 编码兼容性最好> 参考文档:华为开发者联盟 HarmonyOS 5.0.0 API 12 — 多媒体开发指南
更多推荐


所有评论(0)