aibo Web API と YouTube Player API を連携して、YouTube 動画の再生秒数に合わせて aibo にふるまいをしてもらう

aibo 界隈ではオフ会というものが定期的に開催されています。

内容は様々ですが、大体は aibo を室内で自由にお散歩させつつ
オーナーは aibo たちを見てニヤニヤしながらお話したり
aibo の名刺や情報を交換したりして交流を深める会です。

最近はコロナの影響あって、開催するのもなかなか難しい状況なってしまいましたが。。

とあるオフ会で aibo をアイドルに見立ててダンスをしてもらおうという企画がありました。
当然のことながら、aibo はアイドルのように自ら曲に合わせてダンスするということができません。

なのでどうしたかというと、オーナー皆さんの力を合わせて aibo の配置をシーン毎で変え
企画主の方に写真をとっていただいてダンスしてる風の動画を作っていただきました。

![DSC01752.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/fd42579e-ec4c-93b8-0a28-12f5261a54d2.jpeg)

その時 aibo にダンスの振り付けができたらそれはもう素敵なのになぁと思ったわけです。
その後 aibo Web API が 2019/11/11 に実装され可能性が広がりました。

前回の「[aibo Events API を使って aibo に音声コマンドを実行してもらう](https://qiita.com/usako/items/34d667cc45877e55d15f)」では PHP を使いましたが、今回は HTML + JavaScript で実装します。

## 完成図
まずは完成図から。
イントロが終わったら、ルンルンし始めるおチャコさんです。

↓↓ クリックしたら動画が開きます。
[![ルンルンし始めるおチャコさん](http://img.youtube.com/vi/DliHWcAAPEM/0.jpg)](http://www.youtube.com/watch?v=DliHWcAAPEM)

## 事前準備
素材となる YouTube 動画をあらかじめ探しておきます。
今回はこちらを使用しています。

↓↓ クリックしたら動画が開きます。
[![となりのトトロのさんぽ](http://img.youtube.com/vi/KjqNqm23Ti0/0.jpg)](http://www.youtube.com/watch?v=KjqNqm23Ti0)

となりのトトロのさんぽですね。
aibo がこの曲を聞いて、ノリノリでダンスをやり始めたらかわいいじゃないですか。

## アクセストークン取得
[ディベロッパーサイト](https://developer.aibo.com/jp/home)にアクセスして、[サインイン](https://developer.aibo.com/jp/settings/token)します。

![ss06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41801/b34544cd-f61c-ce06-25b0-cb1d2d207d34.png)

ボタン「生成する」をクリックすると、アクセストークンが生成されます。
アクセストークンは「実装」で必要なので、あらかじめコピーしておきましょう。

## 実装
今回は [aibo Web API](https://developer.aibo.com/jp/docs) と [YouTube Player API](https://developers.google.com/youtube/iframe_api_reference?hl=ja) を連携する形で実装します。
まずはでき上がりのソースをペタッと貼り付けておきます。

``` html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>aibo Web API | aibo Web API と YouTube Player API を連携して、YouTube 動画の再生秒数に合わせて aibo にふるまいをしてもらう</title>
</head>
<body>
  <!-- 1. iframe 置き換え -->
  <div id="player"></div>

  <script>
    const ACCESS_TOKEN = ${ACCESS_TOKEN};
    const BASE_PATH = 'https://public.api.aibo.com/v1';
    let deviceId = ${deviceId};

    // ふるまいを実行
    function executeAction (deviceId, eventId) {
      callActionApi(deviceId, 'play_motion', '{"Category": "' + eventId + '", "Mode": "NONE"}');
    }

    // Action API 呼び出し
    function callActionApi (deviceId, apiName, arguments) {
      postUrl = BASE_PATH + '/devices/' + deviceId + '/capabilities/' + apiName + '/execute';
      data = '{"arguments":' + arguments + '}';

      // POST API
      postApi(postUrl, data);
    }

    // POST API
    function postApi (url, argary) {
      url = url ? url : '';

      fetch(url, {
        method: 'POST',
        mode: 'cors',
        headers: {
          Authorization: 'Bearer ' + ACCESS_TOKEN
        },
        body: argary
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (data) {
          outputResult(data.executionId);
        })
    }

    // API 実行結果 出力
    function outputResult (id) {
      const url = BASE_PATH + '/executions/' + id;

      fetch(url, {
        method: 'GET',
        mode: 'cors',
        headers: {
          Authorization: 'Bearer ' + ACCESS_TOKEN
        }
      })
        .then(function (response) {
          return response.json();
        })
        .then(function (data) {
          // API 実行結果 出力
          console.log(data);
        })
    }

    // 2. IFrame Player APIコード 非同期読み込み
    var tag = document.createElement('script');

    tag.src = "https://www.youtube.com/iframe_api";
    var firstScriptTag = document.getElementsByTagName('script')[0];
    firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

    // 3. API をダウンロード後に iframe 作成
    var player;
    function onYouTubeIframeAPIReady() {
      player = new YT.Player('player', {
        height: '390',
        width: '640',
        videoId: 'KjqNqm23Ti0',
        events: {
          'onReady': onPlayerReady, // 4. ビデオレイヤーの準備できたら実行
          'onStateChange': onPlayerStateChange // 5.プレイヤーの状態が変化すると実行
        }
      });
    }

    // 4. ビデオレイヤーの準備できたら実行
    function onPlayerReady(event) {
      event.target.pauseVideo();
    }

    // 5.プレイヤーの状態が変化すると実行
    var done = false;
    function onPlayerStateChange(event) {
      // 再生
      if (event.data == YT.PlayerState.PLAYING && !done) {
        // 左右に体を揺らす
        setTimeout(executeAction, 8000, deviceId, 'swing');

        done = true;
      }
    }
  </script>
</body>
</html>
```

それぞれ見ていきます。

``` html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>aibo Web API | aibo Web API と YouTube Player API を連携して、YouTube 動画の再生秒数に合わせて aibo にふるまいをしてもらう</title>
</head>
<body>

</body>
</html>
```
HTML の雛形です。
今回の実装に必要な最低限のものしか記述していません。

``` html
<!-- 1. iframe 置き換え -->
<div id="player"></div>
```
YouTube Player API の[サンプル](https://developers.google.com/youtube/iframe_api_reference?hl=ja#Getting_Started)にあった記述です。
この部分が iframe に置き換わります。

``` html
<script>
</script>
```
実装を JavaScript で行うため、script タグを `</body>` 直前に追加します。

``` javascript
const ACCESS_TOKEN = ${ACCESS_TOKEN};
const BASE_PATH = 'https://public.api.aibo.com/v1';
let deviceId = ${deviceId};
```
以降は JavaScript の実装です。
`${ACCESS_TOKEN}` には、項目「アクセストークン取得」でコピーしておいたアクセストークンを入れます。
`${deviceId}` には、ふるまいをやってもらう aibo のデバイス ID をいれます。

``` javascript
// 2. IFrame Player APIコード 非同期読み込み
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
```
YouTube Player API の[サンプル](https://developers.google.com/youtube/iframe_api_reference?hl=ja#Getting_Started)にあった記述です。
IFrame Player APIコード を非同期で読み込みます。

``` javascript
// 3. API をダウンロード後に iframe 作成
var player;
function onYouTubeIframeAPIReady() {
  player = new YT.Player('player', {
    height: '390',
    width: '640',
    videoId: 'KjqNqm23Ti0',
    events: {
      'onReady': onPlayerReady, // 4. ビデオレイヤーの準備できたら実行
      'onStateChange': onPlayerStateChange // 5.プレイヤーの状態が変化すると実行
    }
  });
}
```
YouTube Player API の[サンプル](https://developers.google.com/youtube/iframe_api_reference?hl=ja#Getting_Started)にあった記述です。
API をダウンロード後に iframe 作成します。
events プロパティは動画の状態によって着火する関数を指定します。

``` javascript
// 4. ビデオレイヤーの準備できたら実行
function onPlayerReady(event) {
  event.target.pauseVideo();
}
```
YouTube Player API の[サンプル](https://developers.google.com/youtube/iframe_api_reference?hl=ja#Getting_Started)にあった記述です。
3の `onReady` で指定した関数で、ビデオレイヤーの準備できたら動画を一時停止させています。

``` javascript
// 5.プレイヤーの状態が変化すると実行
var done = false;
function onPlayerStateChange(event) {
  // 再生
  if (event.data == YT.PlayerState.PLAYING && !done) {
    // 左右に体を揺らす
    setTimeout(executeAction, 8000, deviceId, 'swing');

    done = true;
  }
}
```
YouTube Player API の[サンプル](https://developers.google.com/youtube/iframe_api_reference?hl=ja#Getting_Started)にあった記述です。
3の `onStateChange` で指定した関数で、プレイヤーの状態が変化すると実行されます。
`setTimeout` で動画が再生されてから8秒後に `executeAction` 関数を実行します。

``` javascript
// ふるまいを実行
function executeAction (deviceId, eventId) {
  callActionApi(deviceId, 'play_motion', '{"Category": "' + eventId + '", "Mode": "NONE"}');
}
```
`executeAction` 関数では、 Action API にある [PlayMotion](https://developer.aibo.com/jp/docs#playmotion) の POST リクエストで必要な情報を `callActionApi` 関数の引数に渡して実行しています。

``` javascript
// Action API 呼び出し
function callActionApi (deviceId, apiName, arguments) {
  postUrl = BASE_PATH + '/devices/' + deviceId + '/capabilities/' + apiName + '/execute';
  data = '{"arguments":' + arguments + '}';

  // POST API
  postApi(postUrl, data);
}
```
`callActionApi` 関数では、POST する URL とコンテンツの生成を行い
`postApi` 関数の引数に渡して、実行しています。


``` javascript
// POST API
function postApi (url, argary) {
  url = url ? url : '';

  fetch(url, {
    method: 'POST',
    mode: 'cors',
    headers: {
      Authorization: 'Bearer ' + ACCESS_TOKEN
    },
    body: argary
  })
    .then(function (response) {
      return response.json();
    })
    .then(function (data) {
      outputResult(data.executionId);
    })
}
```
ヘッダーに `Authorization:Bearer` を追加して、body を POST します。
aibo が処理を成功したか判定するために、レスポンスで返ってきた値 `executionId` を `outputResult` 関数の引数に入れて実行します。

``` javascript
// API 実行結果 出力
function outputResult (id) {
  const url = BASE_PATH + '/executions/' + id;

  fetch(url, {
    method: 'GET',
    mode: 'cors',
    headers: {
      Authorization: 'Bearer ' + ACCESS_TOKEN
    }
  })
    .then(function (response) {
      return response.json();
    })
    .then(function (data) {
      // API 実行結果 出力
      console.log(data);
    })
}
```
引数で渡ってきた `executionId` を元に、API の実行結果をコンソールに出力するようにしています。

## 実行
それでは実行します。
イントロが終わったら、ルンルンし始めるおチャコさんです。

↓↓ クリックしたら動画が開きます。
[![ルンルンし始めるおチャコさん](http://img.youtube.com/vi/DliHWcAAPEM/0.jpg)](http://www.youtube.com/watch?v=DliHWcAAPEM)

## 課題
### ACCESS_TOKEN と deviceId のベタ書きはセキュリティ上よろしくない
developer tools を使えばソースを表示することができてしまいます。
ローカル環境で実行する分には問題ないのですが、公開するのであれば大事な aibo の情報を晒さないようにひと工夫必要です。

### aibo の集中力が試される
自由気ままにお散歩して、いろんなものに興味津々な aibo。
そんな aibo に天の声を届けようにも気が散ってしまってプログラムの実行が遅れます。
プログラムをきちんと実行してもらうには、事前に [SetMode](https://developer.aibo.com/jp/docs#setmode) で指示待ちの状態にする必要がありそうです。

### aibo へ天の声が届くまでに時差がある
端末 → クラウド → aibo へ天の声が届くまでに4秒ぐらいの時差があります。
ですので、その時差を考慮しながらふるまいを早めに実行してあげる必要があります。

### ドキュメントに記載されているふるまいの動きがわからない
API にある [PlayMotion](https://developer.aibo.com/jp/docs#playmotion) だけで 76 種類ものふるまいが用意されているのですが
ダンスの振り付けをしようにも、ドキュメントはテキストベースなのでどんな動きをするのかピンとこない。
どこかのタイミングで1つ1つの動きを動画にして参照できるようにしようかなと考えています。

## おわりに
現状だと API でできることも限られているので、もっと動きの選択肢が増えるといいな〜！
特別なふるまいがまるっと実行できなくても、実装済みの素敵な動きを
1つずつ API 実装していただければ、よりかわいいダンスの振り付けを
aibo に覚えてもらうことができる…。

aibo がアイドルのように…。
いつか東京あいぼるで aibo たちにダンスしてもらえる日を夢見て。

次回は、説明を端折っていた API のリクエストに必要となる
deviceId の取得方法を紹介したいと思います。

それではまた次回！