QVR Pro から任意の時間の録画を取得して mp4 にする

QNAP の監視カメラサービス [QVR Pro](https://www.qnap.com/solution/qvr-pro-official/ja-jp/) のクライアントソフトには、任意の時間の録画を取得して mp4 にエクスポートする機能がある。これを自動化したい。


# 概要
QVR Pro には HTTP API があり、下記の "QVR Pro API を申請" から申込むと見ることができる (自動応答で URL 送られてくる)。

- [QVR Pro開発者ポータル - QNAP](https://www.qnap.com/solution/qvrpro-developer/ja-jp/)

これを利用して、下記の手順を踏む事で期待する結果が得られた。

1. ログイン
2. カメラの id を取得
3. 取得したい録画の情報を指定
4. ダウンロード
5. .264 に変換
6. .mp4 に変換

1-4 は QVR Pro の HTTP API 呼び出し。
5 は何らかのスクリプト (ここでは Ruby を使用。
6 は ffmpeg を使う。

# QVR Pro の API 呼び出し
ここでは curl を使う。

## ログイン
```bash
# パスワードは Base64 encode して percent encoding する
$ curl -k 'https://[host:port]/cgi-bin/authLogin.cgi?user=[username]&serviceKey=1&pwd=[encoded_password]'

```

```xml
<?xml version="1.0" encoding="UTF-8"?>
<QDocRoot version="1.0">
  ...
  <authSid><![CDATA[xxxxxxxx]]></authSid>
  ...
</QDocRoot>
```

認証に成功すれば `/QDocRoot/authSid` にセッション ID が入ってくる。今後のリクエストにはこのセッション ID が必要になる。

## カメラの ID を取得

```bash
# カメラの一覧
# sid: "ログイン" で取得したセッション ID を指定
curl -k 'https://[host:port]/qvrpro/camera/list?sid=[sid]&ver=1.0.0'
```

```json
{
  "datas": [
    {
      "channel_index": 0,
      "name": "Camera",
      "umsid": "XXXXXXXXXXXXXXXX",
      "guid": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "brand": "Panasonic",
      "model": "BB-ST165",
      "mac": "XX:XX:XX:XX:XX:XX",
      "ver": "4.00",
      "ip": "X.X.X.X",
      "port": "80",
      "video_codec_setting": "H.264",
      "video_resolution_setting": "1280x960",
      "frame_rate_setting": "10",
      "video_quality_setting": "8192kbps",
      "stream_state": [],
      "status": "NVR_CAM_CONNECT_IDLE",
      "rec_state": "RECORDING",
      "rec_state_err_code": 0,
      "frame_rate": "-1",
      "bit_rate": -1
    },
    ...
}
```

`datas[n].guid` で得られるカメラの ID が必要になる。

## 取得したい録画の情報を指定
```bash
# sid: "ログイン" で取得したセッション ID を指定
# ch_sid: "カメラの ID を取得" で取得したカメラの ID を指定
# start_time: 録画を取得したい開始日時を Unix time でミリ秒単位で指定
# end_time: 録画を取得したい開始日時を Unix time でミリ秒単位で指定
# gop_mode: 1 を指定するとストリーミングでなく、ダウンロードできる (ドキュメント未記載)
# ほかのオプションはだいたいこれでよいはず (詳しくはAPI ドキュメント参照)
curl -k 'https://[host:port]/qvrpro/apis/qplay.cgi?ver=v1&cmd=open&sid=[sid]&ch_sid=[ch_sid]&query_type=0&start_time=1541486138332&end_time=1541486148332&recording_type=0&stream_id=255&alarm_stream_id=255&data_type=1&gop_mode=1'
```

```bash

0
PSXXXXXXXXXXXXXXXXXXXXXXXXXX__
```

レスポンスは 0 なら成功。そのあとにプレイバックセッション ID が続く。
次のステップでプレイバックセッション ID が必要。

`gop_mode=1` の指定をしない場合、レスポンスに `end_time - start_time` だけ時間がかかる。後述の "既知の問題" も参照。


## ダウンロード
```bash
# sid: "ログイン" で取得したセッション ID を指定
# session: "取得したい録画の情報を指定" で取得したプレイバックセッション ID を指定
curl -k 'https://[host:port]/qvrpro/apis/qplay.cgi?ver=v1&cmd=get&sid=[sid]&session=[session]' > out.q264
```

レスポンスは最初の 3 bytes が `0x0A300A` なら成功。
その後にビデオフレームが続く。

ここでは最初の 3 bytes も含めてファイルに書き出している。

# .264 に変換
(たぶん) "取得したい録画の情報を指定" で `gop_mode=1` を指定した場合、ビデオフレームの形式はドキュメントされているものと異なる。
下記のような形式と思われる。

```
# リトルエンディアン
# 下記の繰り返し
# 最後に 0x00 の padding がつく
uint8 0x0a
string タイムスタンプ (文字列)
uint8 0x0a
string この次の次からフレームの終わりまでのサイズ (文字列)
uint8 0x0a
uint32 0x00
uint32 0x0a なら最後以外のフレーム、0x0b なら最後のフレーム
uint32 0x02
uint8[4] "q264"
uint32 画像幅
uint32 画像高さ
uint8[24] 0x00 * 24
uint8[4] "G276"
uint32 0x1f40
uint32 0x04
uint32 0x01
uint32 0x00
uint64 このあとに続くデータのサイズ (バイト単位)
string H.264 の Raw データ
```

次の処理に必要なのは、最後の `H.264 の Raw データ` なのでこれだけ抽出するスクリプトを書いた。

- [Convert QVR Pro Playback API response (with gop_mode=1) to .264 file. · GitHub](https://gist.github.com/labocho/45c35256a78fdcaec23de8f861a3a05f)


```bash
$ ruby q264to264.rb out.q264 > out.264
```


# .mp4 に変換
```bash
# -r はフレームレート。"カメラの ID を取得" の frame_rate_setting と同じ値でいいはず
$ ffmpeg -r 10 -i out.264 -c copy out.mp4
```

# 既知の問題

"取得したい録画の情報を指定" で `gop_mode=1` を指定した場合、音声のデータが含まれない。そもそもクライアントソフトのエクスポート機能でも音声を含めるよう設定すると、エクスポートが終わらない問題がある (Windows 版で mp4 指定時はエクスポートが完了するが音声は含まれない)。

`gop_mode=1` を指定すると、音声のデータが含まれるようになる (形式は公式のドキュメント参照) が、レスポンスに取得したい録画と同じだけの時間がかかるようになってしまう。