使用ffmpeg.wasm实现在浏览器运行ffmpeg(附带代码仓库地址)

音视频在线处理

使用ffmpeg.wasm实现在浏览器运行ffmpeg,可以处理音视频的剪切,拼接,转换等各种操作


image.png

在线预览

tip:首次加载需要加载ffmpeg.wasm文件,预览网站部署的服务器带宽较低,需要等待一段时间

关键知识点

  • ffmpeg.wasm 在浏览器中的引用
  • 部署之后 SharedArrayBuffer is not defined的解决
  • ffmpeg的命令执行

关键代码

引用的umd的插件,因此需要自己声明,且没有代码提示
好处是可以直接完整迁移到别的框架下面

declare const FFmpeg: any
const { createFFmpeg, fetchFile } = FFmpeg

class CustomVideoHandle {
  private ffmpeg: any
  constructor() {}

  loadFfmpeg() {
    // 依赖ffmpeg.wasm WebAssembly
    if (!this.ffmpeg) {
      this.ffmpeg = createFFmpeg({
        corePath: '/libs/ffmpeg-core.js',
        log: true
      })
    }
    let promise = this.ffmpeg.isLoaded() ? Promise.resolve() : this.ffmpeg.load()
    return promise
  }

  getVideoVoice({ videoUrl = '' as string, blobUrl = true as boolean } = {}) {
    return this.loadFfmpeg()
      .then(() => {
        return fetchFile(videoUrl)
      })
      .then((res: any) => {
        this.ffmpeg.FS('writeFile', 'input.mp4', res)
        return this.ffmpeg.run('-i', 'input.mp4', '-acodec', 'libmp3lame', '-f', 'mp3', 'outputAudio.mp3')
      })
      .then(() => {
        return this.ffmpeg.FS('readFile', 'outputAudio.mp3')
      })
      .then((res: any) => {
        this.ffmpeg.FS('unlink', 'input.mp4')
        this.ffmpeg.FS('unlink', 'outputAudio.mp3')
        let blob = new Blob([res.buffer], { type: 'audio/mp3' })
        return blobUrl ? URL.createObjectURL(blob) : blob
      })
      .catch((err: Error) => {
        console.log(err)
        throw err
      })
  }

  addVideoVoice({ videoUrl = '' as string, voiceUrl = '' as string, blobUrl = true as boolean }) {
    return this.loadFfmpeg()
      .then(() => {
        return Promise.all([fetchFile(videoUrl), fetchFile(voiceUrl)])
      })
      .then((res: any) => {
        let [videoData, voiceData] = res
        return Promise.all([this.ffmpeg.FS('writeFile', 'input.mp4', videoData), this.ffmpeg.FS('writeFile', 'input.mp3', voiceData)])
      })
      .then(() => {
        return this.ffmpeg.run(
          '-i',
          'input.mp4',
          '-i',
          'input.mp3',
          '-c:v',
          'copy',
          '-c:a',
          'aac',
          '-strict',
          'experimental',
          '-map',
          '0:v:0',
          '-map',
          '1:a:0',
          'output.mp4'
        )
      })
      .then(() => {
        return this.ffmpeg.FS('readFile', 'output.mp4')
      })
      .then((mp4Data: any) => {
        this.ffmpeg.FS('unlink', 'input.mp4')
        this.ffmpeg.FS('unlink', 'input.mp3')
        this.ffmpeg.FS('unlink', 'output.mp4')
        let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
        return blobUrl ? URL.createObjectURL(blob) : blob
      })
      .catch((err: Error) => {
        throw err
      })
  }

  reverseVideoAndAudio({ videoUrl = '' as string, blobUrl = true as boolean }): Promise<string | Blob> {
    console.log('反转视频')
    return this.loadFfmpeg()
      .then(() => {
        return fetchFile(videoUrl)
      })
      .then((videoData: any) => {
        return this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
      })
      .then(() => {
        const command: string[] = ['-i', 'input.mp4', '-vf', 'reverse', '-af', 'areverse', 'output.mp4']
        return this.ffmpeg.run(...command)
      })
      .then(() => {
        return this.ffmpeg.FS('readFile', 'output.mp4')
      })
      .then((mp4Data: any) => {
        this.ffmpeg.FS('unlink', 'input.mp4')
        this.ffmpeg.FS('unlink', 'output.mp4')
        let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
        return blobUrl ? URL.createObjectURL(blob) : blob
      })
      .catch((err: Error) => {
        throw err
      })
  }

  async composeAudioAndImages({ voiceUrls = [], imageUrls = [], imageType = 'png', blobUrl = true }) {
    if (voiceUrls.length !== imageUrls.length) {
      throw new Error('The number of audio files must match the number of image files.')
    }
    let index = 0
    let videoList = []
    for (let voiceUrl of voiceUrls) {
      videoList.push(await this.voiceAddImage({ voiceUrl, imageUrl: imageUrls[index], imageType }))
      index++
    }
    try {
      const videoDataList = await Promise.all(videoList.map((i: any) => fetchFile(i)))
      const writePromises = videoDataList.map((data: any, index: number) => {
        return this.ffmpeg.FS('writeFile', `input${index}.mp4`, data)
      })
      await Promise.all(writePromises)
      const inputArgs = videoList.map((_, index) => ['-i', `input${index}.mp4`]).flat()
      const command = [
        ...inputArgs,
        '-filter_complex',
        `concat=n=${videoList.length}:v=1:a=1 [v] [a]`,
        '-map',
        '[v]',
        '-map',
        '[a]',
        '-c:v',
        'libx264',
        '-c:a',
        'aac',
        'output.mp4'
      ]
      await this.ffmpeg.run(...command)
      const mp4Data = await this.ffmpeg.FS('readFile', 'output.mp4')
      videoDataList.forEach((_, index) => {
        this.ffmpeg.FS('unlink', `input${index}.mp4`)
      })
      this.ffmpeg.FS('unlink', 'output.mp4')
      const blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
      return blobUrl ? URL.createObjectURL(blob) : blob
    } catch (err) {
      throw err
    }
  }

  voiceAddImage({ voiceUrl = '', imageUrl = '', imageType = 'png', blobUrl = true }): Promise<string | Blob> {
    return this.loadFfmpeg()
      .then(() => {
        return Promise.all([fetchFile(voiceUrl), fetchFile(imageUrl)])
      })
      .then((res: any) => {
        let [imageData, voiceData] = res
        return Promise.all([
          this.ffmpeg.FS('writeFile', `image.${imageType}`, imageData),
          this.ffmpeg.FS('writeFile', 'input.mp3', voiceData)
        ])
      })
      .then((res: any) => {
        const command = [
          '-i',
          `image.${imageType}`,
          '-i',
          'input.mp3',
          '-c:v',
          'libx264',
          '-tune',
          'stillimage',
          '-c:a',
          'aac',
          '-strict',
          'experimental',
          '-pix_fmt',
          'yuv420p',
          '-q:v',
          '1',
          'output.mp4'
        ]
        return this.ffmpeg.run(...command)
      })
      .then(() => {
        return this.ffmpeg.FS('readFile', 'output.mp4')
      })
      .then((mp4Data: any) => {
        this.ffmpeg.FS('unlink', `image.${imageType}`)
        this.ffmpeg.FS('unlink', 'input.mp3')
        this.ffmpeg.FS('unlink', 'output.mp4')
        let blob = new Blob([mp4Data.buffer], { type: 'video/mp4' })
        return blobUrl ? URL.createObjectURL(blob) : blob
      })
      .catch((err: Error) => {
        throw err
      })
  }

  async trimVideo({ videoUrl = '', startTime = 2, endTime = 6, blobUrl = true } = {}) {
    await this.loadFfmpeg()
    let videoData = await fetchFile(videoUrl)
    await this.ffmpeg.FS('writeFile', `input.mp4`, videoData)
    const outputFileName = 'trimmedVideo.mp4'
    const ffmpegCommand = [
      '-i',
      'input.mp4',
      '-ss',
      String(startTime),
      '-to',
      String(endTime),
      '-c:v',
      'libx264',
      '-c:a',
      'copy',
      '-strict',
      'experimental',
      outputFileName
    ]
    await this.ffmpeg.run(...ffmpegCommand)
    const trimmedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
    this.ffmpeg.FS('unlink', outputFileName)
    this.ffmpeg.FS('unlink', 'input.mp4')
    const trimmedVideoBlob = new Blob([trimmedVideoData.buffer], { type: 'video/mp4' })
    return blobUrl ? URL.createObjectURL(trimmedVideoBlob) : trimmedVideoBlob
  }

  async cropVideo({ videoUrl = '', width = 720, height = 720, x = 0, y = 0, blobUrl = true } = {}) {
    await this.loadFfmpeg()
    let videoData = await fetchFile(videoUrl)
    await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)

    const outputFileName = 'croppedVideo.mp4'
    let ffmpegCommand = ['-i', 'input.mp4', '-vf', `crop=${width}:${height}:${x}:${y}`, '-c:v', 'libx264', outputFileName]

    await this.ffmpeg.run(...ffmpegCommand)

    const croppedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
    this.ffmpeg.FS('unlink', outputFileName)
    this.ffmpeg.FS('unlink', 'input.mp4')

    const croppedVideoBlob = new Blob([croppedVideoData.buffer], { type: 'video/mp4' })
    return blobUrl ? URL.createObjectURL(croppedVideoBlob) : croppedVideoBlob
  }

  async scaleVideo({ videoUrl = '', width = 720, height = 720, blobUrl = true } = {}) {
    await this.loadFfmpeg()
    let videoData = await fetchFile(videoUrl)
    await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)

    const outputFileName = 'croppedVideo.mp4'
    let ffmpegCommand = ['-i', 'input.mp4', '-vf', `scale=${width}:${height}`, '-c:v', 'libx264', outputFileName]

    await this.ffmpeg.run(...ffmpegCommand)

    const croppedVideoData = await this.ffmpeg.FS('readFile', outputFileName)
    this.ffmpeg.FS('unlink', outputFileName)
    this.ffmpeg.FS('unlink', 'input.mp4')

    const croppedVideoBlob = new Blob([croppedVideoData.buffer], { type: 'video/mp4' })
    return blobUrl ? URL.createObjectURL(croppedVideoBlob) : croppedVideoBlob
  }

  async convertVideoToImages({ videoUrl = '', duration = 10, frameRate = 8, imageType = 'png', blobUrl = true } = {}) {
    await this.loadFfmpeg()
    const videoData = await fetchFile(videoUrl)
    await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
    const outputImagePattern = 'output-%04d.' + imageType
    const ffmpegCommand = ['-i', 'input.mp4', '-r', `${frameRate}`, outputImagePattern]
    await this.ffmpeg.run(...ffmpegCommand)
    const imageBlobUrls = []
    const numFrames = Math.floor(duration * frameRate)
    for (let i = 1; i <= numFrames; i++) {
      const frameIndex = i.toString().padStart(4, '0')
      const imageFileName = outputImagePattern.replace('%04d', frameIndex)
      const imageData = await this.ffmpeg.FS('readFile', imageFileName)
      const imageBlob = new Blob([imageData.buffer], { type: `image/${imageType}` })
      const imageUrl = blobUrl ? URL.createObjectURL(imageBlob) : imageBlob
      imageBlobUrls.push(imageUrl)
      this.ffmpeg.FS('unlink', imageFileName)
    }
    this.ffmpeg.FS('unlink', 'input.mp4')
    return imageBlobUrls
  }

  // 运行复合指令处理视频
  async handleVideoCommand({
    videoUrl = '',
    blobUrl = true,
    command = [] as string[],
    outputFileName = 'handleVideo.mp4' as string,
    outputType = 'video/mp4' as string
  } = {}) {
    await this.loadFfmpeg()
    let videoData = await fetchFile(videoUrl)
    await this.ffmpeg.FS('writeFile', 'input.mp4', videoData)
    let ffmpegCommand = ['-i', 'input.mp4', ...command, outputFileName]
    await this.ffmpeg.run(...ffmpegCommand)
    const handleVideoData = await this.ffmpeg.FS('readFile', outputFileName)
    this.ffmpeg.FS('unlink', outputFileName)
    this.ffmpeg.FS('unlink', 'input.mp4')
    const handleVideoBlob = new Blob([handleVideoData.buffer], { type: outputType })
    return blobUrl ? URL.createObjectURL(handleVideoBlob) : handleVideoBlob
  }
}

export const useFFmpeg = () => {
  const ffmpegHandle = new CustomVideoHandle()

  const onInit = () => {
    return ffmpegHandle.loadFfmpeg()
  }

  return { onInit, ffmpegHandle }
}

完整代码

码云仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值