Exclude from Capture

# 録画できないウィンドウ？！
## きっかけ
ある日の事、ぼーっとネットを見ていた時にこんな情報を知った。

> 『動画配信サービスではスクショや画面録画が出来ないようになっている』

なるほど、アニメやドラマの無断転載を防ぐための技術なのだろう。
著作権保護はとっても大事！

しかしエンジニアの視点で見ると、なんとも不思議な技術だ。
どうやって録画している事を感知しているのだろうか？　スクショを防ぐだけなら“Print Screen”キーが押されたことを感知すればよい。だが動画キャプチャにも映らないとはいったいどんな技術を使っているのだろう……？

## 失敗は成功のもと
後々の調査でブラウザの機能でそういった事が出来るらしいと知ったものの、当時は間違った方向で調査を始めてしまった。
当時、自分はWindows APIに興味があった事から、Windows APIから調査を始めてしまった。当時の自分に「そっちに答えは無いよー！」と言ってあげたい。

しかし、『失敗は成功のもと』という言葉があるように、この調査で面白い関数に出会う事が出来た。それが`SetWindowDisplayAffinity`という関数だ。

## SetWindowDisplayAffinity
Windows APIの関数`SetWindowDisplayAffinity(HWND, DWORD)`はあるウィンドウの内容が表示されるかどうかを決める関数だそう。

一引数にはウィンドウのハンドルを、第二引数には以下の三つの値を入れる事が出来る。
+ WDA_NONE
  ウィンドウの表示に制限がない。
+ WDA_MONITOR
  内容はモニタのみに表示され、その他の場所には表示されない。
+ WDA_EXCLUDEFROMCAPTURE
  内容はモニタのみに表示され、その他の場所にはウィンドウも表示されない。

この関数を使う事で、スクリーンショットや画面録画にウィンドウの内容が写らないようにできるらしい。

実際に簡単なアプリケーションを作って`SetWindowDisplayAffinity`関数を使ってみたところ、第二引数に`WDA_MONITOR`を指定すると録画上でウィンドウが黒塗りになった。ほうほう、なるほど！　これを使ってスクショ不可のウェブサイトを作っていたんだな！（←実は違う・詳しくは割愛）

それでは次に`WDA_EXCLUDEFROMCAPTURE`を使ってみよう。試してみると、確かに画面録画にはウィンドウが映らなかった。スゲー！！

# ゲーム開発
## Godot
『画面録画に映らないウィンドウ』には様々な応用性があると感じ、それを使ってゲーム開発をしようと考えた。はじめはWindows API & DirectXで開発していたが、DirectXがあまりにも複雑だったもので、一度断念した。

そんな時に出会ったのがGodotというゲームエンジン。
Godotはオープンソースのゲームエンジンで、Pythonライクな言語を使って手軽にゲーム開発を始められる、非常にとっつきやすいゲームエンジンだ。（是非みんなも使ってみよう！）

さて、Godotの`Window`クラスには`exclude_from_capture`というフラグがあり、これをオンにすることで録画に写らないと思われる。やったぜ！

## 公式ドキュメントはたまに嘘を吐く
しかし公式ドキュメントの説明を読んでみると……

> このウィンドウはGodotの関数`DisplayServer.screen_get_image()`などに映らなくなります。

と書いてあった。「あれ、スクリーンショットに写らないのではなく、ゲーム内のスクショ関数に写らないだけなの……？」とがっかりした。残念……。

しかし、念のために試してみようと思って、`exclude_from_capture`フラグを建てたウィンドウを表示してみた。その状態でスクショを撮ると……。

**「映らないじゃん！！」**

見事にスクショや画面録画に写らないウィンドウになっていた。これは嬉しい誤算!!

なるほど、「公式ドキュメントはたまに嘘を吐く」という事だろう。また一つ、賢くなってしまった。

## しかしまだまだ問題があり……
この「スクショには写らないウィンドウ」をメイン画面の真上に被せる事で、ゲームプレイ中とその録画で写る内容が異なるゲームを作ろうと思った。

さて、そうなるとウィンドウサイズを変更したりウィンドウを動かしたりした時に、「スクショには写らないウィンドウ」を追尾させる必要がある。

Godotには`signal`というものがあり、それを使う事で様々な情報を取得する事が出来る。例えばサイズの変更`size_changed`を知って、そのタイミングで`on_foreground_size_changed()`関数を呼び出す場合、以下のように書く。

```gdscript
# スクショに写らないウィンドウ
# どこかのタイミングで初期化しているものとする
var foreground_window:Window

# セットアップ処理
func _ready()->void:
    foreground_window.size_changed.connect(on_foreground_size_changed)

# foreground_windowのサイズが変更された時に呼び出される。
func on_foreground_size_changed()->void:
    # 何らかの処理
    pass
```

シグナル`size_changed`をキャッチするには`size_changed.connect(関数名)`と書く。

なるほどね。それじゃあウィンドウが動いた事を教えてくれるシグナルは……？

？

……？

？？？

どこを探しても出てこない。な、なんで……？

## まずは失敗談
Godotの公式ドキュメントを調べていると、`DisplayServer.window_set_rect_changed_callback`という関数で、ウィンドウの位置が変わった事を知れるらしい。勝ったな、ガハハッ！

しかし、これを使うと、ウィンドウが上手く動作しなくなった。具体的にはサイズの変更が出来なくなったり……。

おかしいな、なんでだろう？　公式ドキュメントをもう一度読み返すと、このように書かれていた。

> 注意：これを使用すると、デフォルトの設定が塗り替えられ、結果としてバグが発生する恐れがあります。

か、書いてたー！　公式が言ってるんだもの、そりゃあバグが起こりますよね！！

やっぱり公式ドキュメントは参考になる。ちゃんと公式ドキュメントを参照しないとな！
（これぞ見事な手のひら返し！）

## _notification
結果的に`_notification`という組み込みメソッドを使う事で解決できると知った。具体的には、`Window`クラスを継承した以下のようなクラスを作った。

```gdscript
extends Window
class_name WindowWithRectChangedSignal
signal window_position_changed
signal window_size_changed

func _notification(what: int) -> void:
	if what == NOTIFICATION_WM_POSITION_CHANGED:
		window_position_changed.emit()
	elif what == NOTIFICATION_WM_SIZE_CHANGED:
		window_size_changed.emit()
```

意外に簡単な方法だった。今後は`WindowWithRectChangedSignal`を使う事で、ウィンドウの移動を検知できる！

**やったね♪ これで完璧！**（←フラグ）

## ウィンドウの最小化を検知できない……
完璧だと思っていましたが、まだ壁がありました。なんとウィンドウを最小化しても`NOTIFICATION_WM_SIZE_CHANGED`が反応してくれないのです！！

おいおい、なんでだよ！！

これの解決法は……思いつきませんでした。現状は毎フレームウィンドウの状態を確認して、最小化が起きているか調べる方法を使っています。毎フレーム確認すると、処理に多少の負担がかかりそうですが……致し方なし。

```gdscript
signal minimized

# 毎フレーム呼び出される関数
func _process(_delta: float) -> void:
	if mode == MODE_MINIMIZED:
		minimized.emit()
```

もっと良い方法をご存じの方がいらっしゃれば、コメント等で教えて頂ければ幸いです！！

## まだまだ問題は残っていて……
残っている問題と現状の解決法

### タスクバーに二つウィンドウが表示される
「録画に映らないウィンドウ」に`popup_window`フラグを立てる事で解消した。

### 最小化した後に最大化すると、「録画に映らないウィンドウ」が再表示されない事がある。
最小化時に「録画に映らないウィンドウ」を破棄して、復帰時に再構築することで解決した。

### 別ウィンドウで覆った後に再度ゲーム画面を移すと、「録画に映らないウィンドウ」が再表示されない事がある。
  「録画に映らないウィンドウ」に`transient`と`exclusive`フラグを立てる事で解消した。

## 参考文献
+ SetWindowDisplayAffinityについて
Microsoft. "SetWindowDisplayAffinity function", Microsoft Learn, https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowdisplayaffinity . (参照2025年6月12日)

+ Godotの`Window`および`exclude_from_capture`について
Juan Linietsky, Ariel Manzur and the Godot community, "Window", Godot Engine 4.4 documentation, https://docs.godotengine.org/en/stable/classes/class_window.html#class-window-property-exclude-from-capture . (参照2025年6月12日)

+ Godotの`DisplayServer.window_set_rect_changed_callback`について
Juan Linietsky, Ariel Manzur and the Godot community, "Window", Godot Engine 4.4 documentation, https://docs.godotengine.org/en/stable/classes/class_displayserver.html#class-displayserver-method-window-set-rect-changed-callback . (参照2025年6月12日)