我正在尝试使用ffmpeg连接两个mp4文件。我需要这是一个自动过程,因此我选择了ffmpeg。我将这两个文件转换为.ts文件,然后将它们连接起来,然后尝试对连接的.ts文件进行编码。文件是h264和aac编码的,我希望保持质量相同或尽可能接近原始。

ffmpeg -i part1.mp4 -vcodec copy -vbsf h264_mp4toannexb -acodec copy part1.ts
ffmpeg -i part2.mp4 -vcodec copy -vbsf h264_mp4toannexb -acodec copy part2.ts
cat part1.ts part2.ts > parts.ts
ffmpeg -y -i parts.ts -acodec copy -ar 44100 -ab 96k -coder ac -vbsf h264_mp4toannexb parts.mp4

不幸的是,我在编码过程中从ffmpeg返回以下错误消息:

[h264 @ 0x1012600]sps_id out of range
[h264 @ 0x1012600]non-existing SPS 0 referenced in buffering period
[h264 @ 0x1012600]sps_id out of range
[h264 @ 0x1012600]non-existing SPS 0 referenced in buffering period
[NULL @ 0x101d600]error, non monotone timestamps 13779431 >= 13779431kbits/s    
av_interleaved_write_frame(): Error while opening file

这种情况大约发生在编码的一半,这让我认为不能将两个.ts文件合并在一起并使其正常工作。


当前回答

对于Windows Powershell

创建文件列表

PS > ls file* | % { $n = $_.name; "file '\$n'" } | out-file mylist.txt

检查创建的文件

PS > cat mylist.txt
file '/path/to/file1.mkv'
file '/path/to/file2.mkv'
file '/path/to/file3.mkv'

运行ffmpeg(不要忘记列表文件的attach./)

PS > ffmpeg -safe 0 -f concat -i ./mylist.txt -c copy file.mkv
ffmpeg version git-2020-05-15-b18fd2b Copyright (c) 2000-2020 the FFmpeg developers
...

其他回答

我最终使用了mpg作为中间格式,并且效果良好(注意,这是一个危险的示例,-qscale 0将重新编码视频…)

ffmpeg -i 1.mp4 -qscale 0 1.mpg
ffmpeg -i 2.mp4 -qscale 0 2.mpg
cat 1.mpg 2.mpg | ffmpeg -f mpeg -i - -qscale 0 -vcodec mpeg4 output.mp4

这里有一个脚本(适用于任意数量的指定文件(不仅仅是工作目录中的所有文件),没有附加文件,也适用于.mov;在macOS上测试):

#!/bin/bash

if [ $# -lt 1 ]; then
    echo "Usage: `basename $0` input_1.mp4 input_2.mp4 ... output.mp4"
    exit 0
fi

ARGS=("$@") # determine all arguments
output=${ARGS[${#ARGS[@]}-1]} # get the last argument (output file)
unset ARGS[${#ARGS[@]}-1] # drop it from the array
(for f in "${ARGS[@]}"; do echo "file '$f'"; done) | ffmpeg -protocol_whitelist file,pipe -f concat -safe 0 -i pipe: -vcodec copy -acodec copy $output

主要答案不适用于我,因为它给了我下面在“疑难解答”部分描述的错误,所以这里是我自己的答案。

如何使用ffmpeg连接或组合许多mp4视频文件

快速摘要

创建一个inputs.txt文件,其中包含要合并的所有文件路径的列表(请参见下面的示例)。然后,将以上所有视频组合成一个这样的视频:

# Option 1 (preferred): into a single mp4 video with AAC audio encoding
# and original video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c:v copy -c:a aac output.mp4

# Option 2: into a single .mkv video with original audio and video encoding
time ffmpeg -f concat -safe 0 -i inputs.txt -c copy output.mkv

结合威泽安全摄像机镜头的细节和完整示例

在Ubuntu 20.04上进行了测试,将我的一系列Wyze安全摄像头视频合并为一个视频。(我将相机的微型SD卡直接插入电脑)。

创建一个名为inputs.txt的文件,其中包含要连接的所有文件的列表。前任:文件“path/to/file1.mp4”文件“path/to/file2.mp4”文件“path/to/file3.mp4”在我的案例中,我将2022年11月13日18:31(下午6:31)至2022年12月13日19:39(下午7:39)的69分钟视频合并在一起。在怀泽安全摄像头路径中,这意味着从记录/2021113/18/31.mp4’到记录/2021111/19/39.mp4,包括在内。下面是我的完整inputs.txt文件:文件'/media/ggabriel/3339-3730/record/20221113/18/31.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/32.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/33.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/34.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/35.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/36.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/37.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/38.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/39.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/40.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/41.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/42.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/43.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/44.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/45.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/46.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/47.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/48.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/49.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/50.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/51.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/52.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/53.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/54.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/55.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/56.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/57.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/58.mp4'文件'/media/ggabriel/3339-3730/record/20221113/18/59.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/00.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/01.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/02.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/03.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/04.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/05.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/06.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/07.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/08.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/09.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/10.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/11.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/12.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/13.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/14.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/15.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/16.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/17.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/18.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/19.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/20/mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/21.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/22.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/23.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/24.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/25.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/26.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/27.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/28.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/29.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/30.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/31.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/32.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/33.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/34.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/35.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/36.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/37.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/38.mp4'文件'/media/ggabriel/3339-3730/record/20221113/19/39.mp4'将以上所有视频合并为一个:#选项1(首选):使用AAC音频编码将mp4视频转换为单个mp4视频#和原始视频编码时间ffmpeg-f concat-safe 0-i inputs.txt-c:v copy-c:aac输出.mp4#选项2:将原始音频和视频编码转换为单个.mkv视频时间ffmpeg-f concat-safe 0-i inputs.txt-c copy output.mkv

在我的例子中,最初的69个文件占用了411.1MB。下面是上面两个命令的结果:

output.mp4的制作耗时11秒,大小为380.3 MB。AAC编码的音频显然有点小。output.mkv的制作耗时8秒,大小为410.7MB。

故障排除/可能的错误

如果运行ffmpeg-f concat-i inputs.txt-c copy output.mp4并得到以下错误:[cont@0x563ca0173700]不安全的文件名'/media/gabriel/3339-370/record/2021113/18/31.mp4'inputs.txt:不允许操作……这是因为你忘了使用-safe 0。参见:https://nono.ma/says/concat-unsafe-file-name-operation-not-permittedffmpeg concat:“不安全的文件名”如果运行ffmpeg-f concat-safe 0-i inputs.txt-c copy output.mp4并得到以下错误:[mp4@0x55ced96f2100]在流#1中找不到编解码器pcm_law的标记,容器中当前不支持编解码器无法写入输出文件#0的标头(编解码器参数不正确?):参数无效…这是因为“ffmpeg不支持MP4容器中的PCM(PCM_law、PCM_s16le等)。”请参阅此处:容器和此处当前不支持编解码器。因此,请运行ffmpeg-f concat-safe 0-i inputs.txt-c:v copy-c:aaac output.mp4,将音频重新编码为aac格式。或者,运行时ffmpeg-f concat-safe 0-i inputs.txt-c copy output.mkv将其写入.mkv容器,而不是.mp4容器。

ffmpeg \
  -i input_1.mp4 \
  -i input_2.mp4 \
  -filter_complex '[0:v]pad=iw*2:ih[int];[int][1:v]overlay=W/2:0[vid]' \
  -map [vid] \
  -c:v libx264 \
  -crf 23 \
  -preset veryfast \
  output.mp4

如果您更喜欢rogerdpack答案中的方法#2,但不想使用管道(例如,您只想在C中使用execv)或不想创建额外的文件(list.txt),那么只需将concat demuxer与数据和文件协议相结合,即FFmpeg允许您像HTML一样内联输入文件:

<img src="data:image/png;base64,..." alt="" />
ffmpeg -i 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAB4AAAAQ4AQAAAADAqPzuAAABEklEQVR4Ae3BAQ0AAADCIPunfg8HDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4FT45QABPFL5RwAAAABJRU5ErkJggg==' /tmp/blackbg.mp4

下面是我的程序(放在/usr/bin/ffcont中),它自动内联“包含文件路径列表的文件”。此外,与所有其他答案不同,您可以使用任何FFmpeg选项。

如果您使用的不是bash语言(C,Node.js),那么只需查看用法()和最后一行。

#!/bin/bash
# ffconcat v0.3
# @author Arzet Ro, 2021 <arzeth0@gmail.com>
# @license CC-0 (Public Domain)

function usage ()
{
    echo "\
ffconcat's usage:
    ffconcat (anyFfmpegInputOptions) -i /tmp/a.mp4 -i ... -i ... /tmp/out.mp4 (anyFfmpegOutputOptions)
    ffconcat -vn /tmp/a.mp4 /tmp/b.opus /tmp/out.mp4 -y
    ffconcat -http -i https://a/h264@720p@25fps+opus.mp4 -i ftp://127.0.0.1/h264@720p@30fps+opus.mp4 -i /tmp/c.opus /tmp/out.mkv
    ffconcat -http -i https://a/vp9@1080p@30fps+opus.mp4 -i ftp://127.0.0.1/vp9@720p@30fps+opus.mp4 -i /tmp/c.opus /tmp/out.mp4
    WARNING: ffconcat uses `concat` demuxer; when
    using both this demuxer AND -y, FFmpeg doesn't check if
    an input file and output file
    are the same file, so your 100 GB input file
    could immediately become 10 KB.
    ffconcat checks that only when neither -i
    nor new FFmpeg release's boolean args (see valuelessFfmpegArgs in the code)
    are specified.

    ffmpeg has no -http.
    ffconcat has -http because ffmpeg automatically
    sets allowed protocols depending on -f and -i.
    But when -f concat, ffmpeg doesn't know what to do with -i.

    ffmpeg and mpv support VP9+Opus in .mp4
    Only one video codec is possible in an output file.
    You can't have both AAC and Opus in one .mp4 (not sure about other containers).
    If you combine VP9 videos, then make sure they have the same FPS.
    If you combine H264 videos of different resolutions,
    then you get A/V desync
    and also either
    1) the start of video streams after the first video stream are cut
    2) or video player freezes for 5 seconds when switching between video streams.
    Also it seems if DAR (display aspect ratio) differs (at least in H.264)
    then incorrect (5x longer) duration is estimated
    and mpv displays the second video with 1 FPS.
    You can see the info about an input file
    with
    mediainfo file.mp4
    or
    ffprobe -hide_banner -protocol_whitelist file,rtp,udp -show_streams file.mp4"
}

# Configuration [begin]
forceRequireIArgumentForInputFiles=0
# Configuration [end]




in_array ()
{
    local e match="$1"
    shift
    for e; do [[ "$e" == "$match" ]] && return 0; done
    return 1
}

if [[ "$#" == 0 ]]
then
    usage
    exit
fi

requireIArgumentForInputFiles=0
if in_array "--help" "$@"
then
    usage
    exit
elif in_array "-help" "$@"
then
    usage
    exit
elif in_array "-i" "$@"
then
    requireIArgumentForInputFiles=1
elif [[ "$forceRequireIArgumentForInputFiles" == "1" ]]
then
    >&2 echo "forceRequireIArgumentForInputFiles=1, so you need -i"
    usage
    exit 1
fi




NL=$'\n'
inputOptions=()
outputOptions=()
inputFilesRawArray=()
outputFile=""

declare -a valuelessFfmpegArgs=("-http"     "-hide_banner" "-dn" "-n" "-y" "-vn" "-an" "-autorotate" "-noautorotate" "-autoscale" "-noautoscale" "-stats" "-nostats" "-stdin" "-nostdin" "-ilme" "-vstats" "-psnr" "-qphist" "-hwaccels" "-sn" "-fix_sub_duration" "-ignore_unknown" "-copy_unknown" "-benchmark" "-benchmark_all" "-dump" "-hex" "-re" "-copyts" "-start_at_zero" "-shortest" "-accurate_seek" "-noaccurate_seek" "-seek_timestamp"     "write_id3v2" "write_apetag" "write_mpeg2" "ignore_loop" "skip_rate_check" "no_resync_search" "export_xmp")
#^ used when requireIArgumentForInputFiles=0
# TODO: fill all the args
# grep -C 3 AV_OPT_TYPE_BOOL libavformat/ libavcodec/
# grep -C 3 OPT_BOOL fftools/
# Unfortunately, unlike MPV, FFmpeg neither
# requires nor supports `=`, i.e. `--y --i=file.mp4'
# instead of `-y -i file.mp4`.
# Which means it's unclear whether an argument
# is a value of an argument or an input/output file.

areFfmpegArgsAllowed=1
isHttpMode=0

if in_array "-http" "$@"
then
    isHttpMode=1
fi


# if an argument is not a boolean argument, then what key needs a value
secondArgumentIsWantedByThisFirstArgument=""
# if requireIArgumentForInputFiles=1
# then secondArgumentIsWantedByThisFirstArgument must be either "" or "-i"
isCurrentArgumentI=0
localRawFilesArray=()
outputFile=""
for arg in "$@"
do
    if [[
        "$secondArgumentIsWantedByThisFirstArgument" == ""
        &&
        "$arg" == "-http"
    ]]
    then
        continue
    fi
    if [[ "$arg" == "--" ]]
    then
        areFfmpegArgsAllowed=0
        continue
    fi
    if [[
        (
            "$areFfmpegArgsAllowed" == "1"
            ||
            "$secondArgumentIsWantedByThisFirstArgument" != ""
        )
        && !(
            "$requireIArgumentForInputFiles" == "1"
            &&
            "$secondArgumentIsWantedByThisFirstArgument" == "-i"
        )
        &&
        (
            "$secondArgumentIsWantedByThisFirstArgument" != ""
            ||
            (
                "$requireIArgumentForInputFiles" == "0"
                &&
                "$arg" = -*
            )
            ||
            (
                "$requireIArgumentForInputFiles" == "1"
            )
        )
    ]]
    then
        if [[ !(
            "$requireIArgumentForInputFiles" == "1"
            &&
            "$arg" == "-i"
        ) ]]
        then
            if (( ${#inputFilesRawArray[@]} == 0 ))
            then
                inputOptions+=("$arg")
            else
                outputOptions+=("$arg")
            fi
        fi
    elif [[
        "$requireIArgumentForInputFiles" == "0"
        ||
        "$secondArgumentIsWantedByThisFirstArgument" == "-i"
    ]]
    then
        if echo -n "$arg" | egrep '^(https?|ftp)://'
        then
            inputFilesRawArray+=("$arg")
            localRawFilesArray+=("$arg")
        else
            tmp=`echo -n "$arg" | sed 's@^file:@@'`
            localRawFilesArray+=("$tmp")
            if [[ "$secondArgumentIsWantedByThisFirstArgument" == "-i" ]]
            then
                if ! ls -1d -- "$tmp" >/dev/null 2>/dev/null
                then
                    >&2 echo "Input file '$tmp' not found"
                    exit 1
                fi
            fi
            tmp=`echo -n "$tmp" | sed -E 's@(\s|\\\\)@\\\\\1@g' | sed "s@'@\\\\\'@g"`
            # ^ FIXME: does it work for all filenames?
            inputFilesRawArray+=("file:$tmp")
        fi
    elif [[
        "$requireIArgumentForInputFiles" == "1"
        &&
        "$secondArgumentIsWantedByThisFirstArgument" != "-i"
    ]]
    then
        if echo -n "$arg" | egrep '^(https?|ftp)://'
        then
            outputFile="$arg"
        else
            outputFile=`echo -n "$arg" | sed 's@^file:@@'`
            outputFile="file:$outputFile"
        fi
    else
        usage
        exit 1
    fi
    if [[
        "$secondArgumentIsWantedByThisFirstArgument" != ""
        ||
        "$areFfmpegArgsAllowed" == "0"
    ]]
    then
        secondArgumentIsWantedByThisFirstArgument=""
    else
        if [[ "$requireIArgumentForInputFiles" == "1" && "$arg" == "-i" ]]
        then
            secondArgumentIsWantedByThisFirstArgument="$arg"
        elif [[ "$requireIArgumentForInputFiles" == "0" && "$arg" = -* ]]
        then
            if ! in_array "$arg" ${valuelessFfmpegArgs[@]}
            then
                secondArgumentIsWantedByThisFirstArgument="$arg"
            fi
        fi
    fi
done
if [[
    "$requireIArgumentForInputFiles" == "0"
    &&
    "$outputFile" == ""
]]
then
    outputFile="${localRawFilesArray[((${#localRawFilesArray[@]}-1))]}"
fi
actualOutputFile="$outputFile"
if [[ "$requireIArgumentForInputFiles" == "0" || "file:" =~ ^"$outputFile"* ]]
then
    actualOutputFile=`echo -n "$actualOutputFile" | sed 's@^file:@@'`
    actualOutputFile=`readlink -nf -- "$actualOutputFile"`
fi

if [[ "$requireIArgumentForInputFiles" == "0" ]]
then
    unset 'inputFilesRawArray[((${#inputFilesRawArray[@]}-1))]'
    unset 'localRawFilesArray[((${#localRawFilesArray[@]}-1))]'
    outputOptions+=("$outputFile")
fi

#>&2 echo Input: ${inputFilesRawArray[@]}
#if [[ "$requireIArgumentForInputFiles" == "0" ]]
#then
#   >&2 echo Output: $outputFile
#fi


if (( ${#inputFilesRawArray[@]} < 2 ))
then
    >&2 echo "Error: Minimum 2 input files required, ${#inputFilesRawArray[@]} given."
    >&2 echo Input: ${inputFilesRawArray[@]}
    if [[ "$requireIArgumentForInputFiles" == "0" ]]
    then
        >&2 echo Output: $outputFile
    fi
    usage
    #outputFile=/dev/null
    exit 1
fi
if [[
    "$requireIArgumentForInputFiles" == "0"
    &&
    "$outputFile" == ""
]]
then
    >&2 echo "Error: No output file specified."
    usage
    exit 1
fi


ffmpegInputList=""
firstFileDone=0
inputFilesRawArrayLength=${#inputFilesRawArray[@]}

for (( i = 0; i < inputFilesRawArrayLength; i++ ))
do
    lf="${localRawFilesArray[$i]}"
    f="${inputFilesRawArray[$i]}"
    if [[ "${inputFilesRawArray[$i]}" =~ ^file: ]]
    then
        actualF=`readlink -nf -- "$lf"`
        if [[ "$actualF" == "$actualOutputFile" ]]
        then
            >&2 echo "Error: The same file '$actualF' is used both as an input file and an output file"
            exit 1
        fi
    fi
    if [[ "$firstFileDone" == "1" ]]
    then
        ffmpegInputList+="$NL"
    fi
    ffmpegInputList+="file $f"
    firstFileDone=1
done

protocol_whitelist_appendage=""
if [[ "$isHttpMode" == "1" ]]
then
    protocol_whitelist_appendage=",ftp,http,https"
fi


# Also print the final line:
set -x

ffmpeg \
-safe 0 \
-f concat \
-protocol_whitelist data,file"$protocol_whitelist_appendage" \
"${inputOptions[@]}" \
-i "data:text/plain;charset=UTF-8,${ffmpegInputList}" \
-c copy \
"${outputOptions[@]}"
# $ffmpegInputList is
# file file:./test.mp4\nfile file:/home/aaa.mp4\nfile http://a/b.aac
# All whitespace and ' in ffmpegInputList are escaped with `\`.

与HTML不同,-i中不需要百分比编码(JavaScript的encodeURI/encodeURIComponent)(%20等)。