DIALというプロトコルについて足を突っ込んでみた

お久しぶりです。1年に1回 Qiita へ投稿する @Momijinn です。
この記事では DIAL というプロトコルについて軽く触れてみたという記事になります。

## DIAL とは

DIAL (DIscovery And Launch) とは、セカンドスクリーン端末(主にスマートフォン)が、ファーストスクリーン端末（主にテレビ）にインストールしているアプリを検出して、簡単にアプリを起動するためのプロトコルです。
※ あくまで DIAL はファーストスクリーン端末にインストールしてあるアプリを簡単に起動や終了をするのみ（ここ大事）。

### 分かりづらいので例を挙げて説明

スマートフォンにて動画再生アプリで再生している動画を、TVで再生したいとします。
このとき、DIALなしの場合だと以下の手順を踏むことになると思います。

```
1. TVリモコンでTVにインストールしている動画再生アプリを立ち上げる。
2. TVで動画再生アプリをが立ち上がる。
3. TVの動画再生アプリと、スマートフォンの動画再生アプリをペアリング（PINコード等）する
4. ペアリングできたら、スマートフォンで再生している動画をTVへ送信する
5. TV上で、スマートフォンから受け取った動画を再生する
```

DIALがある場合だと、1~3の手順が省かれてより簡単にスマートフォンの動画をTVへ再生することができます。[^1]

DIALありだと手間が省かれる。
```
1. スマートフォンにて動画再生アプリを起動
2. スマートフォンで再生している動画をTVへ送信する
3. TV上で、スマートフォンから受け取った動画を再生する
```


### 仕様書

仕様については以下にあります。
http://www.dial-multiscreen.org/home


### どこで使わているのか

Youtubeのキャストに使われています。
ご家庭に [Fire TV Stick](https://amzn.to/3HqICB4) や [Google Chromecast](https://amzn.to/3W4N501) 等のデバイスがあると、Youtubeの動画を開いたときにテレビマークみたいのがあります。
これを押すとテレビへYoutube動画が送信されます。
ここにDIALが使われています。

| Youtube |
| --- |
| ![スクリーンショット 2022-12-10 23.44.58.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96450/b5d49117-fb59-68df-abf9-0177a4cd6bbd.jpeg) |


### DIAL を使うために必要な登場人物

* DIALサーバー
  DIALプロトコルのサーバー側を実装した装置。
  通常はファーストスクリーン端末にある。

* ファーストスクリーンアプリ
  セカンドスクリーンアプリと接続するアプリ。
  DIALサーバーからの命令で起動や終了をする。
  ファーストスクリーン端末にある。

* セカンドスクリーンアプリ
  ファーストスクリーンアプリに接続するアプリ。
  DIALサーバーに対して、起動や終了してほしいファーストスクリーンアプリを命令する。
  セカンドスクリーン端末にある。

### ざっくりと技術的にみた接続されるまでの道のり

ファーストスクリーンアプリとセカンドスクリーンアプリが接続されるまでの道のりを書きます。

#### 前提

* ファーストスクリーン端末にあるDIALサーバーは起動済み
* セカンドスクリーン視点で記載。


#### 道のり

１. セカンドスクリーンアプリは、IPv4マルチキャストアドレス(239.255.255.250)とUDPポート(1900)に対してリクエスト(M-SEARCH リクエスト)をかける。

２. レスポンスの中に、DIALサーバーの詳細情報が記載されているURLがあるので、そのURLに対して Get リクエストをかける。

３. サーバーから受け取ったレスポンスのヘッダーに、`application-url` があるので、 `application-url + '/' + '${起動したいファーストアプリ名}` で POST リクエストをする。

４. ファーストスクリーン端末にて、起動したいファーストスクリーンアプリが立ち上がる。

-- ここまでが DIAL の機能

５. ファーストスクリーンアプリとセカンドスクリーンアプリが決めた接続方法（WebSocket, PIN Code etc...) で接続する。

６. 接続が確立したら、セカンドスクリーンアプリとファーストスクリーンアプリが相互にやりとりする。


## Fire TV Stick にインストールしている Youtube を起動したりしてみる

なんとなく DIAL の仕組みがわかったので、どのご家庭にもある Fire TV Stick で動作を確かめてみました。
やることは Fire TV Stick にインストールしている Youtube を起動/停止です。
コードは node で書いていきます。

※ Fire TV Stick に再生したい Youtube の動画を送信するについては、DIALの機能ではないようなのでそこは割愛します。

| どのご家庭にもある Fire TV Stick |
| --- |
| ![C30BAE5E-5B5A-4352-A164-A40AF59261B1 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96450/efed8559-cfbf-0071-1e04-b53329dab70b.jpeg) |

### まずは M-SEARCH 

以下のようなコードでスキャンをかけます.
重要なのは body　に `ST:urn:dial-multiscreen-org:service:dial:1` を入れることです。これがないと DIAL サーバーを見つけません。

```typescript
import dgram from 'dgram';

const body = Buffer.from(
  "M-SEARCH * HTTP/1.1\r\n" +
  "HOST:239.255.255.250:1900\r\n" +
  "MAN:\"ssdp:discover\"\r\n" +
  "MX:10\r\n" +
  "ST:urn:dial-multiscreen-org:service:dial:1 \r\n" +
  "\r\n"
);

const client = dgram.createSocket('udp4');
client.bind();

client.send(body, 0, body.length, 1900, '239.255.255.250');

client.on('message', (msg, remote) => {
  console.log('remote: ', remote);
  console.log('msg: ', msg.toString('utf8'));
});
```

スキャン結果は以下のようになりました。
LOCATION にURLがあります。

```
msg: HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
LOCATION: http://192.168.10.4:60000/upnp/dev/1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1/desc
EXT:
ST: urn:dial-multiscreen-org:service:dial:1
USN: uuid:6ad6cf12-efee-3e3e-b45a-b31dd0a0bdd2::urn:dial-multiscreen-org:service:dial:1
WAKEUP: MAC=c0:8d:51:bd:47:86;Timeout=35
SERVER: Linux/4.14.87 UPnP/1.0 Cling/2.0
```

### LOCATION の URL をリクエストしてみる

LOCATION の URL をリクエストしてみます。
GET リクエストするコードを書きました。

```typescript
import got from 'got';

(async () => {
  const data = await got.get('http://192.168.10.4:60000/upnp/dev/1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1/desc');

  console.log('res header: ', data.headers);
  console.log('res body: ', data.body);
})();
```

結果は以下になりました。
※ console.log を整形して記載しています。

body には fire tv stick の情報があることが確認できます。
header の中に、 `application-url` がありました。これで ファーストスクリーンアプリの起動等ができます。

```
// header
{
    server: 'Linux/4.14.87 UPnP/1.0 Cling/2.0',
    'content-type': 'application/xml',
    'application-url': 'http://192.168.10.4:8009/apps/',
    'access-control-allow-origin': '*',
    'content-length': '901',
    connection: 'keep-alive'
},


// body
<root xmlns="urn:schemas-upnp-org:device-1-0">
  <specVersion>
    <major>1</major>
    <minor>0</minor>
  </specVersion>
  <device>
    <deviceType>urn:dial-multiscreen-org:device:dial:1</deviceType>
    <friendlyName>hogehoge Fire TV</friendlyName>
    <manufacturer>Amazon</manufacturer>
    <modelName>AFTKA</modelName>
    <UDN>uuid:1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1</UDN>
    <serviceList>
      <service>
        <serviceType>urn:dial-multiscreen-org:service:dial:1</serviceType>
        <serviceId>urn:dial-multiscreen-org:serviceId:dial</serviceId>
        <SCPDURL>/upnp/dev/1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1/svc/dial-multiscreen-org/dial/desc</SCPDURL>
        <controlURL>/upnp/dev/1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1/svc/dial-multiscreen-org/dial/action</controlURL>
        <eventSubURL>/upnp/dev/1ad1cf33-efee-1e1e-b33a-b31dd0a2bdd1/svc/dial-multiscreen-org/dial/event</eventSubURL>
      </service>
    </serviceList>
  </device>
</root>
```


### Youtube の状態を確認したい

これで Fire TV Stick にインストールしているYoutubeを起動する準備が整いました。
まずは、Youtubeの状態を確認します。

以下のコードを作りました。
`http://192.168.10.4:8009/apps/ + 'YouTube'` に対して Get リクエストをかけます。

※ この `YouTube` という記載方法ですが、[Application Name](http://www.dial-multiscreen.org/dial-registry/namespace-database) で定めらています。他にも色々あるので有名所のアプリを起動したい場合は確認必須です。

また、Youtubeに関してですが、Header の `Origin` に何かしらの値を入れないとリクエストエラーになりました。
私は、WireShark で Chrome が Fire Stick TV にキャストしてほしいときに送っていた Origin を記載しました。

```typescript
import got from "got";

(async () => {
  const data = await got.get("http://192.168.10.4:8009/apps/YouTube", {
    headers: {
      Origin: "package:Google-Chrome.107.Mac-OS-X",
    },
  });

  console.log('res header: ', data.headers);
  console.log('res body: ', data.body);
})();
```

応答は以下になりました。
Fire Stick TV には Youtube がいるようです。安心しました。

```
<?xml version="1.0" encoding="UTF-8"?>
<service xmlns="urn:dial-multiscreen-org:schemas:dial" dialVer="2.2.1">
  <name>YouTube</name>
  <options allowStop="true" />
  <state>running</state>
  <link rel="run" href="run" />
  <additionalData>
    <screenId>3hdtm4tvt74coe9f8gr9r5d4mh</screenId>
    <testYWRkaXR>c0ef1ca</testYWRkaXR>
    <model>AFTKA</model>
    <theme>cl</theme>
    <brand>Amazon</brand>
    <deviceId>4c187576-350e-4c73-1234-6c545a7b2313</deviceId>
  </additionalData>
</service>
```

### Youtube の起動

post リクエストをするコードを書きました。

```typescript
import got from "got";

(async () => {
  const data = await got.post("http://192.168.10.4:8009/apps/YouTube", {
    headers: {
      Origin: "package:Google-Chrome.107.Mac-OS-X",
    },
  });

  console.log('res header: ', data.headers);
  console.log('res body: ', data.body);
})();
```

上記のコードを実行すると Fire TV Stick 上で Youtube が起動した。
レスポンスを見ると、body にはなにもないが、 header には以下の記載があった。

```
{
  'content-type': 'text/plain',
  date: 'Sat, 10 Dec 2022 09:52:59 GMT',
  'access-control-allow-origin': 'package:Google-Chrome.107.Mac-OS-X',
  vary: 'Origin',
  connection: 'keep-alive',
  'content-length': '0'
}
```

### Youtube の終了

delete リクエストをするコードを書きました。

```typescript
import got from "got";

(async () => {
  const data = await got.delete("http://192.168.10.4:8009/apps/YouTube/run", {
    headers: {
      Origin: "package:Google-Chrome.107.Mac-OS-X",
    },
  });

  console.log('res header: ', data.headers);
  console.log('res body: ', data.body);
})();
```

リクエストをおくると、Fire TV Stick 上で Youtubeが終了しました。
body には空文字みたいだが、 header には色々あるようです。

また、すでにYoutubeが終了している状態で、Deleteリクエストを送信するとエラーメッセージを受けとりました。

```
{
  'content-type': 'text/plain',
  date: 'Sat, 10 Dec 2022 09:56:42 GMT',
  'access-control-allow-origin': 'package:Google-Chrome.107.Mac-OS-X',
  vary: 'Origin',
  connection: 'keep-alive',
  'content-length': '0'
}
```


## まとめ

DIAL という仕様について調べてみました。
このプロトコルを使用することで、簡単に機器検索と、検索した機器にあるアプリの状態や起動ができることがわかりました。

実際に、Fire Tv Stick を使用してYoutubeを起動等を行いました。
2022年12月11日現在では、以下のことができることをわかりました。

```
Youtubeの状態リクエスト: /apps/YouTube [GET]
Youtubeの起動：        /apps/YouTube [POST]
Youtubeの終了：        /apps/YouTube/run [DELETE]
```

### 最後に

無事にアドベントカレンダーに記事を投稿することができました。
次回は、@krmtmint　さんです。とても楽しみにしています。

## 参考文献

* DIAL の仕様書: http://www.dial-multiscreen.org/home
* DIAL のGithub: https://github.com/Netflix/dial-reference
* mzyy94.com, DIALというネットワークプロトコル: https://www.mzyy94.com/blog/2021/08/03/dial-rest-protocol/
* ssdp.node.js: https://gist.github.com/chrishulbert/895382

---

[^1]: DIAL公式ではペアリングも手順が省かれるような図があるが仕様を読む限りだとアプリ同士の接続は、DIALの範疇ではなく個々のアプリになると思う。