OculusGoからRemoteでPCを操作したい#1(PC画面送信)

#発端
OculusGoを買って一番驚いたのがPCとのリモート操作が標準でできないということでした。
OculusGoは単体で動作するのだから当たり前といえば当たり前ですが、私にはどうにかしてPCと繋げたいやんごとなき事情があったのです。
そこで要求とゴールを設定しました。

##(自身への)要求
1.開発が3日以内であること(三日坊主な為)
2.致命的でない限りまずは完成させること
##ゴール
<details><summary>***1.寝転んでエロゲができる***</summary>
1.PCの画面を表示できる
2.画面位置のリセットを行える
3.ポインタの位置でクリックできる
</details>

#環境
1.Windows10
2.Python 3.6.5    ※Anaconda、Pyenv未使用(使ってもいいと思う)
3.Unity 2017.4.0f1 (64-bit)

#Step1    PCの画面を送信する
誰がどう考えてもWindows機ならDirectXあたりかWindowsAPIを叩いてデスクトップ画面をキャプチャした方がいいと思いつきますが、3日では終わらないと考えPythonの画像系ライブラリである[Pillow](https://pillow.readthedocs.io/en/latest/)から画面をキャプチャします。
Pillowインストールは特に何も考えず[pip](https://www.google.co.jp/search?q=pip+python)で行います。
```pip install Pillow```
調べると複数モニタの場合は片側しか取れないみたいですが、エロゲは幸いにも片側のモニタしか使用しません。よって特に問題ありません。
Pillowからデスクトップの画面をjpegにて取得し、ソケットを用いてudpで垂れ流します。

```python:
from io     import BytesIO
from PIL    import ImageGrab
from PIL    import Image
import socket
from contextlib import closing

host = '192.168.1.XXX'    #OculusGoのIP(adb shellからifconfig)(固定しておくと楽)
port = 5999               #「画像送信用」ポート番号
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

#画像識別ID
invoke = 0

#udpがデータとして送信できる最大サイズが1500バイト程度なので何個目のブロックかを示すID
index = 0

splitSize = 1024

with closing(sock):
    while True:
        output = BytesIO()
        try:
            img = ImageGrab.grab()
        except OSError:
            print("screen grab failed")
            continue
        img = img.resize((int(img.width / 2), int(img.height / 2)), Image.HAMMING)
        #↑半分の大きさにリサイズ    ↓jpegで保存。保存先をBytesIOとするとバイト列として持てる
        img.save(output, format='jpeg', quality=70)
        jpg_data = output.getvalue()
        sd_block = [jpg_data[i:i + splitSize] for i in range(0, len(jpg_data), splitSize)]
        #↑一度に送れないため、splitSizeバイト毎に分割
        
        index = 0
        for sd_data in sd_block:
            if len(sd_block) - 1 == index:
                index = 65535    #最後のデータであることを示すために16bit最大値にした
            sock.sendto(invoke.to_bytes(1, 'big') + index.to_bytes(2, 'big') + sd_data, (host, port))
            index += 1
        invoke += 1
        if invoke == 256:
            invoke = 0
```

後から分かったことなんですが、PillowのImageGrab.grab()はバグ持ち＋メンテナンスされてないみたいで、稀にOSError吐いて死にます。
あとは一定の間操作せずに画面の電源が切れるとドライバを巻き込んで(？)死ぬので、キャプチャしてる側のディスプレイが復帰しなくなります。改めて作る場合、素直にWindowsapiとかで取得しましょう。

#Step2    Unityから画面を描画する
UnityでOculusGoを使えるようにしておきます
[こことか](https://www.yoshidayo.com/entry/2018/05/09/103503)、[こことか](https://it-bucket.com/og-dev-01/)


どのようにシーンを構築しても自由ですが、unityで作れるPlainやらのみで以下の通りにしました。
![p1.png](https://qiita-image-store.s3.amazonaws.com/0/281660/72369f51-7a35-e39e-e555-f0e70a87b11c.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/281660/8cd106ec-f92e-8acc-c566-a46f45fd1268.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/281660/ad69c3f6-da22-8e4e-ffe2-2f258ae64c3e.png)


Pointer.csは「[Oculus Goでレーザーポインターを表示する](https://qiita.com/noitak/items/f442a45c2d772d9d8269)」を変数名もろとも参考にしました。
opはポインタで使用するLine Rendererで、pは画面の座標取得用の子オブジェクトです。
上記は操作編で解説予定であり、データ送信には関係ありません。必要なのはUdp.csです。

受信したデータを画面に描画するコードは以下の通りにしました。

```c#:Udp.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;

public class Udp : MonoBehaviour {

    static UdpClient udp;
    int PORT = 5999;//「画像送信用」ポート番号
    Thread thread;

    Renderer r;
    Texture2D tex;
    List<byte> rcv;
    byte[] bufs;
    ushort _index = 0;
    bool stain = false;
    int hitc = 0;

    // Use this for initialization
    void Start () {
        rcv = new List<byte>();//受信したデータを入れておく
        r = GetComponent<Renderer>();//描画する画面

        tex = new Texture2D(960, 540);//画面に合わせて適当に決めておく
        tex.filterMode = FilterMode.Bilinear;

        bufs = null;
        udp = new UdpClient(PORT);
        thread = new Thread(new ThreadStart(ThreadMethod));
        thread.Start();
    }

    private void ThreadMethod() {
        while (true) {
            IPEndPoint remoteEP = null;
            byte[] data = udp.Receive(ref remoteEP);

            byte[] d = new byte[data.Length - 3];
            Buffer.BlockCopy(data, 3, d, 0, d.Length);//invoke,indexを除いたデータ部分のみ移動

            byte[] tb = new byte[2];//ビッグエンディアンなのでリトルにする
            tb[1] = data[1];
            tb[0] = data[2];
            ushort index = BitConverter.ToUInt16(tb, 0);
            _index++;
            if(index == 0) {//最初のブロックの場合初期化
                rcv.Clear();
                _index = 0;
                stain = false;
                hitc = 0;
            }
            if(index != _index) {
                hitc++;
                _index = index;
                if (hitc > 10) {//10回以上indexがずれたら諦めて描画しない
                    stain = true;
                }
            }

            rcv.AddRange(d);

            if (index == 65535 && bufs == null && stain == false) {
                bufs = rcv.ToArray();
            }
        }
    }

    void OnApplicationQuit() {
        thread.Abort();
    }

    // Update is called once per frame
    void Update() {
        if(bufs != null) {
            tex.LoadImage(bufs);
            r.material.mainTexture = tex;//ここで描画
            bufs = null;
        }
    }
}
```

改めて見直してもイベントは使ってないわ、async await使ってないわ、エンディアン変換そりゃないだろ等ありますが、まずは動きゃいいんです。
※もちろんinvokeIDも使ってindex==0が受信できなかったときの処理も入れないといけません。なにやってんだこいつ。

まあ、ここまでで、ビルドしてOculusGoから覗くとデスクトップ画面が表示されています。

ただし、テカってすごく画面が見にくいと思うので、ライト設定を切ったりしてみやすさを調整してください。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/281660/0c7cacc9-217c-73a2-d2e0-baacad78055a.png)


小さい文字が潰れてしまっていますが、画像圧縮を変えるとか動画用フォーマットを使用してみるとかするといいと思います。
正直Viveとかも見づらいから画面がでかいことが問題かも。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/281660/d25fb637-7c0d-c9e3-593f-7c3f9e4ab1d7.png)

ポインタ、画面リセット、クリック等の[続きはこちら](https://qiita.com/selfPolygon/items/4cf1f05c69fec5acc69f)

#免責
※コード見れば分かると思いますが、動作を保証しているものではありません。あくまでも参考程度にしましょう。