映画館のスクリーンでYouTubeが見られるクソアプリ

[クソアプリ Advent Calendar 2020](https://qiita.com/advent-calendar/2020/kuso-app) 14日目の記事です。

# 前置き
おはようございます。DE-TEIUです。
微力ながら今年もクソアプリの普及のために投稿させていただきます。
来年あたり「クソアプリ」が流行語大賞に選ばれると良いですね。良くはないですね。

## 過去にアドベントカレンダー用に作ったクソアプリ
- [p5.jsで作ったダメなソリティア](https://qiita.com/de_teiu_tkg/items/c938fbb52218185659e7)
- [絶対に作業できない作業用BGMサイト](https://qiita.com/de_teiu_tkg/items/6ebcef3550507258edaf)
- [社会に一石を投じるクソアプリ開発](https://qiita.com/de_teiu_tkg/items/e82870eca1f265807dbe)
- [鼻毛が生えるカメラアプリ](https://qiita.com/de_teiu_tkg/items/213aa2ade2ef840a33b5)

# 成果物
今年は感染症の蔓延を防ぐために外出を自粛せざるを得ない時期も長く、映画館に足を運ぶ機会が減った、という方も大勢いらっしゃる事と思います。

今回は少しでもそんな皆様の助けとなるようなクソアプリを作成いたしました。
自宅にいながら映画館で好きなYouTube動画を観ているような気分に浸れます。

[劇場版YouTube](https://theater.vercel.app/)

任意のYouTube動画のURLをこのWebアプリのURL入力欄にに貼り付けて再生して下さい。
左上のボタンでフルスクリーンにしたりもできます。

# 映画館でYouTubeが
見られたと思います。

![録画_2020_11_24_22_11_55_926.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173272/0b9c13e2-007a-aa08-79e7-e83e36ad9daf.gif)

今時ならもしかしたらVRで似たようなサービスがあるかもしれませんが、このWebアプリならブラウザだけで完結するのでお手軽です。多分。

# 解説
以下、実装の解説等です。
- [Parcelを使ったWebアプリ開発](#parcelを使ったwebアプリ開発)
- [YouTube PlayerをWebアプリに埋め込む](#youtube-playerをwebアプリに埋め込む)
- [映画館の映像のスクリーン部分を透過する](#映画館の映像のスクリーン部分を透過する)

##Parcelを使ったWebアプリ開発

[Parcel](https://ja.parceljs.org/)とは、JavaScriptのソースをモジュール化して管理できるモジュールバンドラーの一種です。同じモジュールバンドラーである[webpack](https://webpack.js.org/)と比較すると、Parcelは設定ファイルの準備などが不要で、インストールすればすぐに使えるようになります。
(裏を返せば、あまり細かい設定ができないという事でもあります)

~~爆速でクソWebアプリを作れるのでとても重宝しています~~

[インストール方法](https://ja.parceljs.org/getting_started.html)は公式を見るのが良いでしょう。

補足として、**Parcelのビルド対象に含めたくない静的ファイルがある**場合、[parcel-plugin-static-files-copy](https://github.com/elwin013/parcel-plugin-static-files-copy#readme)もインストールしておくと良いです。これを入れておくと、staticフォルダ内の静的ファイルを、Parcelのビルド実行後に生成されたフォルダ(デフォルトではdist)にコピーしてくれるようになります。

##YouTube PlayerをWebアプリに埋め込む
YouTubeでは、任意のページにプレーヤーを埋め込むためのAPIが公開されています。
詳細は[公式のドキュメント](https://developers.google.com/youtube/player_parameters?hl=ja)を見るのが早いですが、この記事ではWebページにYouTubeプレーヤーを埋め込んで再生できるようにするところまでの手順を解説していきます。

### YouTubeプレーヤー用JavaScriptコードを呼び出す
プレーヤーを埋め込みたいHTMLのheadタグ内にこの１行を追加

```html
<script src="https://www.youtube.com/iframe_api"></script>
```

bodyタグ内の適当な位置にid付きのdiv要素を追加(中身は空で良いです)

```html
<div id="player"></div>
```

JavaScriptでプレーヤーを生成するコードを追加

```javascript
const origin = location.protocol + "//" + location.hostname + "/";
//動画IDは、YouTube動画のURL https://www.youtube.com/watch?v=...... の......の部分です
const movieId = "csrP6E9lUuY";
const player = new YT.Player("player", {
  height: "360",
  width: "640",
  videoId: movieId,
  playerVars: { //各種パラメータ
    enablejsapi: "1",
    origin: origin,
    modestbranding: "1",
    fs: "0",
  },
  events: {
    onReady: () => { //プレーヤー生成完了時に発火するイベント
      player.playVideo(); //動画を再生
    },
  },
});
```

上記のコードが実行されたタイミングで、htmlファイルに記述した

```html
<div id="player"></div>
```
の要素内にプレーヤーが埋め込まれて動画の再生が始まります。

##映画館の映像のスクリーン部分を透過する

今回開発したアプリでは、YouTube動画の上に映画館の動画をかぶせているわけですが、その際に映画館のスクリーンの部分を透過しています。今回使用した動画データは透過(アルファチャンネル)の設定がされていないため、実装側でどうにかする必要があります。というわけで、動画をcanvas要素に描画→canvasの一部(スクリーン部分)を透過するという手法で実現しました。

### 動画をcanvasに描画

まずHTML内にvideo要素とcanvas要素を追加
(cssの設定などは割愛しますが、少なくともvideo要素にはdisplay:none;を適用させておきましょう)

今回は、video要素に表示したデータを一旦id="raw-canvas"のcanvasに書き込み、その後にそのcanvasからスクリーン部分を透過した映像をid="view-canvas"に書き込む、という実装にしています。
(用意するcanvasは1つでも実装可能ですが、canvasで画像処理をやる場合、個人的には加工前と加工後でcanvasを分けておいたほうがデバッグがしやすいと思っています)

```html
<video id="cinema" style="display:none;" loop muted playsinline></video>
<canvas id="raw-canvas" style="display:none;">
<canvas id="view-canvas">
```

JavaScript側で動画ファイルを読み込んで再生

```javascript
const cinema = document.getElementById("cinema");
//動画のメタデータ読み込み完了時のイベントを設定
cinema.onloadedmetadata = () => {
  initializeCanvas(cinema);//キャンバスを初期化(後述)
  cinema.play(); //動画を再生する
};
cinema.src = './cinema.mp4'; //動画ファイルのパス
```

これで動画は再生できます。後はinitializeCanvasメソッドの中を作っていきます。

```javascript
const initializeCanvas = (cinema) => {
  const rawCanvas = document.getElementById("raw-canvas");
  const rawContext = rawCanvas.getContext("2d");
  const viewCanvas = document.getElementById("view-canvas");
  const viewContext = viewCanvas.getContext("2d");
  
  // canvasのサイズを動画に合わせる
  rawCanvas.width = cinema.videoWidth;
  rawCanvas.height = cinema.videoHeight;
  viewCanvas.width = rawCanvas.width;
  viewCanvas.height = rawCanvas.height;

  const updateCanvas = () => {
    // 動画をcanvasに描画
    rawContext.drawImage(cinema, 0, 0, rawCanvas.width, rawCanvas.height);
    // canvasに描画した画素データを配列で取り出す
    const imageData = rawContext.getImageData(0, 0, rawCanvas.width, rawCanvas.height).data;
    // canvasの画素データを新たに生成
    let dst = viewContext.createImageData(viewCanvas.width, viewCanvas.height);
    // 画素データを1ピクセルずつチェック
    for (let i = 0; i < dst.data.length; i += 4) {
      dst.data[i] = imageData[i]; // R:赤
      dst.data[i + 1] = imageData[i + 1]; // G:緑
      dst.data[i + 2] = imageData[i + 2]; // B:青
      const y = i / 4 / rawCanvas.width; //画素データのY座標

      // Y座標が動画データのスクリーン部分の座標あたりで、その座標の画素データが白に近い場合
      if (imageData[i] >= 200 && imageData[i + 1] >= 200 
        && imageData[i + 2] >= 200 && y < (250 * cinema.videoHeight) / 342
      ) {
        dst.data[i + 3] = 0; //その座標を透過させる
      } else {
        dst.data[i + 3] = 255; //透過しない
      }
    }
    viewContext.putImageData(dst, 0, 0); //canvasに画素データを書き込む
    requestAnimationFrame(updateCanvas); //次のフレームでupdateCanvasメソッドを再び呼び出す
  };
  updateCanvas();
};
```
こうすることで毎フレームupdateCanvasメソッドが実行されて、canvasの表示がアップデートされます。

# まとめ
やっぱり実際の映画館で観たいですね。


# 参考
- [[HTML5] フルスクリーンの開始と解除 - ねこの足跡R](https://blog.katsubemakito.net/html5/fullscreen)
- [YouTube 埋め込みプレーヤーとプレーヤーのパラメータ &nbsp;|&nbsp; YouTube IFrame Player API](https://developers.google.com/youtube/player_parameters?hl=ja)
- [canvasでクロマキー合成っぽいことしてみる - Qiita](https://qiita.com/keiskimu/items/a4fc9ebb338906b9813e)