FFmpeg命令踩坑

453 阅读2分钟

需求

用一堆图片来合成一个视频

基础的数据类

class FFmpegData {
  ///游标
  int index;
  int x;
  int y;
  int w;
  int h;

  ///旋转角度
  int angle;

  ///文件路径
  String path;

  ///缩放别名
  String scaleAlias;

  ///旋转别名
  String rotationAlias;

  ///游标别名
  String _indexAlias;

  ///位置别名
  String positionAlias;

  ///别名
  String vAlias;

  ///构造函数
  /// index 用来处理ffmpeg的文件参数,不要做修改。
  /// x=x轴的坐标;
  /// y=y轴的坐标;
  /// w=显示的宽度具体像素大小;
  /// h=显示的高度具体像素大小;
  /// path=再存储器上分别存储的全路径地址,一定要全路径;
  FFmpegData(this.index, this.x, this.y, this.w, this.h, this.angle, this.path) {
    _indexAlias = '[$index:v]';
    vAlias = '[${index}v]';
    rotationAlias = '[rotation$index]';
    scaleAlias = '[scale$index]';
    positionAlias = '[dkg$index]';
  }

  ///旋转缩放的命令行
  /// '[1:v]scale=200:200[scale1];'
  String rotationAndScaleString() {
    //:ow=hypot(iw,ih):oh=ow
    return '${_indexAlias}fade=out:st=30:d=1:alpha=1$vAlias;${vAlias}rotate=\'$angle*PI/180:c=none\'$rotationAlias;${rotationAlias}scale=$w:$h$scaleAlias;';
  }

  ///位置的命令行
  String positionString(String lastAlias, bool end) {
    return '$lastAlias${scaleAlias}overlay=$x:$y${end ? ' ' : '$positionAlias;'}';
  }

  String fileString() {
    if (path.toLowerCase().endsWith('.gif')) {
      return '-ignore_loop 0 -i "$path" '; //gif的输入方式的区别
    } else {
      return '-i $path ';
    }
  }
}

合成命令

  String dat(String background, String music, List<FFmpegData> datas, String outMp4FilePath) {
    File mp4 = File(outMp4FilePath);
    String dat = '-threads 4 -y -r 30 ';
    if (background.toLowerCase().endsWith('.mp4')) {
      dat += '-stream_loop -1 -i "$background" ';
    } else {
      dat += '-loop 1 -f image2 -i "$background" ';
    }
    if (datas != null && datas.isNotEmpty) {
      //先添加文件
      for (int i = 0; i < datas.length; i++) {
        dat += datas[i].fileString();
      }
      if (music != null && music.isNotEmpty) {
        dat += '-stream_loop -1 -i "$music" ';
        if (background.toLowerCase().endsWith('.mp4')) {
          int musicIndex = datas.length + 1;
          dat += '-c:v copy -map 0:v:0 -map $musicIndex:a:0 ';
        }
      }
      dat += '-filter_complex ';
      //先缩放修改大小
      for (int i = 0; i < datas.length; i++) {
        dat += datas[i].rotationAndScaleString();
      }
      //设置位置
      for (int i = 0; i < datas.length; i++) {
        dat += datas[i].positionString(i == 0 ? '[0:v]' : datas[i - 1].positionAlias, i == datas.length - 1);
      }
    } else {
      if (music != null && music.isNotEmpty) {
        dat += '-stream_loop -1 -i "$music" ';
        if (background.toLowerCase().endsWith('.mp4')) {
          int musicIndex = 1;
          dat += '-c:v copy -map 0:v:0 -map $musicIndex:a:0 ';
        }
      }
    }
    dat += '-vcodec libx264 ';
    dat += '-preset ultrafast ';
    dat += '-b:v 16m -bufsize 16m -maxrate 16m ';
    dat += '-s 1920x1080 ';
    dat += '-t 00:00:15 ';
    dat += '${mp4.path}';
    print('ffmpeg dat=' + dat);
    return dat;
  }
参数解释
-threads线程数
-y覆盖文件
-r 30帧率30
-loop基础图片组循环
-i获取文件
-stream_loop -1 -i "$music"音乐和视频以流的方式循环
-c:v copy -map 0:v:0 -map $musicIndex:a:0音频文件覆盖视频的音频通道
-ignore_loop 0 -i ${path}gif无限循环铺满视频
-filter_complex开始设置具体参数
[0:v][1:v]overlay=0:0[dkg1]第二个资源放在第一个资源的0,0出,并设置别名dkg1
[1:v]scale=100:100[img1];[2:v]scale=1280:720[img2]资源1缩放到100_100,设置别名img1,图片2设置成1280_720,设置别名img2
-vcodec libx264指定编码
-preset ultrafast合成速度 极快
-b:v 16m -bufsize 16m -maxrate 16m码率等参数
-s 1920x1080视频输出分辨率
-t 00:00:15视频总长度00时00分00秒
${mp4.path}输出路径