新SkyWay チュートリアルの次に覚えること

https://qiita.com/akid3020/items/69752609714272fb8dec

↑こちらに書いたとおりチュートリアルをざっとやりまして、もう少しいろいろ試してみようというときに、覚える必要があったポイントメモの共有になります。

## RoomType: p2pとsfu

`SkyWayRoom.Create` パラメータの `type` として、`p2p` または `sfu` を指定することができます。

WebRTCの技術の違いで、P2P（Peer-to-peer）が端末同士で直接通信を行うのに対し、SFU（Selective Forwarding Unit）はサーバーを介して通信を行う。

なので、ざっくりいってP2Pが少人数の通信に向いているのに対し、SFUは多人数での通信に向いています。

[SkyWayのドキュメント](https://skyway.ntt.com/ja/docs/user-guide/javascript-sdk/overview/)ではP2Pルームで「ユーザーが快適に通話できる人数は 4 人まで」と記載されていますが、端末間で直接やりとりするということは、当然ユーザーの数だけデータの送受信を行う必要があるので、基本的には3人以上になる可能性があるなら最初からSFUを選択するのが良いと思われます。

ただし、SkyWayには **SFU RoomでDataStreamが使えない** という制約があるようなので注意。

## DataStreamで任意のデータをやりとりする

チュートリアルには VideoStream と AudioStream しか登場しませんが、DataStream を使うと、SkyWayの機能を使ってユーザー間で任意の情報をやりとりすることができます。

一番イメージしやすいのは、ビデオ通話のチャット機能ですね。

DataStream をつかうためには、以下の実装が必要。

* `SkyWayStreamFactory.createDataStream` で作成した `LocalDataStream` を、Room参加時に publish する
    * データを送る際は `LocalDataStream.write`

```ts
// ルーム入室時
const me = await room.join();
const dataStream = await SkyWayStreamFactory.createDataStream();
await me.publish(dataStream);

// 任意のデータ
const data = { message: "こんにちは" };
// データ送信
dataStream.write(data);
```

* 他のユーザーの publication を subscribe する
    * データを受信する際は、subscribe で返された `RemoteDataStrem` に対して `RemoteDataStrem.onData.add` でコールバックを登録する
    * データの受信購読を解除するには、 `RemoteDataStrem.onData.add` で返される `removeListener` をコールする

```ts
const me = await room.join();

room.onStreamPublished.add(async (e) => {
    // DataStreamを購読
    if (e.publication.stream?.contentType === "data") {
        const { stream } = await me.subscribe(e.publication.id);
        // ここは必ずRemoteDataStreamになるはず
        if (stream.contentType === "data") {
            // データ受信時のcallbackを登録
            const { removeListener } = stream.onData.add(data => {
                // 受信データ
                const receivedData = data as { message: string };
            });

            // 購読解除するとき
            removeListener();
        }
    }
});
```


## ルームを作るか、探すかを明示的に指定する

チュートリアルでは、 `SkyWayRoom.FindOrCreate` を使っていますが、実際には、新規作成するか、既存の部屋に入室するかは明示的に選択させるケースが多いと思います。

新規作成するときは、[SkyWayRoom.Create](https://javascript-sdk.api-reference.skyway.ntt.com/room/classes/SkyWayRoom.html#Create)を使う。

```ts
const room = await SkyWayRoom.Create(context, {
    type:"p2p",
    name:"[ルーム名]"
});
```

指定した名前の部屋が既に存在する場合は、以下のようなErrorがthrowされるようでした（オブジェクトの一部を抜粋）。
TypeScriptの型としては `SkyWayError` 型というのに当てはまるみたい。

```js
{
  name: 'channelNameDuplicated',
  message 'その名前のChannelはすでに存在します A channel with that name already exists',
  info: {
    name: 'channelNameDuplicated',
    detail: 'その名前のChannelはすでに存在します A channel with that name already exists',
    solution: '別の名前を使ってchannelを作成してください'
  }
}
```

既存の部屋を探して入室する場合は、[SkyWayRoom.Find](https://javascript-sdk.api-reference.skyway.ntt.com/room/classes/SkyWayRoom.html#Find)。

```ts
const room = await SkyWayRoom.Find(context, {
    name: "[ルーム名]"
}, "p2p");
```

指定した名前の部屋が見つからない場合は、以下のようなErrorがthrowされます。

```js
{
  name: 'channelNotFound',
  message '参照しようとしたchannelは存在しません The channel you tried to reference does not exist.',
  info: {
    name: 'channelNotFound',
    detail: '参照しようとしたchannelは存在しません The channel you tried to reference does not exist.',
    solution: 'channelIdやchannelNameが正しいか確かめてください Make sure that the Channel id and channel name are correct.'
  }
}
```

## ルームに任意の情報を付与する

ルームを作成する際に、任意の情報（ルームの説明、参加者に通知したい情報など）を付与したいことがあると思います。

そのための機構として、ルーム作成時に `metadata` を指定できるようになっています。
送受信の際は文字列なので、JSONを文字列化して送るなどするのが良いでしょう。

```ts
interface RoomMetaData {
    description: string;
}

const metaData: RoomMetaData = {
    description: "ようこそ！";
}

const room = await SkyWayRoom.Create(context, {
    type:"p2p",
    name:"[ルーム名]",
    metadata: JSON.stringify(metaData)
});
```

```ts
const room = await SkyWayRoom.Find(context, {
    name: "[ルーム名]"
}, "p2p");

const metaData = JSON.parse(room.metadata) as RoomMetaData;
```

## ルームの人数に上限を設ける

ルームに参加できる人数は4人までとか、制限をかけたいケースもあると思います。

SkyWayには直接的にこれを制御する機能はなさそうだったので、自前で実装する必要がありそう。

`SkyWayRoom.Find` で見つけたルームに対して、人数を確認して上限に達していないときだけ入室させる。

```ts
const room = await SkyWayRoom.Find(context, {
    name: "[ルーム名]"
}, "p2p");

if (room.members.length >= 4) {
    // ユーザーへの通知とか
    showMessage("人数が上限に達しています");
} else {
    await room.join();
}
```

ただし、この方法だとクライアント側でチェックしているだけなので、ローカルでコードを書き替えればなんとでもなってしまいます。
あくまで簡易的なガードなので注意。

## ユーザーの退室を管理する

チュートリアルでは触れていないですが、普通は入室ボタンがあれば退室ボタンがあるでしょう。

退室ボタンの実装としては、[LocalRoomMember.leave](https://javascript-sdk.api-reference.skyway.ntt.com/room/interfaces/LocalRoomMember.html#leave)を実行します。

```ts
// 入室時
const me = await room.join();

// 退室時
const onLeave = async () => {
    // publishしたものは一応unpublishしておくほうがよさそう？
    for (const pub of me.publications) {
        await me.unpublish(pub.id);
    }

    await me.leave();
}
```

他のメンバーが退室したことを検知するには、[Room.onMemberLeft](https://javascript-sdk.api-reference.skyway.ntt.com/room/interfaces/Room.html#onMemberLeft)があるので、この中でvideo Elementの破棄などをすればよさそう。

1点問題として、一度退室してから同じ部屋に入室しようとすると、ルームメンバーのsubscribeをするときに既に存在しないメンバー（前回の自分）をsubscribeしようとしてエラーになってしまいました。

どう判断するのが正しいかわからないですが、とりあえず以下のように回避しました。

```ts
let stream: RemoteDataStream | RemoteVideoStream | RemoteAudioStream;
try {
    stream = (await me.subscribe(publication.id)).stream;
} catch(err) {
    if (err instanceof SkyWayError && err.name === "localPersonNotJoinedChannel") {
        return;
    }
    throw err;
}
```


## IDの代わりにユーザー名を使用する

チュートリアルでは自動で割り当てられる一意のIDをユーザー識別子としていますが、一般的なWeb会議とかでは、表示用の名前を自分で決めて入室したいものだと思います。

そのためのフィールドとして、Room に join する際のパラメータで name を指定することができます。

```ts
await room.join({ name:"[ユーザ名]" });
```

指定した名前のユーザーがすでに同じルームに存在する場合は `alreadySameNameMemberExist` というエラーがThrowされるようです。

他のユーザーの name は RoomPublication の publisher.name で参照できます。


## おわりに

ある程度いじってみた感想ですが、[APIドキュメント](https://javascript-sdk.api-reference.skyway.ntt.com/room/)はあるものの、なんというかいかにもプログラマーが機械的に作ったんだろうなという感じで、正直あまり親切なものではないかなと思いました汗
↓こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2903443/f1911609-5774-53cc-18fc-44503c6d4027.png)

ただ[サンプルコード](https://skyway.ntt.com/ja/docs/sample-code/javascript-sdk/)が一通りあるので、どちらかというとこっちを見るほうが参考になるかもと、個人的には思いました。

あとはまあ、全体にわかりやすいインターフェースになっていると思うので、型をたよりにやみくもに探していってもけっこうどうにかなりますね笑