2024/12/31(Tue)ディスプレイの入力切替イベントを拾う方法メモ
2024/12/31 22:23
どのイベントを拾うか
初めに愚直に考えると、HDMIは映像ストリーム以外にもEDIDとかHDCPとか制御信号をやりとりしているはずなので、このイベントをどうにかして拾ってあげれば良さそう。しかし表示デバイスにも依るのかもしれないが、HDMIケーブルを都度抜き差しするならともかく、表示デバイスの電源の入切や、入力切替が選ばれている/いないを考慮して「本当に映っているか」を調べるのはなかなか大変そうだ。
DPMS
まず初めにLinuxで情報を探したところ(テストマシンはThinkPad X250でFedora、miniDP端子経由でHDMI接続したテレビを想定)、DPMS*1というディスプレイ省電力関連の規格を拾う方法が見つかった。とりあえず、libX11-devel, libXext-develあたりをインストールし、X11/Xlib.h, X11/Xproto.h, X11/extensions/dpms.hが参照できるようにしておく。LDFLAGSには-lX11 -lXextを指定すると、XOpenDisplayやDPMSInfoといった外部関数がリンクされる。
Display *disp = XOpenDisplay(NULL); CARD16 power_level; BOOL state; DPMSInfo(disp, &power_level, &state); XCloseDisplay(disp);stateにはTRUE/FALSEでDPMSの有効状態、power_levelはDPMSModeOn, DPMSModeStandby, DPMSModeSuspend, DPMSModeOffの4択(0~3)で状態が返るとあったが、そもそもDPMSが利用できないというエラーで呼び出し自体が成功しない。
まず今回試したFedora 39*2では、既にXorgの替わりにWaylandが動いているということで、そもそもDPMSがサポート外と表示されているためかも。
$ xset q Keyboard Control: auto repeat: on key click percent: 0 LED mask: 00000000 XKB indicators: 00: Caps Lock: off 01: Num Lock: off 02: Scroll Lock: off 03: Compose: off 04: Kana: off 05: Sleep: off 06: Suspend: off 07: Mute: off 08: Misc: off 09: Mail: off 10: Charging: off 11: Shift Lock: off 12: Group 2: off 13: Mouse Keys: off auto repeat delay: 500 repeat rate: 33 auto repeating keys: 00ffffffdffffbbf fadfffefffedffff 9fffffffffffffff fff7ffffffffffff bell percent: 50 bell pitch: 400 bell duration: 100 Pointer Control: acceleration: 2/1 threshold: 4 Screen Saver: prefer blanking: yes allow exposures: yes timeout: 0 cycle: 0 Colors: default colormap: 0x3f BlackPixel: 0x0 WhitePixel: 0xffffff Font Path: catalogue:/etc/X11/fontpath.d,built-ins DPMS (Display Power Management Signaling): Server does not have the DPMS ExtensionWaylandを無効にしてXorgに戻せばDPMSは自動的に有効になると言うことなので、ログイン時の歯車をクリックしてXorgベースのデスクトップを選択した状態でログインするとDPMSは有効になった。
(一度ログインすれば次回も同様の設定を維持するようだが、/etc/gdm/custom.confの[daemon]セクションにWaylandEnable=falseを書いて再起動しても良い)
$ xset q Keyboard Control: auto repeat: on key click percent: 0 LED mask: 00000000 XKB indicators: 00: Caps Lock: off 01: Num Lock: off 02: Scroll Lock: off 03: Compose: off 04: Kana: off 05: Sleep: off 06: Suspend: off 07: Mute: off 08: Misc: off 09: Mail: off 10: Charging: off 11: Shift Lock: off 12: Group 2: off 13: Mouse Keys: off auto repeat delay: 500 repeat rate: 33 auto repeating keys: 00ffffffdffffbbf fadfffefffedffff 9fffffffffffffff fff7ffffffffffff bell percent: 50 bell pitch: 400 bell duration: 100 Pointer Control: acceleration: 2/1 threshold: 4 Screen Saver: prefer blanking: yes allow exposures: yes timeout: 0 cycle: 0 Colors: default colormap: 0x20 BlackPixel: 0x0 WhitePixel: 0xffffff Font Path: catalogue:/etc/X11/fontpath.d,built-ins DPMS (Display Power Management Signaling): Standby: 0 Suspend: 0 Off: 0 DPMS is Enabled Monitor is On
しかし、これで万事解決かというと、どうも電源や入力切替を考慮した状態が返ってこないようだ……。常にMonitorはOnだというステータスが返ってくる。
今回試したのがノートPCで、内部的にLCDパネルと結線状態が保たれているせいかもしれないが、とりあえず「このポートのディスプレイに出力しているか」という判定には使えなさそう。
xrandr
Linuxでディスプレイ制御というと大御所はXRandRかなということで、実機またはDISPLAY環境変数に:0とか:1を指定した状態でxrandrを実行してみた。まずは、ディスプレイ側がOFFの状態。(HDMIケーブルは接続してある)
$ DISPLAY=:1 xrandr Screen 0: minimum 320 x 200, current 1920 x 1080, maximum 16384 x 16384 eDP-1 connected (normal left inverted right x axis y axis) 1920x1080 60.04 + 60.01 59.97 59.96 59.93 1680x1050 59.95 59.88 1400x1050 59.98 1600x900 59.99 59.94 59.95 59.82 1280x1024 60.02 1400x900 59.96 59.88 1280x960 60.00 1440x810 60.00 59.97 1368x768 59.88 59.85 1280x800 59.99 59.97 59.81 59.91 1280x720 60.00 59.99 59.86 59.74 1024x768 60.04 60.00 960x720 60.00 928x696 60.05 896x672 60.01 1024x576 59.95 59.96 59.90 59.82 960x600 59.93 60.00 960x540 59.96 59.99 59.63 59.82 800x600 60.00 60.32 56.25 840x525 60.01 59.88 864x486 59.92 59.57 700x525 59.98 800x450 59.95 59.82 640x512 60.02 700x450 59.96 59.88 640x480 60.00 59.94 720x405 59.51 58.99 684x384 59.88 59.85 640x400 59.88 59.98 640x360 59.86 59.83 59.84 59.32 512x384 60.00 512x288 60.00 59.92 480x270 59.63 59.82 400x300 60.32 56.34 432x243 59.92 59.57 320x240 60.05 360x202 59.51 59.13 320x180 59.84 59.32 DP-1 disconnected (normal left inverted right x axis y axis) HDMI-1 disconnected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 0mm x 0mm DP-2 disconnected (normal left inverted right x axis y axis) HDMI-2 disconnected (normal left inverted right x axis y axis) 1920x1080 (0x9a) 148.500MHz +HSync +VSync h: width 1920 start 2008 end 2052 total 2200 skew 0 clock 67.50KHz v: height 1080 start 1084 end 1089 total 1125 clock 60.00HzeDP-1は内蔵ディスプレイに繋がっているので良いとして、この時点ではHDMI-1は"disconnected"だと認識している。
この状態はディスプレイ側をオンにするだけでは変化せず、入力切替をして該当のHDMIポートを選ぶことで、以下のように変化した。
(略) HDMI-1 connected primary 1920x1080+0+0 (normal left inverted right x axis y axis) 819mm x 461mm 1920x1080 60.00*+ 59.94 24.00 23.98 23.97 1920x1080i 60.00 59.94 1280x720 60.00 59.94 1440x480i 59.94 720x480 60.00 59.94 720x480i 60.00 59.94 640x480 60.00 59.94よしよし。
ということは、これを知るためのAPIを叩くと良いわけですね。
libXrandr
libXrandr-develを入れると、X11/extensions/Xrandr.hがincludeできるようになり、ここにXRRから始まる関数がいくつかある。XRRGetOutputInfoで上記のxrandコマンドの各ディスプレイの解像度・周波数情報などが取れるようになるので、これに与えるリソース情報をXRRGetScreenResourcesで取得しておき、さらにそれに渡すウィンドウ情報をDefaultRootScreenで取得する。
順番に書くとこんな感じ。LDFLAGSに-lX11 -lXext -lXrandrをお忘れ無く。
Display *disp = XOpenDisplay(NULL); Window root = DefaultRootWindow(disp); XRRScreenResources *res = XRRGetScreenResources(disp, root); for(int i=0; i<res->noutput; i++) { XRROutputInfo *info = XRRGetOutputInfo(disp, res, res->output[i]); if(!strcmp("HDMI-1", info->name) { printf("connection: %d\n", (info->connection & 0x1)); } XRRFreeOutputInfo(info); } XRRFreeScreenResources(res); XCloseDisplay(disp);上記部分のうち、info->connection & 0x1がtrueになったときに接続中、falseなら未接続、ということになる。
筆者の環境では、電源オン→入力切替で画面表示が始まるとtrueになり、その後は電源をオフにするまでこのフラグはfalseにならなかった……ので、厳密に表示しているかというよりは、表示が始まったかどうか程度を判別する物として使っている。
なお、この方法は予めイベントハンドラを登録しておいて、変化があったときに呼んでもらう、という風には出来ないようなので、愚直に while(true){...}と sleep(1);でポーリングすることにした。
しばしテスト
とりあえずinfo->connectionの変化を見て制御してみるコードを書いたものの、問題点が2つ。XRRGetScreenResourcesを呼ぶと、ドロップフレームする
今回HDMIのイベントを拾って、VLC等のプレーヤーを起動させるようにしたのだが、XRRGetScreenResourcesを呼んだタイミングで、コンマ何秒画面が固まる。音は問題無い。VLCの統計情報を見たところ、徐々にドロップフレームが増えていくのと、XRRGetScreenResourcesCurrentでは問題が起こらないが、接続情報が更新されないことから、この2つの関数差をざっと確認すると、Currentが無い方はハードウェア問合せ、Currentの方は何らかのキャッシュを使うような挙動ということが分かったので、本来あんまり連発して叩く物では無いのかもしれない。
なお、Waylandに戻すとこの挙動は起こらないのだが、DPMSが使えなくなってinfo->connectionの値が変化しないので、残念ながらこれは許容することにした。(一度オン判定したら、オフ判定するまでのインターバルを1秒などではなく、10分とか長めにとる)
単発でinfo->connectionが変化するときがある
1秒ごとにループして状態を確認していたところ、数分~数十分に1度くらいのペースで、電源/選局状態が変化していないのにtrueになることがあった。おそらくディスプレイ側がなんらかの挙動でバックグラウンドで起きたり、処理しているタイミングを拾ってしまったのだろう……詳しいことは分からないが、3回連続で変化が認められたときにオン判定する、といったロジックに置き換えて対処した。
結局は…
上記2つの点をなんとかリカバーして、この方法を採用することにした。他にも、サウンドデバイスが増えたり減ったりするのが使えるかと思って確認したりしたが、プレーヤーで再生が続いていると、ディスプレイ側の電源が切れてもサウンドデバイスがinactiveにならないようだったので、この方法は没かも。
Linuxは長く使っていますが、Xは殆ど使って居らず、X11/以下のheaderを参照するプログラムも今回初めて作ったので、色々間違ってたらすいません。
Windowsでは?
ここまでやっておいて、じゃあWindowsではどうなるのか、とC#で.NETアプリを作って試してみたところ*3- デバイスマネージャ上は、接続したときにデバイスが見え、その後はディスプレイ側の電源状態に関係無く接続済として見える
- SystemEvents.DisplaySettingsChangedにイベントハンドラーを登録しても、物理的な挿抜を伴わない状況では意味ない模様。
- GetDevicePowerStateでは、ディスプレイの電源状態は取れないとわざわざ書いてある。*4
- EnumDisplayMonitorsやQueryDisplayConfigを叩いてみた結果も同様。ディスプレイは消灯していても、デバイスとしては接続されているので、使用可というスタンスのようだ。
- WM_POWERBROADCAST - PBT_POWERSETTINGCHANGEのウィンドウメッセージハンドラをフックして云々については、Windows側の電源変化イベントをフックするのが目的であって、ディスプレイ側のイベント変化は取れない。
多分、もっとローレイヤーな所のイベントをフックできないとダメだと思うし、ディスプレイ側が電源オフかつ主電源オンの時にEDIDを保持するなどの仕組みがある場合は、Windows側は切断と見なしていないようなので(スリープ復帰後にウィンドウ位置が崩れないようにするなどの目的では、これが望ましいわけだし…)、ディスプレイの挙動によって出来たり出来なかったりするのだろう。