• タイトル: あるいは: ffmpegで大量の中間 png files を作らずにimagemagickなどのコマンドによる静止画単位での加工をする方法。
  • ffmpegfifoを入力や出力に使う場合に注意しなければならないこと。

このページで説明することは

  • ffmpegで無限に入力して、無限に出力する映像をimagemagickなどで加工する
  • ffmpegに任意のフレーム補間コマンドを噛ませる

などに応用できるかも知れません。

このページで説明する方法ではUnix互換環境を想定している。
WSL(Windows Subsystem for Linux)やgit bashでは、しばしばfifoが期待通りの動きをしなかったという噂あり。
Windowsでの動作確認はしていない。

はじめに

ffmpegには実行時に custom video filter を追加する方法はありません(ffmpeg ver 5.1.1時点)。

コードを書いて、自分でビルドする必要あり。
https://github.com/FFmpeg/FFmpeg/blob/master/doc/writing_filters.txt

imagemagickなどのコマンドを使って動画の全フレームを加工したい場合は

  • 全フレームをpngに書き出す
  • pngを加工する
  • ffmpegで再び動画に戻す

大量の一時pngが作られることになる。
なんとなくそれがいやだから回避する。

出来る人にとっては簡単な問題なせいか、2023-03-07時点では随時変換の実装例がない。

概要

以下3つを並列実行する。

  • step 1:
    • ffmpeginput.mp4rawvideoとしてfifoAへ書き込み
  • step 2:
    • fifoAを読み込みとしてopen
    • ループ
      • ffmpegfifoAから1フレーム分だけ読み取ってpng
      • もしpng0サイズならループ終了
      • pngを加工する
      • ffmpegpngrawvideoとしてfifoBへ書き込み
    • fifoAclose
  • step 3:
    • ffmpegfifoBoutput.mp4

実装例

実装例を示すためのコードであるため実際に使うなら改変が必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env bash

#
# bash -version: GNU bash, version 5.2.2(1)-release (x86_64-pc-linux-gnu)
#
# ffmpeg version 5.1.1-1ubuntu1 Copyright (c) 2000-2022 the FFmpeg developers
#
# convert -version: Version: ImageMagick 6.9.11-60 Q16 x86_64 2021-01-25 https://imagemagick.org
#
# mkfifo (GNU coreutils) 8.32
# mktemp (GNU coreutils) 8.32
#

function main() {
    ## force_clean

    filter_each_frames 'input.mp4'  'output.mp4'
}


function filter_image() {
    local dest="$(mktemp --suffix=.ffmpeg.png)"
    # convert command of imagemagick
    convert "$1" -liquid-rescale '200x150!'  "${dest}" &&
        mv "${dest}" "$1"
}


function force_clean() {
    ps xa | grep -e ' ffmpeg.*-nostdin\| cat.*ffmpeg' | sed -e '/ grep /d'

    kill -9 $(ps xa |
                  grep -e ' ffmpeg.*-nostdin\| cat.*ffmpeg' |
                  sed -e '/ grep /d' |
                  cut -d ' ' -f 1)

    rm -rf /tmp/tmp.*.ffmpeg*
}


function get_video_whrp() {
    # whrp: Width, Height, (frame)Rate, Pixel-format
    
    ((local != 0)) && echo "local ${prefix}width ${prefix}height ${prefix}r_frame_rate ${prefix}pix_fmt ${prefix}nb_read_frames ${prefix}exit_code"

    if [[ $1 == *.png || $1 == *.jpg  ]]; then
        {
            ffprobe -select_streams v:0  -v error  -show_entries stream=width,height,pix_fmt \
                    -of default=noprint_wrappers=1  -i "$1"
            local exit_code=$?
            echo exit_code=${exit_code}
        } | sed -e s/^/"${prefix}"/
    else
        {
            ffprobe -select_streams v:0  -v error  -count_frames \
                    -show_entries stream=width,height,r_frame_rate,pix_fmt,nb_read_frames \
                    -of default=noprint_wrappers=1 -i "$1"
            local exit_code=$?
            echo exit_code=${exit_code}
        } | sed -e s/^/"${prefix}"/
    fi
}


function make_temp_fifo() {
    local temp="$(mktemp --dry-run --suffix=.ffmpeg)"
    mkfifo -m 600  "${temp}"  &&  echo "${temp}"
}


function filter_each_frames() {

    local input="$1"
    local output="$2"
    local log_isolation=0  # switch. 0 or 1

    source <(local=1 prefix='' get_video_whrp "${input}")

    echo "nb_read_frames[${nb_read_frames}] ${width}x${height}"
    ((exit_code==0)) || return 1

    local fifo_s1_to_s2="$(make_temp_fifo)"
    local fifo_s2_to_s3="$(make_temp_fifo)"
    local dest_whrp="$(make_temp_fifo)"
    local png="$(mktemp --suffix=.ffmpeg.png)"

    # see bash(1)/REDIRECTION/Moving File Descriptors
    exec 101>& 1  # FD[101]: Bypass to stdout for logging

    function log_redirect() {
        if ((log_isolation==1)); then
            local d="$(dirname "$1")"
            test '.' '!=' "${d}" && {
                mkdir -p "${d}"
                mkfifo "$1"
            } &> /dev/null

            cat  &> "$1" ;

            echo end "$1" 1>& 101

            rm "$1"
        else
            cat
        fi
    }

    function step1() {
        # AV_LOG_FORCE_COLOR=1
        ffmpeg  -i "${input}"  -f rawvideo  -pix_fmt rgba  "${fifo_s1_to_s2}"  -y  -nostdin

        echo step1 done 1>& 101
    }

    function step2() {

        # see bash(1)/REDIRECTION/Redirecting Input
        # see open(2)/NOTES/FIFOs: ...blocks until the other end is also opened...
        # Open fifo_s1_to_s2 in read mode and handle as FD 101
        exec 110< "${fifo_s1_to_s2}"

        local count=0
        local first=1

        # while ((count < nb_read_frames)); do
        while true; do
            ((++count))

            echo -n > "${png}"
            # 0<&110 : Redirect ffmpeg standard input to 110(fifo_s1_to_s2).
            ffmpeg  -nostdin  \
                    -f rawvideo  -pix_fmt rgba  -video_size "${width}x${height}"  -i pipe:-  \
                    -frames:v 1  "${png}" -y  0<& 110 || {
                break
            }
            # ffmpeg does not error even if rawvideo input is 0 size.
            test -s "${png}" || break

            filter_image "${png}"

            ((first != 0)) && {
                first=0
                local=1 prefix='dest_' get_video_whrp "${png}"  | tee >(cat  1>&101) > "${dest_whrp}"
                # see bash(1)/REDIRECTION/Redirecting Output
                # Open fifo_s3_to_s3 in write mode and handle as FD 121
                exec 121> "${fifo_s2_to_s3}"
            }

            # output to 121(fifo_s2_to_s3)
            ffmpeg -nostdin  \
                   -i "${png}" -frames:v 1  \
                   -pix_fmt rgba  -f rawvideo  >(cat  1>& 121) -y || {
                break
            }
        done

        # see bash(1)/REDIRECTION/Duplicating File Descriptors: 
        #       ...If word evaluates to -, file descriptor n is closed.
        # '<& -', '>& -' mean the same thing
        exec 110<& -  # close fifo_s1_to_s2
        exec 121>& -  # close fifo_s2_to_s3

        echo step2 done 1>& 101
    }

    function step3() {
        source <(cat "${dest_whrp}")

        ffmpeg -nostdin \
               -f rawvideo  -pix_fmt rgba  -video_size "${dest_width}x${dest_height}" \
               -framerate "${r_frame_rate}"  -i pipe:- \
               -pix_fmt "${pix_fmt}"  "${output}"  -y  0< "${fifo_s2_to_s3}"

        echo step3 done 1>& 101
    }

    set -m  # enable job contorl

    step1  2>&1 | log_redirect fifo/fs1 &
    step2  2>&1 | log_redirect fifo/fs2 &
    step3  2>&1 | log_redirect fifo/fs3 &

    wait

    rm -f "${fifo_s1_to_s2}"  "${fifo_s2_to_s3}"  "${dest_whrp}"  "${png}"
}


main "$@"

Notes

force_cleanは強制終了によって残ったゴミの掃除。
不本意なモノが消えたり停止したりするかも知れない。

log_isolation変数が0の場合は、全ての進捗ログが標準出力と標準エラー出力に混ざって表示される(ごちゃ混ぜに表示される)。
1の場合には step1,step2,step3 のログをそれぞれfifo/fs1,fifo/fs2,fifo/fs3に流し込む。
別の terminal で cat fifo/fs1, cat fifo/fs2, cat fifo/fs3とすることでログの区別が容易になる。

解説

上記コードはbashで動くshell scriptというもの。
shell scriptが何であるかはここでは説明しない。

ffmpeg -f rawvideo

rawvideoフォーマットはヘッダーやメタデータを持たない形式。
1 frameのサイズはwidth * height * pixel_bytes固定。
-pix_fmt-pixel_formatは同義。
-r-framerateは同義。
ffmpegの入力ファイル前ならば入力ファイルの形式を指定している事になる。
入力ファイルの後ならば出力ファイルの形式を指定している事になる(これらの使用はrawvideoに限った話ではない)。
-f rawvideo -pix_fmt rgba -video_size "400x300" -framerate 30
rawvideo形式による書き込みにはseekを必要としない。

ffmpegfifoに出力する

特に注意することはない。

fifoの取り扱いについては別ページへ:

余談: 一部のフォーマットは書き込み時にseekを必要とするためfifoへの出力に失敗する。

ffmpegfifoや標準入力から入力する

fifoファイルを指定しての入力はしばしば上手くいかない。
入力ファイルがfifoであるということを示すfifo:のようなプロトコル指定子も存在しない。
したがって、(ffmpegにストリームがパイプ的性質であることを伝えるために)標準入力を利用する必要がある。

今回のようにfifo経由で流れてくる複数フレームのrawvideoを1フレームずつ処理する場合、ffmpegとの間にcatteeddなど他のコマンドを挟んではいけない。
バッファリングにより読み込み過ぎが発生した状態でffmpegが終了することにより、読み込み過ぎたままffmpegに渡らなかった分のデータが消える。

  • 下記2つは動作が異なる。前者では読み込み過ぎは発生しない。
  • ffmpeg -nostdin -i pipe:0 -frames:v 1 dest.mp4 < fifo.rawvideo
  • cat fifo.rawvideo | ffmpeg -nostdin -i pipe:0 -frames:v 1 dest.mp4

-nostdin: インタラクティブコマンドを無効にするオプション。指定しなくてもいいかも?