【鸿蒙HarmonyOS Next App实战开发】视频提取音频

在多媒体处理场景中,经常需要从视频文件中提取纯净的音频轨道。本文将介绍如何在HarmonyOS应用中实现这一功能,核心代码基于@ohos/mp4parser库的FFmpeg能力。

功能概述

我们实现了一个完整的视频音频提取页面,包含以下功能:

  1. 通过系统选择器选取视频文件
  2. 将视频复制到应用沙箱目录
  3. 使用FFmpeg命令提取音频
  4. 将生成的音频文件保存到公共下载目录

实现详解

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 = '选择视频';

优化点分析

  1. ​临时文件清理​:无论提取成功与否,都会尝试删除临时文件
  2. ​错误处理​:每个关键步骤都包含try-catch错误捕获
  3. ​权限隔离​:通过沙箱机制处理敏感文件操作

注意事项

  1. ​模块依赖​ :需要提前配置好mp4parser的FFmpeg能力
  2. ​存储权限​:操作公共目录需要申请对应权限
  3. ​大文件处理​:实际生产环境应考虑分块读写避免内存溢出

效果展示

  • 视频选择界面
  • 完成后的提示弹窗

总结

本文介绍的方案实现了完整的视频音频提取功能,充分利用了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();
  }
} 
相关推荐
EasyCVR2 小时前
浅述视频汇聚平台EasyCVR视频编解码与转码技术如何成就视频体验
音视频·视频编解码
文火冰糖的硅基工坊2 小时前
[人工智能-大模型-84]:大模型应用层 - AI/AR眼镜:华为智能眼镜、苹果智能眼镜、Google Glass智能眼镜
华为
yuanlaile3 小时前
Flutter开发HarmonyOS鸿蒙App商业项目实战已出炉
flutter·华为·harmonyos
EasyGBS3 小时前
EasyGBS视频实时监控系助力实现换热站全景可视化管理
音视频
zgyhc20503 小时前
【Android Audio】安卓音频中Surround mode切换流程
android·音视频
cooldream20094 小时前
【案例实战】智能出行导航助手HarmonyOS 开发全流程复盘
华为·harmonyos
CodeCaptain4 小时前
可直接落地的「Flutter 桥接鸿蒙 WebSocket」端到端实施方案
websocket·flutter·harmonyos
猫林老师4 小时前
HarmonyOS图形图像处理与OpenGL ES实战
harmonyos
白鹿第一帅4 小时前
【成长纪实】星光不负 码向未来|我的 HarmonyOS 学习之路与社区成长故事
harmonyos·白鹿第一帅·成都ug社区·csdn成都站·鸿蒙开放能力·鸿蒙学习之路·鸿蒙第一课
数字化顾问4 小时前
(122页PPT)华为初级项目管理培训(附下载方式)
华为