FFmpeg 是视频处理最常用的开源软件。

它功能强大,用途广泛,大量用于视频网站和商业软件(比如 Youtube 和 iTunes),也是许多音频和视频格式的标准编码/解码实现。

官方网站:https://www.ffmpeg.org/

安装

最简单的方法就是用包管理工具如:apt 安装:

apt update
apt install ffmpeg

或者也可以从源码安装,可以参考我之前的教程:https://blog.niekun.net/archives/891.html

常用指令

查看 ffmpeg 版本:

ffmpeg -version

查看支持的编码格式:如 h.264, h.265

ffmpeg -codecs

查看支持的容器:如 mp4, mp3, mkv

ffmpeg -formats

查看已安装的编码器:如 libx264, libx265, libvpx, aac

ffmpeg -encoders

使用格式

FFmpeg 的命令行参数非常多,可以分成五个部分。

ffmpeg {1} {2} -i {3} {4} {5}

上面命令中,五个部分的参数依次如下:

全局参数
输入文件参数
输入文件
输出文件参数
输出文件

参数太多的时候,为了便于查看,ffmpeg 命令可以写成多行:

$ ffmpeg \
[全局参数] \
[输入文件参数] \
-i [输入文件] \
[输出文件参数] \
[输出文件]

下面是一个例子:

ffmpeg \
-y \ # 全局参数
-c:a libfdk_aac -c:v libx264 \ # 输入文件参数
-i input.mp4 \ # 输入文件
-c:v libvpx-vp9 -c:a libvorbis \ # 输出文件参数
output.webm # 输出文件

上面的命令将 mp4 文件转成 webm 文件,这两个都是容器格式。输入的 mp4 文件的音频编码格式是 aac,视频编码格式是 H.264;输出的 webm 文件的视频编码格式是 VP9,音频格式是 Vorbis。

如果不指明编码格式,FFmpeg 会自己判断输入文件的编码。一般可以省略输入文件参数。

常用命令参数

-c:指定编码器
-c copy:直接复制,不经过重新编码(这样比较快)
-c:v:指定视频编码器
-c:a:指定音频编码器
-i:指定输入文件
-an:去除音频流
-vn: 去除视频流
-preset:指定输出的视频质量,会影响文件的生成速度,有以下几个可用的值 ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow。
-y:不经过确认,输出时直接覆盖同名文件。

常规使用方法

查看元数据信息,如时长,比特率等:

ffmpeg -i test.mp4

输出的信息较多,可以通过 -hide_banner 只显示媒体文件信息:

ffmpeg -i test.mp4 -hide_banner

转码,如 avi to h.264:

ffmpeg -i test.avi -c:v libx264 test.mp4

转换容器:

ffmpeg -i test.mp4 -c copy test.webm

转换容器不需要转码,所以直接 copy 即可。

转换码率,转换成固定码率:

ffmpeg -i test.mp4 -b:v 500k test_out.mp4

转换码率,转换成一个码率范围:

ffmpeg -i test.mp4 -minrate 964K -maxrate 3856K -bufsize 2000K test_out.mp4

改变分辨率:转换成 480p

ffmpeg \
-i input.mp4 \
-vf scale=480:-1 \
output.mp4

视频中提取音频:

ffmpeg \
-i input.mp4 \
-vn -c:a copy \
output.aac

上面例子中,-vn 表示去掉视频,-c:a copy 表示不改变音频编码,直接拷贝。

视频截图:下面的例子是从指定时间开始,连续对1秒钟的视频进行截图

ffmpeg \
-y \
-i input.mp4 \
-ss 00:01:24 -t 00:00:01 \
output_%3d.jpg

%3d 在 shell 里表示至少输出3个字符空间的数字:

% means "Print a variable here"
3 means "use at least 3 spaces to display, padding as needed"
d means "The variable will be an integer"

如果只需要截一张图,可以指定只截取一帧。

$ ffmpeg \
-ss 01:23:45 \
-i input \
-vframes 1 -q:v 2 \
output.jpg

上面例子中,-vframes 1 指定只截取一帧,-q:v 2 表示输出的图片质量,一般是1到5之间(1 为质量最高)。

裁剪:
裁剪(cutting)指的是,截取原始视频里面的一个片段,输出为一个新视频。可以指定开始时间(start)和持续时间(duration),也可以指定结束时间(end)。

$ ffmpeg -ss [start] -i [input] -t [duration] -c copy [output]
$ ffmpeg -ss [start] -i [input] -to [end] -c copy [output]

下面是实际的例子。

# 从1分50秒开始截取10.5秒
ffmpeg -ss 00:01:50 -i test.mp4 -t 10.5 -c copy out.mp4

# 从25秒开始截取10秒
ffmpeg -ss 25 -i test.mp4 -to 10 -c copy out.mp4
ffmpeg -i test.mp4 -ss 25 -to 10 -c copy out.mp4

上面例子中,-c copy 表示不改变音频和视频的编码格式,直接拷贝,这样会快很多。

高级用法

压缩视频内容到指定容量大小

使用的技术主要是 ffmpeg 的 2 pass 方法和 ffprobe 得到码率和时长信息。

bash脚本:

#!/bin/bash

target_video_size_MB="$2"
origin_duration_s=$(ffprobe -v error -show_streams -select_streams a "$1" | grep -Po "(?<=^duration\=)\d*\.\d*")
origin_audio_bitrate_kbit_s=$(ffprobe -v error -pretty -show_streams -select_streams a "$1" | grep -Po "(?<=^bit_rate\=)\d*\.\d*")
target_audio_bitrate_kbit_s=$origin_audio_bitrate_kbit_s # TODO for now, make audio bitrate the same
target_video_bitrate_kbit_s=$(\
    awk \
    -v size="$target_video_size_MB" \
    -v duration="$origin_duration_s" \
    -v audio_rate="$target_audio_bitrate_kbit_s" \
    'BEGIN { print  ( ( size * 8192.0 ) / ( 1.048576 * duration ) - audio_rate ) }')

ffmpeg \
    -y \
    -i "$1" \
    -c:v libx264 \
    -b:v "$target_video_bitrate_kbit_s"k \
    -pass 1 \
    -an \
    -f mp4 \
    /dev/null \
&& \
ffmpeg \
    -i "$1" \
    -c:v libx264 \
    -b:v "$target_video_bitrate_kbit_s"k \
    -pass 2 \
    -c:a aac \
    -b:a "$target_audio_bitrate_kbit_s"k \
    "${1%.*}-$2mB.mp4"

使用方法:压缩视频到 50 MB 大小

./script.sh test.mp4 50

切割视频到指定时长的多个视频

使用的技术主要是 python,ffprobe 得到视频时长,然后计算需要切割为几个视频。
python 脚本:

#!/usr/bin/env python

import csv
import subprocess
import math
import json
import os
import shlex
from optparse import OptionParser


def split_by_manifest(filename, manifest, vcodec="copy", acodec="copy",
                      extra="", **kwargs):

    if not os.path.exists(manifest):
        print("File does not exist: %s" % manifest)
        raise SystemExit

    with open(manifest) as manifest_file:
        manifest_type = manifest.split(".")[-1]
        if manifest_type == "json":
            config = json.load(manifest_file)
        elif manifest_type == "csv":
            config = csv.DictReader(manifest_file)
        else:
            print("Format not supported. File must be a csv or json file")
            raise SystemExit

        split_cmd = ["ffmpeg", "-i", filename, "-vcodec", vcodec,
                     "-acodec", acodec, "-y"] + shlex.split(extra)
        try:
            fileext = filename.split(".")[-1]
        except IndexError as e:
            raise IndexError("No . in filename. Error: " + str(e))
        for video_config in config:
            split_str = ""
            split_args = []
            try:
                split_start = video_config["start_time"]
                split_length = video_config.get("end_time", None)
                if not split_length:
                    split_length = video_config["length"]
                filebase = video_config["rename_to"]
                if fileext in filebase:
                    filebase = ".".join(filebase.split(".")[:-1])

                split_args += ["-ss", str(split_start), "-t",
                               str(split_length), filebase + "." + fileext]
                print("########################################################")
                print("About to run: "+" ".join(split_cmd+split_args))
                print("########################################################")
                subprocess.check_output(split_cmd+split_args)
            except KeyError as e:
                print("############# Incorrect format ##############")
                if manifest_type == "json":
                    print("The format of each json array should be:")
                    print("{start_time: <int>, length: <int>, rename_to: <string>}")
                elif manifest_type == "csv":
                    print("start_time,length,rename_to should be the first line ")
                    print("in the csv file.")
                print("#############################################")
                print(e)
                raise SystemExit


def get_video_length(filename):

    output = subprocess.check_output(("ffprobe", "-v", "error", "-show_entries",
                                      "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)).strip()
    video_length = int(float(output))
    print("Video length in seconds: "+str(video_length))

    return video_length


def ceildiv(a, b):
    return int(math.ceil(a / float(b)))


def split_by_seconds(filename, split_length, vcodec="copy", acodec="copy",
                     extra="", video_length=None, **kwargs):
    if split_length and split_length <= 0:
        print("Split length can't be 0")
        raise SystemExit

    if not video_length:
        video_length = get_video_length(filename)
    split_count = ceildiv(video_length, split_length)
    if(split_count == 1):
        print("Video length is less then the target split length.")
        raise SystemExit

    split_cmd = ["ffmpeg", "-i", filename, "-vcodec",
                 vcodec, "-acodec", acodec] + shlex.split(extra)
    try:
        filebase = ".".join(filename.split(".")[:-1])
        fileext = filename.split(".")[-1]
    except IndexError as e:
        raise IndexError("No . in filename. Error: " + str(e))
    for n in range(0, split_count):
        split_args = []
        if n == 0:
            split_start = 0
        else:
            split_start = split_length * n

        split_args += ["-ss", str(split_start), "-t", str(split_length),
                       filebase + "-" + str(n+1) + "-of-" +
                       str(split_count) + "." + fileext]
        print("About to run: "+" ".join(split_cmd+split_args))
        subprocess.check_output(split_cmd+split_args)


def main():
    parser = OptionParser()

    parser.add_option("-f", "--file",
                      dest="filename",
                      help="File to split, for example sample.avi",
                      type="string",
                      action="store"
                      )
    parser.add_option("-s", "--split-size",
                      dest="split_length",
                      help="Split or chunk size in seconds, for example 10",
                      type="int",
                      action="store"
                      )
    parser.add_option("-c", "--split-chunks",
                      dest="split_chunks",
                      help="Number of chunks to split to",
                      type="int",
                      action="store"
                      )
    parser.add_option("-S", "--split-filesize",
                      dest="split_filesize",
                      help="Split or chunk size in bytes (approximate)",
                      type="int",
                      action="store"
                      )
    parser.add_option("--filesize-factor",
                      dest="filesize_factor",
                      help="with --split-filesize, use this factor in time to"
                      " size heuristics [default: %default]",
                      type="float",
                      action="store",
                      default=0.95
                      )
    parser.add_option("--chunk-strategy",
                      dest="chunk_strategy",
                      help="with --split-filesize, allocate chunks according to"
                      " given strategy (eager or even)",
                      type="choice",
                      action="store",
                      choices=['eager', 'even'],
                      default='eager'
                      )
    parser.add_option("-m", "--manifest",
                      dest="manifest",
                      help="Split video based on a json manifest file. ",
                      type="string",
                      action="store"
                      )
    parser.add_option("-v", "--vcodec",
                      dest="vcodec",
                      help="Video codec to use. ",
                      type="string",
                      default="copy",
                      action="store"
                      )
    parser.add_option("-a", "--acodec",
                      dest="acodec",
                      help="Audio codec to use. ",
                      type="string",
                      default="copy",
                      action="store"
                      )
    parser.add_option("-e", "--extra",
                      dest="extra",
                      help="Extra options for ffmpeg, e.g. '-e -threads 8'. ",
                      type="string",
                      default="",
                      action="store"
                      )
    (options, args) = parser.parse_args()

    def bailout():
        parser.print_help()
        raise SystemExit

    if not options.filename:
        bailout()

    if options.manifest:
        split_by_manifest(**(options.__dict__))
    else:
        video_length = None
        if not options.split_length:
            video_length = get_video_length(options.filename)
            file_size = os.stat(options.filename).st_size
            split_filesize = None
            if options.split_filesize:
                split_filesize = int(
                    options.split_filesize * options.filesize_factor)
            if split_filesize and options.chunk_strategy == 'even':
                options.split_chunks = ceildiv(file_size, split_filesize)
            if options.split_chunks:
                options.split_length = ceildiv(
                    video_length, options.split_chunks)
            if not options.split_length and split_filesize:
                options.split_length = int(
                    split_filesize / float(file_size) * video_length)
        if not options.split_length:
            bailout()
        split_by_seconds(video_length=video_length, **(options.__dict__))


if __name__ == '__main__':
    main()

使用方法:将视频切割为单个视频100秒

./split.py -f test.mp4 -s 100

ffprobe 使用

ffprobe 可以用来得到视频信息。

视频时长:秒

ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 input.mp4

视频码率:bit

ffprobe -v error -show_entries format=bit_rate -of default=noprint_wrappers=1:nokey=1 input.mp4

参考链接

http://www.ruanyifeng.com/blog/2020/01/ffmpeg.html
https://stackoverflow.com/questions/29082422/ffmpeg-video-compression-specific-file-size
https://github.com/c0decracker/video-splitter
https://trac.ffmpeg.org/wiki/FFprobeTips

标签:无

你的评论