在多媒体处理场景中,经常需要从视频文件中提取纯净的音频轨道。本文将介绍如何在HarmonyOS应用中实现这一功能,核心代码基于@ohos/mp4parser
库的FFmpeg能力。
功能概述
我们实现了一个完整的视频音频提取页面,包含以下功能:
- 通过系统选择器选取视频文件
- 将视频复制到应用沙箱目录
- 使用FFmpeg命令提取音频
- 将生成的音频文件保存到公共下载目录
实现详解
1. 视频选择与沙箱准备
视频选择使用PhotoViewPicker
组件,限定选择类型为视频文件:
private async selectVideo() {
// 创建视频选择器
let context = getContext(this) as common.Context;
let photoPicker = new picker.PhotoViewPicker(context);
let photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE;
// ...其他设置
}
选择视频后,为防止权限问题,我们将视频复制到应用沙箱目录:
private async copyFileToSandbox(sourcePath: string): Promise<string|undefined> {
// 创建沙箱路径
const sandboxPath = getContext(this).cacheDir + "/temp_video.mp4";
// 读写文件操作...
// 具体代码略...
}
2. FFmpeg音频提取
核心提取功能通过MP4Parser
模块实现:
MP4Parser.ffmpegCmd(
`ffmpeg -y -i "${sandboxVideoPath}" -vn -acodec libmp3lame -q:a 2 "${sandboxAudioPath}"`,
callBack
);
关键参数说明:
-vn
:禁止视频输出-acodec libmp3lame
:指定MP3编码器-q:a 2
:设置音频质量(2表示较高品质)
3. 结果保存
音频提取完成后,将文件移动到公共目录:
const documentViewPicker = new picker.DocumentViewPicker(context);
const result = await documentViewPicker.save(documentSaveOptions);
// 在回调中处理文件写入
const targetPath = new fileUri.FileUri(uri + '/'+ audioName).path;
// ...写入操作
4. 状态管理与用户体验
提取过程中通过状态变量控制UI显示:
@State isExtracting: boolean = false;
@State btnText: string = '选择视频';
// 提取开始时更新状态
this.isExtracting = true;
this.btnText = '正在提取...';
// 完成时恢复状态
that.isExtracting = false;
that.btnText = '选择视频';
优化点分析
- 临时文件清理:无论提取成功与否,都会尝试删除临时文件
- 错误处理:每个关键步骤都包含try-catch错误捕获
- 权限隔离:通过沙箱机制处理敏感文件操作
注意事项
- 模块依赖 :需要提前配置好
mp4parser
的FFmpeg能力 - 存储权限:操作公共目录需要申请对应权限
- 大文件处理:实际生产环境应考虑分块读写避免内存溢出
效果展示
- 视频选择界面
- 完成后的提示弹窗
总结
本文介绍的方案实现了完整的视频音频提取功能,充分利用了HarmonyOS的文件管理和FFmpeg处理能力。核心代码约200行,展示了从视频选择到音频生成的关键流程。开发者可基于此方案扩展更复杂的多媒体处理功能。
具体效果华为应用商店搜索【图影工具箱】查看

完整代码
TypeScript
import { MP4Parser } from "@ohos/mp4parser";
import { ICallBack } from "@ohos/mp4parser";
import { fileIo as fs } from '@kit.CoreFileKit';
import { fileUri, picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { TitleBar } from "../components/TitleBar";
@Entry
@Component
struct AudioExtractPage {
@State btnText: string = '选择视频';
@State selectedVideoPath: string = '';
@State isExtracting: boolean = false;
@State imageWidth: number = 0;
@State imageHeight: number = 0;
getResourceString(res: Resource) {
return getContext().resourceManager.getStringSync(res.id)
}
build() {
Column() {
// 顶部栏
TitleBar({
title: '视频音频提取'
})
if (this.selectedVideoPath) {
Text('已选择视频:' + this.selectedVideoPath)
.fontSize(16)
.margin({ bottom: 20 })
}
Button(this.btnText, { type: ButtonType.Normal, stateEffect: true })
.borderRadius(8)
.backgroundColor(0x317aff)
.width(250)
.margin({ top: 15 })
.onClick(() => {
if (!this.isExtracting) {
this.selectVideo();
}
})
if (this.isExtracting) {
Image($r('app.media.icon_load'))
.objectFit(ImageFit.None)
.width(this.imageWidth)
.height(this.imageHeight)
.border({ width: 0 })
.borderStyle(BorderStyle.Dashed)
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.index_tab_bar'))
}
private async selectVideo() {
try {
let context = getContext(this) as common.Context;
let photoPicker = new picker.PhotoViewPicker(context);
let photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.VIDEO_TYPE;
photoSelectOptions.maxSelectNumber = 1;
let result = await photoPicker.select(photoSelectOptions);
console.info('PhotoViewPicker.select result: ' + JSON.stringify(result));
if (result && result.photoUris && result.photoUris.length > 0) {
this.selectedVideoPath = result.photoUris[0];
console.info('Selected video path: ' + this.selectedVideoPath);
this.extractAudio();
}
} catch (err) {
console.error('选择视频失败:' + JSON.stringify(err));
AlertDialog.show({ message: '选择视频失败' });
}
}
private async copyFileToSandbox(sourcePath: string): Promise<string|undefined> {
try {
// 获取沙箱目录路径
const sandboxPath = getContext(this).cacheDir + "/temp_video.mp4";
// 读取源文件内容
const sourceFd = await fs.open(sourcePath, fs.OpenMode.READ_ONLY);
const fileStats = await fs.stat(sourceFd.fd);
const buffer = new ArrayBuffer(fileStats.size);
await fs.read(sourceFd.fd, buffer);
await fs.close(sourceFd);
// 写入到沙箱目录
const targetFd = await fs.open(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await fs.write(targetFd.fd, buffer);
await fs.close(targetFd);
return sandboxPath;
} catch (err) {
console.error('复制文件到沙箱失败:' + err);
return undefined;
}
}
private async moveToPublicDirectory(sourcePath: string): Promise<string|undefined> {
try {
const documentSaveOptions = new picker.DocumentSaveOptions();
documentSaveOptions.pickerMode = picker.DocumentPickerMode.DOWNLOAD;
let context = getContext(this) as common.Context;
const documentViewPicker = new picker.DocumentViewPicker(context);
const result = await documentViewPicker.save(documentSaveOptions);
if (result && result.length > 0) {
const uri = result[0];
console.info('documentViewPicker.save succeed and uri is:' + uri);
// 读取源文件内容
const sourceFd = await fs.open(sourcePath, fs.OpenMode.READ_ONLY);
const fileStats = await fs.stat(sourcePath);
const buffer = new ArrayBuffer(fileStats.size);
await fs.read(sourceFd.fd, buffer);
await fs.close(sourceFd);
// 写入到目标文件
const audioName = 'extracted_audio_' + new Date().getTime() + '.mp3';
const targetPath = new fileUri.FileUri(uri + '/'+ audioName).path;
const targetFd = await fs.open(targetPath, fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
await fs.write(targetFd.fd, buffer);
await fs.close(targetFd);
return audioName;
}
return undefined;
} catch (err) {
console.error('移动到公共目录失败:' + err);
return undefined;
}
}
private async extractAudio() {
if (!this.selectedVideoPath) {
AlertDialog.show({ message: '请先选择视频' });
return;
}
this.isExtracting = true;
this.imageWidth = 25;
this.imageHeight = 25;
this.btnText = '正在提取...';
try {
// 1. 复制视频到沙箱目录
const sandboxVideoPath = await this.copyFileToSandbox(this.selectedVideoPath);
// 2. 在沙箱目录中执行ffmpeg命令
const sandboxAudioPath = getContext(this).cacheDir + "/temp_audio.mp3";
const that = this;
let callBack: ICallBack = {
async callBackResult(code: number) {
that.isExtracting = false;
that.imageWidth = 0;
that.imageHeight = 0;
that.btnText = '选择视频';
if (code == 0) {
try {
// 3. 将音频文件移动到公共目录
const publicPath = await that.moveToPublicDirectory(sandboxAudioPath);
AlertDialog.show({
message: '音频提取成功,保存路径:我的手机/Download(下载)/图影工具箱/' + publicPath
});
} catch (err) {
console.error('移动文件失败:' + err);
AlertDialog.show({ message: '音频提取成功但保存失败' });
}
} else {
AlertDialog.show({ message: '音频提取失败' });
}
// 清理临时文件
try {
await fs.unlink(sandboxVideoPath);
await fs.unlink(sandboxAudioPath);
} catch (err) {
console.error('清理临时文件失败:' + err);
}
}
}
// 使用ffmpeg命令提取音频
MP4Parser.ffmpegCmd(
`ffmpeg -y -i "${sandboxVideoPath}" -vn -acodec libmp3lame -q:a 2 "${sandboxAudioPath}"`,
callBack
);
} catch (err) {
this.isExtracting = false;
this.imageWidth = 0;
this.imageHeight = 0;
this.btnText = '选择视频';
console.error('提取过程出错:' + err);
AlertDialog.show({ message: '提取过程出错' });
}
}
aboutToAppear() {
MP4Parser.openNativeLog();
}
}