分类
ffmpeg

解决 FFMPEG 合并视频没有声音的问题

FFMPEG 是个非常强大的视频工具,操作音视频必备。

使用 FFMPEG 合并音视频很简单,只需要把待合并的文件名都放在列表文件里,然后使用 -f concat 连接即可。

列表文件是纯文本,格式如下:

# 可以用 # 写注释
file '/path/to/file1.mp4'
file '/path/to/file2.mp4'
....

然后调用命令,其中,files.txt 就是列表文件:

ffmpeg -f concat -safe 0 -i files.txt -c copy -y output.mp4

解决没有声音的问题

这些只是基础,实际应用要复杂的多。比如今天遇到一个问题,合并后的视频没有声音。

第一步,很快搜到 Concatenating videos with ffmpeg produces silent video when the first video has no audio track 这个答案。按照里面的说法,因为第一个视频(即封面视频)没有声音,所以后面就都没有声音。解决方案是,给第一个视频添加一条空白音轨,这样就不影响其它视频的声音了。

答案里的方法是:

ffmpeg -i INPUT -f lavfi -i aevalsrc=0 -shortest -y OUTPUT

试了一下不好使,而且合并时大量报错:

[aac @ 0x55e495967e80] decode_band_types: Input buffer exhausted before END element found
Error while decoding stream #0:1: Invalid data found when processing input

仔细阅读答案的评论,有位同学说:

For the silent audio make sure to match the channel layout and sample rate of the other inputs. For example, 44.1kHz stereo audio: -i anullsrc=cl=stereo:r=44100

生成空白音轨的时候,要确保声道和采样率与其它输入视频一致。比如,44.1KHz 立体声:-i anullsrc=cl=stereo:r=44100

似乎有点关系。我立刻使用 ffprobe 查看源视频的信息:

ffprobe -i input.mp4 -show_streams -select_streams a -loglevel error

得到:

sample_rate=24000
channels=2
channel_layout=stereo

于是,我修改前面的空白音轨生成方式,这次终于成功了:

ffmpeg -i input.mp4 -f lavfi -i anullsrc=cl=stereo:r=24000 -shortest -y input-new.mp4

一点推测

  • FFMPEG 会以第一个视频为基底,往上合并其它视频,所以第一个视频的音轨就很重要
  • 合并视频前最好先进行转码,然后 -c copy 就好了。

其它 FFMPEG 操作笔记在:FFMPEG 笔记

参考资料:

分类
工具

使用 jumpcutter 粗剪视频,移除静音片段

今年打算在直播和视频上做点努力,所以通过直播录了不少视频,周末想剪一剪,但是一方面我不擅长剪视频,另一方面也没有合适的工具,所以颇费了一些周折。

后来发现 jumpcutter,按照其作者的设计,它可以识别视频中的 👍 和 👎,只保留 👍 的片段,剪掉 👎 的片段;并且可以自动剪掉没有声音的部分,这样视频可以很快完成初剪。

前面的功能很炫酷,不过对于我来说不太实用,后面的功能比较有价值,于是我试用了一下,记录过程如下:

# clone 代码到本地
git clone git@github.com:carykh/jumpcutter.git

# 安装依赖,位于 requirements.txt
pip3 install Pillow audiotsm scipy numpy pytube

# 然后就可以使用了
python3.9 jumpcutter.py --input_file left.mp4 --output_file ./1.mp4 --silent_threshold 0.06 --silent_speed 99

其它参数如下:

--input_file目标视频
--urlYouTube 视频,国内用户用途不大
--output_file输出视频
--silent_threshold静音阈值,即多小的声音可以认为是有声音的(浮点数,0~1,默认 0.03)
--sounded_speed有声音的片段,以怎样的速率播放(浮点数,默认 1)
--silent_speed静音的片段,以怎样的速度播放(浮点数,默认5,即 5 倍速度)
--frame_margin在有声音的片段两边保留多少间隔(默认1)
--sample_rate声音取样率
--frame_rate帧数
--frame_quality质量
参数列表

比如前面我用的命令,就是认为小于 0.06 的声音为静音,静音播放速率 99(基本上直接跳过了)。

我处理了两段视频,感兴趣的同学可以试一试:

接下来我还想做两个功能,有了这两个功能视频处理后基本就能满足我的需求了:

  1. 找到“嗯”、“哦”、“那个”等无意义的语气助词,把它们干掉
  2. 识别“上一段不要”这样的语音命令,并且找到最合适的片段干掉
分类
nodejs

使用 Node.js 驱动 FFMPEG 录屏

FFMPEG 功能非常强大,不仅能转换视频格式、压缩视频、添加字幕等,还能录制屏幕内容。使用 FFMPEG 录屏的方法很简单:

# Linux
ffmpeg -video_size 1024x768 -framerate 25 -f x11grab -i :0.0+100,200 output.mp4 

# macOS
ffmpeg -f avfoundation -list_devices true -i "" 

# Windows
ffmpeg -f gdigrab -framerate 30 -offset_x 10 -offset_y 20 -video_size 640x480 -show_region 1 -i desktop output.mkv 

更详细的介绍可以参考官方文档,这里不做太多摘抄。

使用 Node.js child_process 可以调用外部程序执行操作,详细的用法可以参考官方文档。大概来说,分为:

  1. exec/execFile 调用程序,当执行完成,一次性获得结果
  2. spawn 调用程序,过程中流式获得输出

我的录屏对时间点有非常高的要求,力求准确。所以我只能选择 spawn,然后通过检查日志输出得到准确的录制开始时间。

所以,我的代码大概如此:

class FfmpegRecorder {
  // 其它函数略去
  startRecording(args) {
    this.isFileReady = new Promise((resolve, reject) => {
      this._isFileReadyResolve = resolve;
      this._isFileReadyReject = reject;
    });
    this.recording = spawn(this.ffmpeg, args);
    
    this.recording.stdout.on('data', this.onStdData);
    this.recording.stderr.on('data', this.onStdErr);
  }
  stopRecording() {
    this.recording.kill('SIGINT');
    const path = resolve(process.cwd(), 'a.mp4');
    return path;
  }

  onStdData(data) {
    data = data.toString();
    this.stdout += data;
    if (this.stdout.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
      this._isFileReadyResolve();
      this._isFileReadyReject = null;
    }
  }
  onStdErr(data) {
    data = data.toString();
    this.stderr += data;
    if (this.stderr.indexOf('Output #0, mp4, to \'a.mp4\'') !== -1) {
      this._isFileReadyResolve();
      this._isFileReadyReject = null;
    }
  }
}

根据我在命令行里直接跑 ffmpeg 的结果,它会先初始化,然后开始录屏。录屏开始时,会输出 Output #0, mp4, to xxx.mp4 这样的日志,所以我就会反复检查 stdoutstderr,直到关键日志出现,然后告诉外面的程序开始录制了。

这里比较奇怪的是,日志输出应该是正常的,走 stdout 通道,结果只能从 stderr 通道获取。我为防万一,两边都留下了同样的代码。可能我对 Linux 理解不够,将来再研究一下为什么会这样吧。

上面的代码忽略了 onErroronExit 部分,有兴趣的同学请等我开源(公司代码)。

在 Linux,stopRecording(),即 kill('SIGINT')(相当于按下 Ctrl+C)之后,FFMPEG 会终止录屏,并且生成可以播放的视频文件。但是在 Windows,只会留下一个无法播放的文件。

通过观察命令行直接运行 ffmpeg 的结果和 node.js 保存的结果,我发现缺失了结束录制后处理视频文件的部分。实际上 FFMPEG 录屏时只会记录视频内容,录制结束后才会生成影片的meta 信息,而播放器必须读取后者才可以正常播放。所以我猜测在 Windows 下 kill('SIGINT') 会直接彻底杀死进程,而不仅仅是发送一个信号给 FFMPEG,并让它完成后续的工作。

做出判断后,我尝试用按下 q 的方式通知 FFMPEG 停止工作,并等待 5s。果然成功,于是我构建了新类,继承前面的 FfmpegRecorder,大概代码如下:

const endTag = /kb\/s:\d+.\d{2}/m;

class WindowsRecorder extend FfmpegRecorder {
  stopRecording() {
    // 向 child process 输入 `q`
    this.recording.stdin.setEncoding('utf8');
    this.recording.stdin.write('q');
    const p = new Promise((resolve, reject) => {
      this._stopResolve = resolve;
      this._stopReject = reject;
    });
    // 设置一个超时保护 15s
    setTimeout(() => {
      this._stopReject();
    }, 15E3);
    return p;
  }
  onStdData(data) {
    super.onStdData(data);
    if (this._stopResolve && endTag.test(this.stdout)) {
      const path = resolve(process.cwd(), 'a.mp4');
      this._stopResolve(path);
    }
  }
  onStdErr(data) {
    super.onStdErr(data);
    if (this._stopResolve && endTag.test(this.stderr)) {
      const path = resolve(process.cwd(), 'a.mp4');
      this._stopResolve(path);
    }
  }
}

为了功能完善,我没有选择等待固定的时间,而是继续检查日志,直到发现 endTag 标记。另外,我也留下了 15s 超时保护,避免某些暂时没遇到的问题破坏功能。

至此,功能基本稳定完成。

分类
工具

FFMPEG 笔记

截取视频

ffmpeg -i abc.mp4 -ss 3:13 -to 2:41:13 -c copy output.mp4
  • -i 输入文件
  • -ss 开始时间
  • -to 结束时间
  • -c copy 直接截取

这样的截取方式,如果源视频的关键帧间隙很大,可能出现因缺失第一个关键帧导致的黑屏。此时,可以考虑用 -c:v libx264 之类的参数重新编码。

合并

ffmpeg -f concat -safe 0 -i files.txt -c copy -y output.wav

其中,files.txt 是所有待合并的文件,以以下的形式记录:

file /path/to/wav/1.wav
file /path/to/wav/2.wav
....

转换格式

转换格式很简单,有输入有输出,ffmpeg 会根据它们的扩展名自动选择合适的编码器,生成通用性最好的目标文件。比如 wav 2 mp3:

ffmpeg -i a.wav a.mp3

如果需要截取或者使用特定的编码器,那么就按照一般的用法添加参数即可,比如 flv 2 mp4:

ffmpeg -i 1.flv -c:v libx264 -crf 19 -strict experimental 1.mp4

其中,-c:v 是“视频编码器”的意思,音频编码器就是 -c:acrf 是质量,最小越好,取值范围是 18 -28。类似的,rm 2 MP4:

ffmpeg -i ss.rm -c:v libx264 -c:a aac -b:a 32k -strict experimental ss.avi

在 Ubuntu 下不能使用 libfaac,只能使用 aac。还要调整级别,-strict -2 不行,必须是 -strict experimental

缩放

ffmpeg -i input.mp4 -vf scale=320:-1 -strict -2 output.mp4

好吧,这次 -strict -2 好使了。如果报错,可以试着把 -1 改成 -2

裁剪画面

ffmpeg -i input.mp4 -filter:v "crop=500:1080:1420:0" output.mp4

crop 的参数为:宽、高、x、y。

调整声音

有些视频声音太小,需要调整一下:

ffmpeg -i input.mp4 -filter:a "volume=N" output.mp4

其中,N 可以是百分比,比如 1.0(一倍,不变),2.0(两倍);或者是加减的分贝,比如 10dB(增加10分贝),-20dB(减少20分贝)。不过根据我实地测试,调整后的视频的平均音量并不完全是调整的分贝。

获取平均音量:

ffmpeg -i input.mp4 -filter:a volumedetect -f null /dev/null