0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ffmpegのgdigrabは「何も動いていない画面」でもコマを落とす — ddagrab+NVENCでdrop 32→0、録画中のCPU時間を約1/25にした実測

0
Last updated at Posted at 2026-07-26

Windowsで画面録画を自動化するとき、ネットに転がっているコマンドはたいてい -f gdigrab -i desktop です。私も長いことそれで録っていました。

ところが「録画しながら別ターミナルで作業する」という使い方を始めたら、出力がどうも滑らかでない。そこで実際に測ったら、ほぼ静止したデスクトップですら 450フレーム中32フレームを落としていました

同じ条件で ddagrab に替えると drop=0、さらに h264_nvenc を組み合わせると 録画中のffmpegプロセスのCPU時間が 8.6〜10.5秒 → 0.27〜0.42秒になりました。追加インストールはゼロです。

この記事は、その計測方法と数字、そして乗り換え時に踏むデグレを全部書いたものです。


環境

項目
OS Windows 11 Home 26200
GPU GeForce RTX 5070 (VRAM 12GB)
ffmpeg 7.1 essentials build(gyan.dev。imageio-ffmpeg に同梱されているもの。PATHは通していない)
計測 Python 3.12 + ctypes で Win32 GetProcessTimes
録画条件 1920x1080 / 30fps / 15秒 / 同一デスクトップ(ほぼ静止)
計測日 2026-07-26

「ほぼ静止」は文字どおりで、動画も再生していなければスクロールもしていません。いちばん条件のいい状態です。


結論(先に表)

期待フレーム数は 30fps × 15s = 450。各方式2回ずつ実行しています。

方式 frames/450 drop ffmpegプロセスのCPU時間 出力サイズ
gdigrab + libx264 veryfast crf18(1回目) 417 32 8.64s(1コアの57.2%) 0.40MB
同(2回目) 431 19 10.53s(69.5%) 0.37MB
ddagrab + libx264 veryfast crf18(1回目) 449 0 8.75s(57.3%) 0.38MB
同(2回目) 449 0 9.08s(59.4%) 0.37MB
ddagrab + h264_nvenc p5 cq19(1回目) 449 0 0.42s(2.8%) 0.73MB
同(2回目) 449 0 0.27s(1.7%) 0.70MB

読み取れることは2つです。

  1. gdigrab は取り込み段階で 4.2〜7.1% のコマを落としている。 これはエンコーダのせいではありません。ddagrab に替えてエンコーダを libx264 のまま据え置いても drop は 0 になっているからです。
  2. CPU時間が減るのは NVENC に替えたときだけ。 ddagrab + libx264 では CPU時間はほぼ変わりません(8.75s / 9.08s)。取り込みの改善とエンコードの改善は別の話です。ここを混ぜて「ddagrabにしたら軽くなった」と書くと嘘になります。

計測スクリプト(そのまま動きます)

frame=drop= は ffmpeg が stderr に出す -stats の行から拾い、CPU時間は Win32 API の GetProcessTimes でカーネル時間+ユーザー時間を取っています。

GetProcessTimesプロセス終了後もハンドルが生きていれば読めるので、起動直後にハンドルを開いておいて、終了後に読むのがポイントです。

# -*- coding: utf-8 -*-
"""gdigrab vs ddagrab の実測ベンチ(CPU時間 + 落ちフレーム数)"""
import ctypes, ctypes.wintypes as wt, re, subprocess, time
from pathlib import Path

import imageio_ffmpeg
FF = imageio_ffmpeg.get_ffmpeg_exe()   # 同梱のffmpeg.exeパス。PATHは不要
SP = Path(__file__).resolve().parent

k32 = ctypes.windll.kernel32


class FT(ctypes.Structure):
    _fields_ = [("l", wt.DWORD), ("h", wt.DWORD)]


def ft2s(f):
    # FILETIME は 100ns 単位
    return ((f.h << 32) | f.l) / 1e7


def bench(name, args, secs):
    out = SP / f"{name}.mp4"
    cmd = [FF, "-y", "-hide_banner", "-loglevel", "warning", "-stats"] + args + [str(out)]
    t0 = time.perf_counter()
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL)
    # PROCESS_QUERY_INFORMATION | PROCESS_QUERY_LIMITED_INFORMATION
    h = k32.OpenProcess(0x0400 | 0x1000, False, p.pid)
    err = p.communicate()[1].decode("utf-8", "replace")
    wall = time.perf_counter() - t0

    c, e, kt, ut = FT(), FT(), FT(), FT()
    cpu = float("nan")
    if h:
        if k32.GetProcessTimes(h, ctypes.byref(c), ctypes.byref(e),
                               ctypes.byref(kt), ctypes.byref(ut)):
            cpu = ft2s(kt) + ft2s(ut)      # カーネル時間 + ユーザー時間
        k32.CloseHandle(h)

    m = re.findall(r"frame=\s*(\d+)", err)
    frames = int(m[-1]) if m else -1
    d = re.findall(r"dup=\s*(\d+)\s+drop=\s*(\d+)", err)
    dup, drop = (d[-1] if d else ("0", "0"))
    exp = int(round(30 * secs))
    print(f"{name:12s} frames={frames:4d}/{exp}  drop={drop:>3s} dup={dup:>3s}  "
          f"wall={wall:6.2f}s  CPU={cpu:6.2f}s ({cpu / wall * 100:5.1f}% of 1 core)  "
          f"size={out.stat().st_size / 1e6:.2f}MB")


SEC = 15

GDI = ["-f", "gdigrab", "-framerate", "30", "-draw_mouse", "1",
       "-offset_x", "0", "-offset_y", "0", "-video_size", "1920x1080", "-i", "desktop",
       "-t", str(SEC), "-c:v", "libx264", "-preset", "veryfast", "-crf", "18",
       "-pix_fmt", "yuv420p", "-an"]

DDA = ["-f", "lavfi", "-i",
       "ddagrab=framerate=30:video_size=1920x1080:offset_x=0:offset_y=0:draw_mouse=1",
       "-t", str(SEC), "-vf", "hwdownload,format=bgra,format=yuv420p",
       "-c:v", "libx264", "-preset", "veryfast", "-crf", "18", "-an"]

DDANV = ["-f", "lavfi", "-i",
         "ddagrab=framerate=30:video_size=1920x1080:offset_x=0:offset_y=0:draw_mouse=1",
         "-t", str(SEC), "-c:v", "h264_nvenc", "-preset", "p5", "-cq", "19", "-an"]

for i in (1, 2):
    bench(f"gdi_x264_{i}", GDI, SEC)
    bench(f"dda_x264_{i}", DDA, SEC)
    bench(f"dda_nvenc_{i}", DDANV, SEC)

⚠️ 出力される mp4 には自分のデスクトップがそのまま写ります。計測が終わったら中身を見ずに消してください(私はそうしました)。


なぜ gdigrab は落ちるのか

gdigrab は GDI の BitBlt で画面DCから毎フレーム引っこ抜き、システムメモリへコピーする方式です。ベストエフォートのポーリングなので、他プロセスが割り込むと単純に取りこぼします。

対して ddagrabDXGI の Desktop Duplication API を使います。OSのデスクトップコンポジタから更新通知つきでフレームを受け取るので、取りこぼしの構造がそもそも違います。

さらに ffmpeg の ddagrab フィルタには dup_frames オプションがあり、既定が true です。

$ ffmpeg -h filter=ddagrab
  output_idx / draw_mouse (既定true) / framerate (既定30) / video_size /
  offset_x / offset_y /
  output_fmt (auto,8bit,bgra,10bit,x2bgr10,16bit,rgbaf16) /
  allow_fallback / force_fmt / dup_frames (既定true)

画面が更新されなかった区間は直前のフレームを複製してフレームレートを維持する。これが drop=0 の直接の理由です。「落とさない」のではなく「落とす代わりに複製する」設計だ、という理解が正確です。

一方の gdigrab は、公式ドキュメント(ffmpeg-devices)に載っているオプションが draw_mouse / framerate / video_size / offset_x / offset_y だけで、この手の面倒を見る仕組みがありません。


同梱バイナリで既に使えるか確認する

ddagrabh264_nvenc もビルドオプション依存です。追加DLの前に、手元の ffmpeg にあるか確認してください。

ffmpeg -hide_banner -filters  | findstr ddagrab
ffmpeg -hide_banner -encoders | findstr nvenc

imageio-ffmpeg に同梱されている gyan.dev の essentials build(7.1)には両方入っていました。私のケースでは追加インストールはゼロです。


乗り換えるときにデグレする点(ここが本題)

「速いから替える」で済まないところがあります。実際に引っかかったものを全部書きます。

注意点 内容 対処
ウィンドウ指定ができない gdigrab-i title=<ウィンドウタイトル> に相当する機能が ddagrab には無い。DXGIの出力(モニタ)単位でしか取れない gdigrab 経路を消さずに残す。 私は --backend gdi|dda で切り替えられるようにした
対話セッションが必須 画面ロック中・RDP越しでは Desktop Duplication を張れない 収録は実機のログイン状態で行う。タスクスケジューラで「ログオンしていなくても実行する」にすると録れない
保護コンテンツ(DRM)は黒になる 動画配信サービス等は真っ黒 用途次第。技術解説の画面収録なら無関係
libx264 に渡すには変換が要る GPUテクスチャで出てくるので、そのままでは食わせられない -vf hwdownload,format=bgra,format=yuv420p を挟む(上のスクリプトの DDA そのまま)
NVENC はGPUを使う ただし NVENC は専用の固定機能ブロックで、CUDA/SM とは別回路。VRAM消費は数百MB程度(実測はしていない、ここは推定) GPUで画像生成などを回している時間帯の収録は避けるのが安全
中間ファイルが大きくなる cq19 で 0.70〜0.73MB/15s。libx264 crf18 は 0.37MB 中間素材なら問題にならない。最終エンコードは別途 libx264 で掛ける

とくに1行目が効きます。「特定のウィンドウだけ録る」要件があるなら ddagrab は使えません。 全画面録画に限った話です。


採用したコマンド

長尺の画面収録用に、中間素材は品質を余裕側に振っています(文字の視認性が最優先なので -qp 16)。

ffmpeg -y ^
  -f lavfi -i "ddagrab=framerate=30:video_size=1920x1080:offset_x=0:offset_y=0:draw_mouse=1" ^
  -t 900 -c:v h264_nvenc -preset p7 -tune hq -rc constqp -qp 16 -pix_fmt yuv420p -an ^
  raw.mp4

-rc constqp -qp 16 は「品質固定・ビットレート青天井」です。中間素材だからこれでよく、配信用には別途 libx264 で掛け直します。


OBS Studio を入れるべきか → 私は入れませんでした

  • OBS の Display Capture も内部的に Windows Graphics Capture / DDA を使うので、取り込み品質は ddagrab と同系統です。
  • OBS で得られるのはシーン切替・複数ソース合成・ホットキー。合成を全部 ffmpeg で後処理しているなら重複です。
  • ただし OBS が明確に勝つ点が1つあります。「特定ウィンドウを、他のウィンドウに隠されていても取れる」(WGC のオクルージョン耐性)。上の表の1行目の制約を突破したいなら OBS が答えになります。

まとめ

  • gdigrab静止画面でも 4〜7% コマを落とす。実測しないと気づかない。
  • ddagrab に替えると drop=0。ただし CPU は軽くならない。軽くなるのは NVENC を併用したときだけ(8.6〜10.5s → 0.27〜0.42s)。
  • 代償はウィンドウ単位で録れないこと。gdigrab 経路は残しておくのが無難。
  • 手元の ffmpeg に入っているかは -filters / -encoders で先に確認する。

「録画が重い」と感じたら、まず drop= を読んでください。エンコーダを軽くしても、落ちているのが取り込み側なら何も直りません。


参考

  • ffmpeg 公式 Devices ドキュメント(gdigrab のオプション一覧) https://ffmpeg.org/ffmpeg-devices.html
  • ffmpeg -h filter=ddagrab(オプションの一次情報はこれが早い)

関連して公開しているもの

Windows常駐の自動化まわりで踏んだ罠を、スクリプトごと MIT で公開しています。コンソール窓を出さずにタスクスケジューラから実行する方法、リソース枯渇の記録、PrintWindow で隠れウィンドウを撮る方法など。


筆者: タク。Windows上の業務自動化を作っています。 https://setlog-app.github.io/ai-jidoka-lab/

0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?