天狗会議録 Posts Pages About

ウィンドウをAndroidにミラーリングする

#android#windowsapi2024/2/7

概要

PCゲームを寝ながらやりたいと思った。 しかし、単にミラーリングするだけの安価なHMDはなく、Android端末に高速でミラーリングするアプリもない。 そこで、後者である「PCゲームのウィンドウをAndroid端末にUSB通信でミラーリングするアプリ」を作成した。

どのように実装したかはソースコードを見れば明らかであるため、開発する上で困ったことや「事の顛末」を当記事にまとめる。

ソースコードはこちら

通信内容読取

Javaでのソケット通信のサンプルプログラムは、その殆どすべてがBufferedReaderを使っている。 しかし、BufferedReaderはストリームをString型で読み取る。 今回のアプリではピクセルデータを転送するため、いちいちString型からint[]型にパースするのは無駄に感ぜられる。

解決策として、DataInputStreamを用いる。 次のように、byte[]型で通信内容を読み取ることができる。

try (DataInputStream stream = new DataInputStream(socket.getInputStream())) {
  byte[] buffer = new byte[BUFFER_SIZE];
  if (stream.read(buffer, 0, buffer.length) <= 0) {
      // failed to read
  }
} catch (IOException e) {
  // failed to create a data input stream or read data from buffer
}

なお、InputStreamを上手く使えば実装できるのだろうが、如何せん資料が不十分であり、挙動も意味不明であったため、諦めた。

横画面強制

アプリの画面を横画面に強制するには、setScreenOrientation(ScreenOrientation.LANDSCAPE)を呼べばいい。 しかし、この方法では、何故かonCreateが二度呼ばれてしまう。 一回目のonCreateの内容は破棄されるため、色々工夫しなければ期待通りに動かない可能性がある。

解決策として、AndroidManifest.xmlに指定する。 どうせ動的に画面の向きを強制する必要はないだろうためできる解決策である。

<application
  ...
  <activity
      android:screenOrientation="landscape"
      ...

ウィンドウのキャプチャ

基本的にウィンドウのキャプチャ手順は次の通りである。

  1. キャプチャしたいウィンドウのハンドルを取得
  2. キャプチャしたいウィンドウのDevice Context (DC)を取得
  3. キャプチャしたいウィンドウのサイズを取得
  4. ステージバッファのDCとして、キャプチャしたいウィンドウのDCと互換性のあるDCを作成
  5. ステージバッファとして、キャプチャしたいウィンドウのDCと互換性のあるビットマップを作成
  6. ステージバッファのDCとビットマップを関連付け
  7. ステージバッファにキャプチャしたいウィンドウのビットマップをコピー
  8. ステージバッファのビットマップをプログラム側の配列にマップ・コピー

しかし、キャプチャしたいウィンドウからコピーを行うと、ARGBのピクセルデータが取得できるものの、Alphaチャンネルが壊れている。 Alpha値を255に修正してやれば期待通りのピクセルデータが得られるが、処理が重い。

解決策として、プライマリスクリーンのDCからトリミング領域を指定してコピーを行う。 この解決策を取る場合は、次のことに注意しなければならない。

  • GetClientRectによって得られる左上座標はローカル座標であるため常に(0,0)になる。そのため、別途ClientToScreenによって左上座標を取得しなければならない。
  • プライマリスクリーン上では「見たまま」のピクセルデータが得られる。つまり、ウィンドウが重なっていると、重なっているウィンドウのピクセルデータも含まれる。

実機評価

リリースビルドで生成されたapkにより、実機(Oppo Reno 5 A)にインストール・実行した。

まず、PCとAndroid端末との接続には、Android端末のUSBデバッグをONにしなければならない。 ColorOS V12では、「ビルド番号」を十回ほどタップすることで開発者モードをONにできる。 なんて仕様だ。

残念なことに、Oppo Reno 5 AのUSB規格はUSB 2.0である。 つまり、通信速度は480Mbps(理論値)である。 一方、640x480pxのウィンドウのARGBピクセルデータを60fpsで送るときの通信量は589Mbpsである。 つまり、理論上60fpsでのミラーリングは不可能である。

60fpsを諦めて30fpsで実行したところ、CPUの処理能力不足か、遅延が広がるばかりだった。 更に30fpsを諦めて20fpsで実行したところ、まともに実行できた。

とはいえ、20fpsではゲームをプレイしている感覚がないため、このアプリの実用化に価値は無い