PCからTHETAを操作して録画する方法（無線バージョン）

# THETAのAPIを使って録画したい

## 状況

THETA Vを使って録画をしたい。
でも、タイムコードとか他のシステムとの連携をしたいから、プログラムから録画を管理できるようにしたい。

## THETA APIを使用する

THETAにはAPIが用意されており、無線と有線のそれぞれで操作ができる。
今回はタイトルの通り、無線で操作する。（有線バージョンは後日投稿予定）
[THETA API Reference](https://developers.theta360.com/ja/docs/v2.1/api_reference/)

### 接続とコマンドの送信

THETAを無線LANルータとして使用する(ダイレクトモード)のとき、
192.168.1.1:80 がTHETAのIPとポートとなる。
THETAを無線LANクライアントとして使用する(クライアントモード)のとき、
設定によってIPアドレスが変わるので要確認


一例として録画開始コマンドの送信プログラムを載せる。
WebRequestを使用してRequestを送信する。
WebRequestを作成するライブラリとそれをサポートするCommandとOptionを作成した。

```C#:SendStartCapture
public async void SendStartCapture()
{
    WebRequest request = null;
    byte[] postBytes;
    try
    {
        (request, postBytes) = CommanHelper.StartCapture();

        using (var reqStream = await request.GetRequestStreamAsync())
        {
            await reqStream.WriteAsync(postBytes, 0, postBytes.Length);
        }

    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
    finally
    {
        request?.Abort();
    }
}
```

```C#:StartCapture
public static (WebRequest webRequest, byte[] postBytes) StartCapture()
{
    var url = "http://192.168.1.1:80/osc/commands/execute";
    var request = WebRequest.Create(url);
    request.Method = "POST";
    request.Timeout = (int)(30 * 10000f);
    request.ContentType = "application/json;charset=utf-8";

    var command = new Command("camera.startCapture");
    var postBytes = Encoding.Default.GetBytes(command.ToString());
    request.ContentLength = postBytes.Length;

    return (request, postBytes);
}
```

コマンドは以下の通り（一部）

| コマンド | 用途 |
|:-----------------:|:----------------:|
| camera.takePicture | 画像の撮影 |
| camera.startCapture | 画像の連続撮影・動画の録画 開始 |
| camera.stopCapture | 画像の連続撮影・動画の録画 停止 |
| camera.listFiles | 画像・動画のファイルリストを取得 |
| camera.delete | 画像・動画の削除 |
| camera.getLivePreview | ライブビューの取得 |
| camera.getOptions | 各プロパティの取得 |
| camera.setOptions | 各プロパティの設定 |

## 動画撮影の仕様

- 投影タイプ (Equirectangular / Dual-Fisheye)
- 画像サイズ (3840x1920 / 1920x960)
- 記録可能時間 (5分 / 25分)

4Kで撮影しているとき5分経過すると自動的に撮影が停止してしまう。
その際、撮影停止コマンドを送っても正しいResponseが得られないため、
ファイルリストを取得して最新のファイルを取得するなどの工夫が必要。

### 付録（Command, Option）

```C#:Command.cs
public class Command
{
    public string Name { get; set; }
    public List<Option> Options { get; set; }

    public Command(string name, List<Option> options = null)
    {
        Name = name;
        Options = options;
    }

    public Command(string name, Option option)
    {
        Name = name;
        Options = new List<Option> { option };
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.Append("{");
        sb.Append("\"name\": \"");
        sb.Append(Name);
        sb.Append("\", \"parameters\": {");
        if (Options != null)
        {
            foreach (var o in Options.Select((x, i) => new { Value = x, Index = i }))
            {
                if (o.Index > 0) sb.Append(", ");
                sb.Append(o.Value.ToString());
            }
        }
        sb.Append("}}");

        return sb.ToString();
    }
}
```

```C#:Option.cs
public class Option
{
    public string Type { get; set; }
    public string Value { get; set; }
    public int ValueInt { get; set; }
    public List<string> Values { get; set; }
    public bool IsOptionsDoubleQuot { get; set; }
    public Dictionary<string, string> Options { get; set; }

    public Option(string type, string value)
    {
        Type = type;
        Value = value;
    }

    public Option(string type, int valueInt)
    {
        Type = type;
        ValueInt = valueInt;
    }

    public Option(string type, List<string> values)
    {
        Type = type;
        Values = values;
    }

    public Option(string type, Dictionary<string, string> options, bool isOptionsDoubleQuot = true)
    {
        Type = type;
        Options = options;
        IsOptionsDoubleQuot = isOptionsDoubleQuot;
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        sb.Append("\"");
        sb.Append(Type);
        sb.Append("\": ");

        if (Values != null)
        {
            sb.Append("[");
            sb.Append(string.Join(",", Values.Select(x => $"\"{x}\"")));
            sb.Append("]");
        }
        else if (Value != null)
        {
            sb.Append($"\"{Value}\"");
        }
        else if (Options != null)
        {
            sb.Append("{");
            if (IsOptionsDoubleQuot)
            {
                sb.Append(string.Join(",", Options.Select(x => $"\"{x.Key}\": \"{x.Value}\"")));
            }
            else
            {
                sb.Append(string.Join(",", Options.Select(x => $"\"{x.Key}\": {x.Value}")));
            }
            sb.Append("}");
        }
        else
        {
            sb.Append(ValueInt);
        }

        return sb.ToString();
    }
}
```