ブラウザで映像と音声を自在に操ろう、そして録画もね（CanvasとWebAuidoを使って）

# はじめに

この記事は、[「WebRTCを試すときにオッサンが映り続ける問題に対処する」](https://qiita.com/massie_g/items/5a6c4b69374d5997dc37)、[「WebRTCオッサン問題を超えて。ローカルファイルを自在に操りたい」](https://qiita.com/massie_g/items/0635a212f5860fe59047)の続編です。

また、[Web Music Demo Party #1](https://connpass.com/event/65499/) のLT用の記事でもあります。

# 自己紹介

* @massie_g （まっしー）
* [WebRTC Meetup Tokyo](https://atnd.org/groups/webrtc) の主催者の一人です


# デモ

## 環境

* GitHub pages https://mganeko.github.io/webrtcexpjp/tool/switch_to_stream_rec.html
* ソースコード https://github.com/mganeko/webrtcexpjp/blob/master/tool/switch_to_stream_rec.html
* デモに使った映像は [http://動画素材.com/](http://動画素材.com/) のものを利用させていただきました
* デモに使った音楽は、GarageBand のループを適当に繋ぎました

## 手順

* ローカルにあらかじめ映像ファイルx2と、音声ファイルx2 を準備
* ファイルを指定
    * ローカルにある映像ファイル1を、左半分の領域の、左から1番目のビデオ(Video)領域にドラッグ＆ドロップ
    * ローカルにある映像ファイル2を、左半分の領域の、左から2番目のビデオ(Video2)領域にドラッグ＆ドロップ
    * ローカルにある音声ファイル1を、右半分の領域の、左から1番目のオーディオ(Audio)領域にドラッグ＆ドロップ
    * ローカルにある音声ファイル2を、右半分の領域の、左から2番目のオーディオ(Audio2)領域にドラッグ＆ドロップ
    * 参考操作イメージ（音声1つだけのケース）
    * ![file select](https://camo.qiitausercontent.com/7ba013645260ae5b5e018730733dfff88cd72132/68747470733a2f2f6d67616e656b6f2e6769746875622e696f2f7765627274636578706a702f746f6f6c2f64656d6f2f64656d6f5f73746172742e676966)
* ミックス開始
    * 上下中央あたりの [Start] ボタンをクリック
    * その上のスライダーを左右に動かすと、映像1/音声1 ←→ 映像2/音声2 を指定した割合で表示
    * swipe / stripe / dissolve で切り替え方法を選択可能
    * [Full Screen]ボタンをクリックすると、ミックした結果を全画面表示
    * 参考操作イメージ
    * ![change_video](https://camo.qiitausercontent.com/550a7925ec6b93b9a9d278217db98ec9cf6ad9ba/68747470733a2f2f6d67616e656b6f2e6769746875622e696f2f7765627274636578706a702f746f6f6c2f64656d6f2f64656d6f5f666164652e676966)


# 仕組み

## 映像(video)ファイルの変換

基本的な流れは[「WebRTCを試すときにオッサンが映り続ける問題に対処する」](https://qiita.com/massie_g/items/5a6c4b69374d5997dc37) と同じです。

* ユーザにローカルの動画ファイルを指定してもらう（ファイル選択 or ドラッグ＆ドロップ）
* FileオブジェクトからURLを生成 (window.URL.createObjectURLを利用）
* それをVideoタグで再生
* 継続的にCanvasタグに転写 (window.requestAnimationFrameを利用）
    * 今回、映像が2つあり、スライダーの値によって描画を制御しています
* 録画用に、CanvasタグからMediaStreamを取り出す (Canvas.captureStreamを利用）

![switch_2_video_to_stream2.png](https://qiita-image-store.s3.amazonaws.com/0/19403/930340d0-4622-cd80-eca0-6658842cc036.png)


## 映像の合成

2つの映像をスライダーの値にしたがって合成しています。その方法は[「WebRTCオッサン問題を超えて。ローカルファイルを自在に操りたい」](https://qiita.com/massie_g/items/0635a212f5860fe59047)の方法そのままです。

### スワイプの場合

* スライダーが0の時はvideo 1の内容を、100の時はvideo 2の内容をCanvasに転写しています
* あいだの場合は、その割合に応じてvideoの一部を切り出して、 drawImage()で転写します
    * 30ならvideo 1が70%、video 2が30%の比率にしています

<img width="698" alt="swipe30.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/8d76c445-4dad-d08a-bc4f-b8eacd4d55c7.png">

```js:swipe
      let video1Width = Math.round((100- faderValue)/100.0 * localVideo.videoWidth);
      let video2Width = Math.round(faderValue/100.0 * localVideo2.videoWidth);
      let canvasWidth1 = Math.round((100- faderValue)/100.0 * duplicateCanvas.width);
      let canvasWidth2 = Math.round(faderValue/100.0 * duplicateCanvas.width);

      if (canvasWidth1 > 0) {
        ctxDuplicate.drawImage(localVideo, 0, 0, video1Width, localVideo.videoHeight,
          0, 0, canvasWidth1 , duplicateCanvas.height
        );
      }

      if (canvasWidth2 > 0) {
        ctxDuplicate.drawImage(localVideo2, (localVideo2.videoWidth - video2Width), 0, video2Width, localVideo2.videoHeight,
          (duplicateCanvas.width - canvasWidth2), 0, canvasWidth2 , duplicateCanvas.height
        );
      }
```

### ストライプの場合

全体を10個に分割し、その1列ごとにスワイプと同じ処理を行っています。
<img width="730" alt="stripe30.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/6f2d9ee7-4fad-7728-561d-523eae727ebf.png">

### ディゾルブの場合

片方を透けさせながらもう片方に切り替えるディゾルブの処理はについては、globalAlphaという指定を使って実現しています。
<img width="717" alt="dissolve30.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/c941746c-70ef-7d21-08d3-dd50fec2cbfe.png">

```js:dissolve
      let video1Alpha = (100 - faderValue)/100.0;
      let video2Alpha = (faderValue)/100.0;
      
      ctxDuplicate.globalAlpha = video1Alpha;
      ctxDuplicate.drawImage(localVideo, 0, 0, localVideo.videoWidth, localVideo.videoHeight,
        0, 0, duplicateCanvas.width , duplicateCanvas.height
      );
      
      ctxDuplicate.globalAlpha = video2Alpha;
      ctxDuplicate.drawImage(localVideo2, 0, 0, localVideo2.videoWidth, localVideo2.videoHeight,
        0, 0, duplicateCanvas.width , duplicateCanvas.height
      );
```

変換したメディアストリームは、RTCPeerConnectionで通信相手に送ることができます。（※ new MediaStream() でオブジェクトを作っているため、現状ではFirefoxではエラーになってしまいます）

## 音声(audio)ファイルの変換

こちらも基本的な流れは[「WebRTCを試すときにオッサンが映り続ける問題に対処する」](https://qiita.com/massie_g/items/5a6c4b69374d5997dc37) と同じです。

* ユーザにローカルの音声ファイルを指定してもらう（ファイル選択 or ドラッグ＆ドロップ）
* FileReaderでファイルを読み込む
* WebAudio API デコード、再生
    * 今回、音声が2つあり、スライダーの値によってゲインを制御しています
* 録音用に、WebAudio API の createMediaStreamDestination でMediaStreamを取り出す

![switch_2_audio_to_stream.png](https://qiita-image-store.s3.amazonaws.com/0/19403/6d9b2847-8bc4-0c12-3ca0-1e9a64da77d5.png)

## 録画/録音

* Canvasから取り出した映像のMediaStreamから、videoTrackを取り出す
* おなじく、WebAudioで作ったMediaStreamから、audioTrackを取り出す
* 2つを新しいMediaStreamにまとめる
* MediaRecorderで録画/録音する
* 実際の録画処理はこちらを参考にしてください
    * [WebRTCハンズオン MediaRecorder編](https://qiita.com/massie_g/items/ecfc16018210441051b4)

<img width="960" alt=" mix_rec.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/6b5a6db2-92c9-2687-d8af-043db4f4899f.png">


# おまけ Leap Motion

今回のデモでは [Leap Motion](https://www.leapmotion.com) を使ってスライダーを操作してみました。久しぶりに使ってみましたが、ドライバが随分進化していて、安定して手の動きを取れるようになってと感じました。

* Leap Motion のドライバを入れると、サービスプロセスが起動
* そのプロセスが、USBからLeap Motion のデータを読み込む
* プロセスは WebSocket サーバーになっている
    * [デフォルトでは ws://localhost:6437/](https://developer.leapmotion.com/documentation/javascript/api/Leap_Classes.html#leap-namespace)
* ブラウザで利用するSDKを使うと、WebSocketでプロセスに接続し、データを取得する
* Leap Motion 版のソースは[こちら GitHub](https://github.com/mganeko/webrtcexpjp/blob/master/tool/switch_to_stream_leap.html) 

```js:LeapMotion
  // https://developer.leapmotion.com/documentation/javascript/devguide/Leap_Guides2.html
  // ======== LEAP MOTION =============
  // X: -left +righ
  // Y: -down +up
  // Z: -push +pull

  let controller = Leap.loop(function(frame){
    if(frame.hands.length > 0)
    {
        let hand = frame.hands[0];
        const position = hand.palmPosition;
        const velocity = hand.palmVelocity;
        const direction = hand.direction;
        const grab = hand.grabStrength;

        // --- handle X -----
        yourLeapFunction(grab, position[0], velocity[0], direction[0]);
    }
  });
```