用代码写视频——Electron中使用ffmpeg初体验

在Electron应用开发中有时候会涉及到视频的处理,而说起音视频的处理总有一个绕不开的名词—— ffmpeg

ffmpeg简介

虽然使用video标签加上canvas的加持,我们对mp4等基本格式的视频文件有一定的数据处理能力,但对一些不常见的格式就无能为力了。

引入ffempeg

node环境中使用ffempeg需要用到2个库

  • fluent-ffmpeg ffmpeg指令库ffmpeg文档
  • @ffmpeg-installer ffmpeg二进制文件
import ffmpegPath from '@ffmpeg-installer/ffmpeg'
import ffprobePath from '@ffprobe-installer/ffprobe'
import ffmpeg from 'fluent-ffmpeg'

ffmpeg.setFfmpegPath(ffmpegPath.path)
ffmpeg.setFfprobePath(ffprobePath.path)

视频帧解析

ffmpeg提供了视频流式解析的方法,可以在解析过程中获取到视频流数据

ffmpeg('/path/to/file.avi')
  .on('progress', function(progress) {
    console.log('Processing: ' + progress.percent + '% done');
  });

这种方法比较慢,我们需要解析每一帧的输入,如果我们只需要获取其中某处时间点的帧数据或固定时间间隔的帧数据,比如我们需要以秒为单位的帧数据,这时候我们只需要把输入的帧速率改成1帧/秒就可以极大节约计算成本

ffmpeg('/path/to/file.avi')
    .inputFPS(1)
  .on('progress', function(progress) {
    console.log('Processing: ' + progress.percent + '% done');
  });

视频的合成

ffmpeg可以通过转换/合并/读取二进制/图片等方法生成视频,调用方法也非常简单,要想无中生有,我们只需要生成一个帧图片序列就行。

将颜色数据转换成图片

在electron提供了nativeImage对象,它可以将颜色数据buffer转换成图片文件,颜色数据在buffer中是从左上角第一个像素开始从左到右按照像素的r,g,b,a值排列的,那么我们只需要生成一个带有颜色信息的buffer就可以。

生成一个3秒的帧图片

// 帧颜色
const colors = [[255,255,255,255], [0,255,255,255], [255,255,0,255]]

// 保存帧文件
function resolvePicture(index) {
  return path.resolve(TEMP_PATH, index + '-seconds.png')
}

// 生成帧数据buffer
function parseBufferData(color, width, height) {
  const buffer = new Array(width * height * 4)
  let index = 0
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {

            buffer[index] = color[0]
            buffer[index + 1] = color[1]
            buffer[index + 2] = color[2]
            buffer[index + 3] = color[3]

      index += 4
    }
  }
  return Buffer.from(new Uint8Array(buffer).buffer)
}


// 生成帧图片
function saveAnimateDataToImg(streamColor, width, height, time) {

  let bufferData
  let pngBit

  for (let i = 0; i < time - 1; i++) {

        bufferData = parseBufferData(streamColor[i], width, height)

    pngBit = nativeImage.createFromBuffer(bufferData, { width, height, 1 }).toPNG()
    fs.writeFileSync(resolvePicture(i), pngBit)
  }
}

// 生成帧图片
saveAnimateDataToImg(colors, 300, 300, 3)

将帧图片保存为视频

 ffmpeg(path.join(TEMP_PATH, '%d-seconds.png'))
    .inputFPS(1)
    .videoBitrate('2048k')
    .videoCodec('mpeg4')
    .on('end', function() {
      console.log('video one end')
      initPictureFiles(TEMP_PATH)
    })
    .on('error', function(error) {
      console.log('an error happend: create one video' + error)
    })
    .mergeToFile(
      path.resolve(savePath[0], editingAnimation.name + '.mp4'),
      path.join(__dirname, 'temp')
    )