标签: ffmpeg

  • 解决 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 粗剪视频,移除静音片段

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

    后来发现 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. 识别“上一段不要”这样的语音命令,并且找到最合适的片段干掉
  • 使用 Node.js 驱动 FFMPEG  录屏

    使用 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 笔记

    截取视频

    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

    转换 iPad 支持的视频

    ffmpeg -i input.mkv -c:v libx264 -profile:v main -level 3.1 -preset medium -crf 23 -x264-params ref=4 -c:a copy -movflags +faststart output.mp4

    不知道为啥,直接 .mp4 或者 -c:v libx264 都不行,必须用上面这个。

    缩放

    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 

    提取声音

    # 不转码,只提取声音,很快
    ffmpeg -i input-video.avi -vn -acodec copy output-audio.aac
    # 转码,便于四处播放
    ffmpeg -i sample.avi -q:a 0 -map a sample.mp3