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つです。
-
gdigrabは取り込み段階で 4.2〜7.1% のコマを落としている。 これはエンコーダのせいではありません。ddagrabに替えてエンコーダを libx264 のまま据え置いても drop は 0 になっているからです。 -
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から毎フレーム引っこ抜き、システムメモリへコピーする方式です。ベストエフォートのポーリングなので、他プロセスが割り込むと単純に取りこぼします。
対して ddagrab は DXGI の 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 だけで、この手の面倒を見る仕組みがありません。
同梱バイナリで既に使えるか確認する
ddagrab も h264_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/