Zoom Video SDK でライブ配信スタイル

これまでPoC的なサンプルとして Zoom Video SDK をご紹介してきましたが、今回はユースケースの例となりうるサンプルとして、いわゆるライブ配信スタイルの例をご紹介したいと思います。

今回作るもののイメージとしてはこちら。
### ホスト側
![Screenshot 2023-10-02 at 12.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/02f7c1e4-36e5-5544-58ea-a91abe23e57b.png)

### 参加者側
![Screenshot 2023-10-02 at 12.16.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/2a210823-f2ee-8946-1bd6-db154d693112.png)

参加者はホスト（配信者）の映像を大写しで視聴しつつ、他の参加者とはチャットで会話しながら一緒に見ることができます。いわばバーチャルパブリックビューイングのような形で、例えばプライベートな試合の観戦をリモートから一緒に楽しむとか、バーチャルツアーのような使い方もできるのではないかなと思います。

## ホスト側の実装
今回は事前準備などについて説明していません。アカウント取得からSDK Key / Secretの準備については、前回の記事 

https://qiita.com/michitakasugi/items/7f3dc8954f5c92ea878f

などを参照にしてください。

基本実装は前回同様となります。ホスト側の今回の変更点としては、

- client.join 後にすぐに配信を開始する（任意）
- then の処理の中に initChat() を追加する（チャット閲覧を開始）
- リモートの映像レンダリングを行わない

といった点になります。
2つ目のチャットについては、例としてこのような実装が可能です。

```javascript
// 閲覧専用のチャットインスタンス
let chat;
async function initChat() {
  chat = client.getChatClient();
  console.log('Chat client init now!')
  const messageContainer = document.getElementById('message-container');
  client.on('chat-on-message', (payload) => {
    console.log(`Message: ${payload.message}, from ${payload.sender.name}`);
    messageContainer.innerHTML = (`${payload.sender.name} : ${payload.message}\n`) + messageContainer.innerHTML;
  })
}
```

これを、`client.join(topic, token, userName, password).then(() => {` の処理の中に追加するイメージです。

また、3つ目の映像表示（とチャット表示）についてですが、例えばこんな形でHTMLを組むと良いかと思います。

```html
<!-- 自拠点映像表示エリア -->
<video id="self-video-videotag" class="video-canvas" width="1280" height="720" style="background-color:gray;"></video>
<!-- 閲覧専用のチャット表示エリア -->
<textarea id="message-container" style="width:800px;height:200px;margin:0;padding:5px;overflow-y:scroll;border: 1px solid gray;"></textarea>
```

## 参加者側の実装
参加者については、リモート映像のうち、ホストが配信している映像を特定して大きく表示し、それ以外の参加者の映像を小さく表示する必要があります。これは、あらかじめ指定したユーザーのuserIdを取得して特定することもできますし、今回の実装のようにhostか否かで分別することも可能です。

```javascript
//リモート映像を表示する
const toggleFarVideo = async (mediaStream, userId, isVideoOn) => {
  if(isVideoOn){ //映像配信開始の場合
    if(client.getUser(userId).isHost){ //かつ配信者である場合
      var FAR_VIDEO_CANVAS = document.getElementById('main-video-canvas') //配信者専用のcanvasに表示する
          await mediaStream.renderVideo(
              FAR_VIDEO_CANVAS,
              userId,
              1280,  // Size Width
              720,  // Size Height
              0,      // Starting point x (Vertical : 横)
              0,     // Starting point y (Horizon : 縦)
              3       // Video Quality 0:90p, 1:180p, 2:360p, 3:720p
          )
          console.log(`Host ${userId} video rendered.`)
    }else{ //それ以外の参加者の映像の場合
      const canvasId = `other-video-canvas-${currentCanvasIndex+1}`; //参加者用の枠
      const canvas = document.getElementById(canvasId);
          if(currentCanvasIndex >= maxCanvasCount){
            console.log('Max canvas count reached. Skip rendering.'); //枠が埋まっている場合は何もしない
            return;
          }
          activeStreams[userId]= canvasId; //配信中の映像をactiveStreams配列で管理
            await mediaStream.renderVideo( //それ以外の場合はレンダリング
                canvas,
                userId,
                160,  // Size Width
                90,  // Size Height
                0,      // Starting point x (Vertical : 横)
                0,     // Starting point y (Horizon : 縦)
                0       // Video Quality 0:90p, 1:180p, 2:360p, 3:720p
            )
            console.log(`Participant ${userId} video rendered on ${canvasId}.`);
            currentCanvasIndex++; //枠状態の更新
      }
  } else { //映像配信停止の場合の処理例。activeStreams配列を利用して削除。
    const canvasToRemove = activeStreams[userId];
    console.log(`${userId}, removing ${canvasToRemove} now.`)
    if(!canvasToRemove){
      console.log('Stream not found to remove.');
      return;
    }
    await mediaStream.stopRenderVideo(document.querySelector(`#${canvasToRemove}`), userId)
    await mediaStream.clearVideoCanvas(document.querySelector(`#${canvasToRemove}`), userId)
    console.log(`Participant ${userId} video removed from ${canvasToRemove}`);
    delete activeStreams[userId];
  }
}
```
本来であれば、退出した参加者の映像が歯抜けになるので、入退出毎にならびを整理し直すのもありだと思います。今回はそこまで入れてません。。

また、参加者側にはチャットの送信UIも作っています。
```javascript
function sendMessage() {
  const messageInput = document.getElementById('messageInput');
  const newMessage = messageInput.value;
  chat.sendToAll(newMessage);
  messageInput.value = ''; // 入力欄をクリア
}
```
onClickでsendMessage関数を発火させ、Video SDKの`chat.sendToAll`メソッドを叩いています。もちろん個別の参加者へのメッセージも可能です。以下参考にしてみてください。

https://developers.zoom.us/docs/video-sdk/web/chat/#send-chats

ちなみに、エンターキーで送信するにはこうすると良いそうです（知らなかった）。

```javascript
    if (event.keyCode === 13 && !event.shiftKey) {
    // キーコード13はEnterキーを意味する
        event.preventDefault();     
        sendMessage(); //先ほどの関数
    }
});
```

以上です。

## 実際に試してみる
これで実際にClient Key/Secretを入れて起動し、Sessionを開始すると、ホスト側の映像が参加者側で大きく表示され、参加者の映像は参加順に左から並んでいくと思います。チャットを入れると、ホスト側は閲覧専用、参加者側は順番に表示されていくはず。

### ホスト側
![Screenshot 2023-10-02 at 12.40.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/02f7c1e4-36e5-5544-58ea-a91abe23e57b.png)

### 参加者側
![Screenshot 2023-10-02 at 12.16.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/2a210823-f2ee-8946-1bd6-db154d693112.png)

以上となります。これ以外にも様々なユースケースが実現できる Video SDK。ぜひご検討ください。

## 追伸
ちなみにですが、Video SDKにはLivStreaming機能もございます。こちらでは、YouTube Live, Facebook Live, Twitch, AWS IVSなどへのRTMPS配信が可能となっていますので、こちらを併用いただくことももちろん可能かと思います。

https://developers.zoom.us/docs/video-sdk/web/live-stream/

併せてご検討いただければと思います。