Web会議、そのままでいいですか？


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/c474db9a-bd2f-90ce-f4a4-405e9d6f2173.png)


<b>本記事を読むと嬉しくなる人</b>
・Web会議アプリケーション(ZoomやSkypeなど)を利用して、業務を行っている人
・今のWeb会議に不満がある人
・オリジナルWeb会議アプリケーションを実装してみたい人


#背景

本記事を見つけてくれたあなたに私はまず問いかけます。

<b>Web会議、そのままでいいですか？</b>　と。

昨今のコロナ禍により、企業ではリモートワークが推進され頻繁にWeb会議が行われるようになりました。
私は、入社1年目の新人ですが、これまで参加した部署の会議もほとんどWeb会議で開催されました。
そのWeb会議では、特定の数人だけが話すことが多かったり、
話しづらい雰囲気があると感じることもありました。
また、研修もWeb会議形式で行われましたが、
対面形式よりもディスカッションを行うのが難しいと感じることがしばしばありました。
そこで、現状のWeb会議の方式では問題があるのではないかという疑問が湧きました。
したがって、今回はWeb会議の現状を調査し、問題解決に至るまでを記事にしました。

#今回の取り組み
* 所属部署にスポットライトを当て、 部内でのアンケートをもとにWeb会議における現状の問題点を洗い出し
* 問題を解決する機能を備えたWeb会議アプリケーションのプロトタイプを実装
*  実装したアプリケーションを使用してWeb会議実験





#Web会議実態調査アンケートの実施
部内でのWeb会議における問題点を明らかにするためにアンケート調査を実施しました。
アンケート回答者は部内のメンバー27人です。
アンケートは下図のように、選択方式や自由記述を含んだものとしました。

![アンケート図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323865/f2d3b99e-72cc-f28f-81ef-a3d4e32aff37.png "アンケート抜粋")

アンケートを実施する前に、Web会議における問題点について以下のように仮説を2つたてました。

1. <b>発言しづらいと感じたことがある</b>
2. <b>自分が話しているときに、聞き手の反応がわからず困ったことがある</b>

上記の仮設は、自身や上司のWeb会議における経験談をもとにたてました。
これらの仮設をもとにアンケートを作成し、調査を行いました。


##Web会議における発言のしづらさに関する回答結果

仮説：Web会議で発言しづらいと考えている人は全体の9割以上である

ここでは、この仮説に対するアンケート結果について述べます。

設問：あなたがWeb会議に参加したとき、発言しづらいと感じたことがありますか？

結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/dbcea23c-de8c-4a2e-f1c7-06a2035557db.png)



自身でたてた仮説よりも少なくて驚きました。
同時に、「発言しづらいと感じたことがない」という人も一定数いるということを認識できました。
とはいえ、部の7割以上の人がWeb会議で発言しづらいと考えているというのは
無視できない問題であるといえそうです。

この問題の解決を図るために、発言しづらい理由についても調査しました。

仮説：Web会議で発言しづらい理由は、
　　　顔が見えず反応が確認できないからと考えている人が全体の8割以上である

設問：Web会議で発言しづらいと感じたことが今まであると回答した方に伺います。
　　　発言しづらいのは、聞き手の顔が見えないことがあり、反応が確認できないからだと思いますか？

結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323865/ccaa7fe1-b20d-e520-3e8a-529ea024ac19.png)

全体の約4割、発言しづらいと考えている人の内約5割の人が、
発言しづらい理由は顔が見えないことであると考えているという結果になりました。
自分自身、顔が見えないことで聞き手の反応が確認できないため発言しづらい場面を、
何度も経験していたことから、仮説より少ないこの結果には驚きました。
また、全体の約4割の人が、「どちらでも」「そう思わない」と回答したため、
<b>発言しづらい理由として他にも要因がある</b>ことが考えられます。
そこで、他の発言しづらい理由の回答結果を確認すると、興味深い結果を得られました。

発言しづらい他の理由

* 遅延による<b>発言被り</b>を気にしてしまう
* <b>発言する適切なタイミング</b>がつかめない

これらのように、発言のタイミングに関する回答が自由記述にもかかわらず14件も得られました(n=20)。
したがって、発言しづらい問題を解決するために
上記の2つの要因を排除しなければならないことが明らかになりました。


##Web会議における聞き手の反応に関する回答結果

仮説：Web会議では聞き手の反応が確認できないことで、困ったことがある人が全体の5割以上である

設問：Web会議中に、聞き手の反応が確認できないことで、困ったことが今までありますか？

結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/4c23c0b3-ee80-8798-20fe-1aa0d58c132d.png)



全体の8割以上の人が「ある」と回答しました。
仮説と大きな差があったため、この結果にはとても驚きました。
したがって、<b>発言者が聞き手の反応を確認できるWeb会議が求められている</b>ということが明らかになりました。

#Web会議アプリケーションに求められる機能

アンケートを踏まえて、Web会議アプリケーションに求められる要件について整理してみると、

* 遅延が全く発生しない
* 次に誰が発言するのか全員が認知できる
* 発言者は聞き手の反応が確認できる

が挙げられます。
遅延に関しては、個人のネットワーク環境の問題もあるため解決は難しいと考えられます。
残りの2つの要件は、アプリケーションの機能により解決できる可能性があります。

次に誰が発言するか会議参加者全員が認知するためには、
<b>発言の意志を主張する挙手ボタン</b>のような機能が必要と考えられます。
また、発言者が聞き手の反応を確認するためには、
<b>聞き手の顔やリアクションの表示</b>から反応を伺う機能が挙げられます。
ただし、顔出しについては個人のプライバシーにも関わる問題なので、強制させるのは難しそうです。

##既存のWeb会議ツール
上記で挙げた機能が既存のWeb会議ツールに備わっているかを確認するために、
主な既存ツールの機能について整理してみます。


|  | Zoom | Skype | Google Meet | Remo |
|:-:|:-:|:-:|:-:|:-:|
|  挙手ボタン |  ◯ |  ✖ | ✖  |  ◯ |
|  聞き手のリアクション表示 |  ◯ |  ✖ | ◯  | ✖  |

一見すると、Zoomは要件に相当する機能が実装されているようにみえます。
しかし、Zoomの挙手ボタンは押したとしても、参加者一覧にしか表示されず
発言者の目に届かない事がほとんどかと思われます。
また、この機能は発言したい人が挙手するというよりも、
ファシリテーターの意図に応じて使用することが多いように感じられます。

反応を表すリアクションについては、種類が2種類と少なく聞き手の反応を適切に表現できません。
<b>リアクションが表示される時間も数秒と短く、発言者が見逃してしまう可能性が高い</b>と考えられます。
同様に、拡張機能を導入することで使用できるGoogle Meetのリアクション機能も数秒しか表示されません。

したがって、今回は要件を満たすよう以下の2つの機能を提案します。
<br>
##提案機能

* <b>挙手ボタンと次発言者リスト</b>
    * 発言ボタンを押すと、次発言者リストに追加され発言意思を会議参加者に周知させる
    * 同時に発言したい人が多い場合、順番待ちになる

　次発言者リスト

| 順番 | 次の発言者 |
|:-:|:-:|
|   1.|山田太郎   |
|   2.| nomura  |
|   3.|  n-hanako |

<br>

* <b>聞き手のリアクションを表示</b>
    * 常にアイコンで議論へのリアクションを表示
    * 聞き手それぞれが自身の反応に近いアイコンを選択
    * 聞き手の話への理解度が一目瞭然

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323865/6c4379ba-125c-de99-4ce1-1f354f4c5709.png)

これらの機能を含んだWeb会議アプリケーションを実装していきます。

#Web会議アプリケーション実装
##SkyWay
早速Web会議アプリケーションを実装していくわけですが、
一からすべて実装するのはとても大変です。
そこで今回は、WebRTCを簡単に実装できる<b>SkyWay</b>というSDKを使用します。
WebRTC(Web Real-Time-Communication)とは、
Webブラウザを介して高速なデータ通信を実現する技術のことです。
特に映像や音声などのデータをリアルタイムに送受信できることを指します。

SkyWay https://webrtc.ecl.ntt.com/?origin=skyway

SkyWayでは、サーバを用意することなくすぐにアプリケーション開発を行うことができます。
また、50万回までの接続ならば無料ということなので、お試しで使う場合にも嬉しいですね。

通常、WebRTCには以下の4つのサーバが必要になってきます。

| サーバ名称 | 役割 |
|:-:|:--|
| シグナリングサーバ  | 通信相手に関する情報を交換する  |
| STUNサーバ  |  NATによって変換されていた、グローバルIPアドレスやポート番号を取得する |
|  TURNサーバ |  企業内ネットワークなどP2P通信が利用できない特定のネットワーク環境での通信を実現する |
| SFUサーバ  |  3人以上で通信する際に端末のエンコード負荷や通信量を削減する |


SkyWayでは、上記のサーバをAPI経由で利用することができます。
したがって、簡単にWebRTCを試したいという人には持ってこいなSDKですね。

また、SkyWayのデモアプリとして、SkyWay ConferenceというOSSが公開されています。
SkyWay Conferenceでは、フロントエンド開発のみで、Web会議アプリケーションを実装することができます。
フロントの開発には、TypeScriptが使用されています。
今回は、このSkyWay Conferenceを使用してWeb会議アプリケーションを実装していきます。

##SkyWay Conferenceを実行
では、実際にskyway Conferenceを実行するまでの手順を紹介します。

###SkyWay Community Editionへの登録

https://console-webrtc-free.ecl.ntt.com/users/registration

上記のURLにアクセスしてアカウント登録を行います。
メールアドレスとパスワードを設定するだけです。
アカウントを作成したあとは、アプリケーションを作成します。
![アプリ登録.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323865/1ff1bdf0-ce8c-1bf4-81ca-2645fa71a123.png)

図の赤く囲われている追加ボタンからアプリケーションを追加します。
追加ボタンをクリックするとアプリケーションの設定画面が表示されます。
各項目は以下の表を参考にして設定します。

<!--
ここそのままコピーしてるから変えるべき？
-->

| 入力項目 | 設定 | 説明 |
|:-:|:-:|:--|
|  アプリケーション説明文 |  SkyWay Confの <br> テスト用 |  アプリケーションにつける説明文で、ダッシュボードでの表示のみに利用されます。128文字以内で指定してください。 |
| 利用可能 <br> ドメイン名  |  localhost | 作成するアプリケーションで利用するドメイン名を入力します。ここでは localhost と入力します。この場合、 http://localhost にWebアプリをホスティングして利用することが出来ます。  |
| TURNを利用する  | 有効  |  TURNサーバは、ファイアウォールを経由する等の理由によりP2P通信ができない場合でも、メディアやデータをリレーすることにより通信を可能とします。 有効にした場合、SDK並びにブラウザが利用するかどうかを自動判別します。 |
| SFUを利用する  | 有効  |  SFUとは、P2PではなくSFUというメディアサーバを経由して映像や音声の送受信を行う技術です。 SkyWay Confでsfuルームを利用する場合は有効にします。 |
|  listAllPeers APIを <br> 利用する | 無効  |  listALLPeers APIを使用する場合は有効にします。 SkyWay Confでは利用しないため無効にします。 |
| APIキー認証を <br> 利用する  |  無効 | APIキーの不正利用を防止するための認証機能を利用する場合は有効にします。SkyWay Confは対応していないため無効にしますが、SkyWay Confをベースに商用サービスを開発される場合などは機能追加することをオススメします。  |
※参照 
@yusuke84 さんの OSSを使って独自のWeb会議アプリ(SkyWay Conf)を立てる方法
https://qiita.com/yusuke84/items/e86501810acaa146195d

アプリケーション作成後に表示されているAPIキーを控えます。
このAPIキーは、のちほどソースコード編集時に使用します。

###ソースコードのダウンロードとAPIキーの追記

https://github.com/skyway/skyway-conf/

↑ソースコードはこちらからダウンロードします。

ダウンロードしたソースコードのskyway.tsファイルにAPIキーを追記します。

skyway-conf-master\src\conf\utils\skyway.ts

↑のフォルダにあります。
APIキーはソースコードのkey部分に追記します。

```TypeScript:
import Peer, { SfuRoom } from "skyway-js";

export const initPeer = (forceTurn: boolean): Promise<Peer> => {
  return new Promise((resolve, reject) => {
    const peer = new Peer({
      key: "",
      debug: 2,
      config: {
        iceTransportPolicy: forceTurn ? "relay" : "all",
      },
    });

//略
//
//
```



###開発用サーバで実行

事前に準備しておく環境

* Node.js/npm
* git
* TLS/SSLにより保護されたhttpsを使用できるサーバ(他マシンから接続する場合)

上記の事前準備を済ませ、
以下のコマンドを実行し、パッケージをインストールします。

```:
$ npm install
```
この時、自身の環境にgitが入ってないとエラーが出てしまうので、
必ず事前に用意しておきましょう。

次に、skyway-conf-masterフォルダ内(package.jsonファイルが存在するフォルダ内)で、
以下のコマンドを実行し開発用サーバを起動します。

```:
$ npm run dev
```

サーバーが起動すると、以下のような表示になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/b20922c6-169c-b516-fdf9-28af78f6823c.png)

以下のURLにアクセスし、SkyWay Conferenceのルーム作成画面にアクセスできれば実行は成功です。
http://localhost:9000/webpack-dev-server/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/93c57677-6d98-3301-26f2-3c6daae1f593.png)


好きなROOM IDを入力し、ルームを作成します。
ROOM TYPEは基本的にsfuで問題ないです。
あとは、自由にビデオ通話を行うことができるので、このルームをデバッグ用に使用します。
複数ユーザがルームに参加したときの挙動をデバッグしたい場合は、
同一マシンの別タブで同じROOM IDにアクセスすれば挙動が確認できます。
ただし、この時点で別マシンからは、同一ルームにアクセスすることができないのでご注意ください。
別マシンからも同じルームにアクセスし挙動を確認したい場合は、
次節で説明する通り自身で別途サーバを先に用意し、
そこにアプリケーションを配置する必要があります(つまり本番環境と同じ)。

###Webサーバへの配置


自分用にソースコードを編集したあとに保存を行い、まずはビルドをします。
skyway-conf-masterフォルダ内(package.jsonファイルが存在するフォルダ内)で、
以下のコマンドを実行してください。

```:
$ npm run build
```

ビルドを行うと同一フォルダ内に docs フォルダが生成されます。

次に、配置用サーバを用意します。
自身でサーバの用意がある人はそちらをお使いください。
ただし、使用するサーバはTLS/SSLにより保護されたhttpsを使用できるものをお使いください。
筆者は当初ローカルサーバである Web Server for Chrome を使って配置できなくて苦しみました。
用意がない人には、さくらサーバー( https://www.sakura.ne.jp/ )をオススメします。

SkyWayのアプリケーション一覧から設定変更をクリックし、
利用可能ドメインにサーバのドメインを登録します。

例：sample.sakura.ne.jp

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/0a623465-0010-d051-a5ae-121d08607bee.png)



先ほどのビルドで生成した docs フォルダを、用意したサーバにアップロードすれば配置完了です。
https://自身のドメイン/docs/ にアクセスして、ROOM作成画面が表示されることを確認してください。


#話し手のリアクションを表示する機能の実装

では、実際にソースコードを編集して追加した機能について説明します。
今回は、<b>常に聞き手が選択したリアクションを表示し続ける機能</b>を実装しました。
下図のように、通話画面右部にある特定のアイコンを押すと、
それが通知形式で左上にユーザ名とともに表示されます。
この通知は<b>常に表示される</b>ため、発言者の話に対する聞き手の理解度や反応を表します。
そのため、発言者は話している最中でも、聞き手の反応を常に目視で確認することができます。
この「<b>常に表示</b>」という部分は、既存のWeb会議ツールのリアクション機能とは大きく異なりますね。

もともとソースコードに存在したリアクション機能を編集し、今回の機能を実装しました。
当初は、顔画面近辺にリアクション表示を行いたかったのですが、
ライブラリの都合上実装不可と判断しました。
どの位置にリアクション表示を行うのかは、
運用を行ってみてどこがベストなのか探っていくのが良さそうです。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/76f9a73d-fa53-1ea3-aa9f-af7684f751af.png)

##ソースコード解説

大きく編集したのは、この skyway-conf-master\src\conf\stores\notification.ts のみです。
bootstrap.tsファイルでどのアクションを実行するか判断してメソッドを呼び出します。
基本的に通知系の表示メソッドはnotification.tsファイルに集約されているので、
そこにあったメソッドを自分用に編集して完了です。
簡単なプログラムなので、コメントを読んでいただければわかると思いますが、
リアクション表示を時間によって管理しています。
今回実装したリアクション機能は、従来のツールのリアクション機能と異なり
常にユーザとリアクションが通知形式で表示されます。
そのため、<b>発言者の目に入りやすく、聞き手の反応を容易に確認することができる</b>と考えられます。

以下にソースコードを記載しますが、精査出来ていないためサンプルを動作させる用途程度でご利用ください。

<details><summary>notification.ts</summary><div>

```TypeScript:notification.ts

import { decorate, observable, action } from "mobx";
import { IObservableArray } from "mobx";
import { NotificationItem, NotificationType } from "../utils/types";

let count = 0;//自身の1回目のリアクションかどうか判断する
let count_other1 = 0;//ユーザ1の1回目のリアクションかどうか判断する。ユーザの人数分変数を用意。
let count_other2 = 0;
let count_other3 = 0;
let count_other4 = 0;
let count_other5 = 0;

let u_sum = 0;//現在リアクションを行ったユーザ数
let reaction_num = 1000;//各ユーザの最大リアクション数。ユーザの人数分変数を用意。
let reaction_num1 = 2000;
let reaction_num2 = 3000;
let reaction_num3 = 4000;
let reaction_num4 = 5000;
let reaction_num5 = 6000;

let item1: NotificationItem = { id: Math.random(), type: "info", text: "aaa" };//リアクションデータを一時保存するオブジェクト。ユーザ一人につき3つ必要。
let item2: NotificationItem = { id: Math.random(), type: "info", text: "aaa" };
let item3: NotificationItem = { id: Math.random(), type: "info", text: "aaa" };
let item4: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item5: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item6: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item7: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item8: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item9: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item10: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item11: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item12: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item13: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item14: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item15: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item16: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item17: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };
let item18: NotificationItem = { id: Math.random(), type: "insert_emoticon", text: "aaa" };

let user = Array(5);//ユーザ名を保存する

class NotificationStore {
    items: IObservableArray<NotificationItem>;

    constructor() {
        // @ts-ignore: to type IObservableArray
        this.items = [];
    }

    showInfo(text: string) {
        this.show("info", text, 3000);
    }

    showChat(from: string, text: string) {
        this.show("chat", `${from}: ${text}`, 6000);
    }

    newShowReaction(text: string, from: string) {
        this.show2("info", text, from);//自身のリアクションを通知するメソッドを呼び出す
    }


    showReaction(from: string, reaction: string) {
        this.show3("insert_emoticon", `${from}: ${reaction}`, from);//他人のリアクションを通知するメソッドを呼び出す
    }

    showJoin(name: string) {
        this.show("person", `${name} joined`, 2000);
    }

    showLeave(name: string) {
        this.show("person", `${name} left`, 2000);
    }



    private show(type: NotificationType, text: string, duration: number) {
        const item: NotificationItem = { id: Math.random(), type, text };
        this.items.push(item);
        setTimeout(() => this.items.remove(item), duration);
    }


    private show2(type: NotificationType, text: string, from: String) {//自身のリアクションを表示・非表示にするメソッド
        if (count != 0) {
            item3 = item2;//item3にitem2を代入。これをしないと次行の処理でリアクションが表示されなくなってします
            setTimeout(() => this.items.remove(item3), 0);//リアクションを非表示にする
            reaction_num++;//現在のリアクション数をカウント
        }
        item1 = { id: reaction_num, type, text };//リアクションを代入
        item2 = item1;//リアクションを保存
        this.items.push(item1);//itemsにプッシュ、つまり保存
        setTimeout(() => this.items.remove(item1), 9999999);//一定時間リアクションを表示する(便宜上大きな数字を指定している)
        count = 1;//条件式を繰り返さないよう制
    }

    private show3(type: NotificationType, text: string, from: String) {//他人のリアクションを表示・非表示にするメソッド
        for (let i = -1; i < u_sum; i++) {//リアクションを既に行った人数分繰り返す
            switch (from) {//どのユーザのリアクションなのか判別
                case user[1]:
                    user[1] = from;
                    break;

                case user[2]:
                    user[2] = from;
                    break;

                case user[3]:
                    user[3] = from;
                    break;

                case user[4]:
                    user[4] = from;
                    break;

                case user[5]:
                    user[5] = from;
                    break;

                default:
                    user[u_sum + 1] = from;
                    u_sum++;
            }
        }

        switch (from) {//ユーザごとにリアクションの表示・非表示を行う
            case user[1]:
                if (count_other1 != 0) {
                    item6 = item5;
                    setTimeout(() => this.items.remove(item6), 0);
                    reaction_num1++;
                }
                item4 = { id: reaction_num1, type, text };
                item5 = item4;
                this.items.push(item4);
                setTimeout(() => this.items.remove(item4), 9999999);
                count_other1 = 1;
                break;

            case user[2]:
                if (count_other2 != 0) {
                    console.log(9);
                    item9 = item8;
                    setTimeout(() => this.items.remove(item9), 0);
                    reaction_num2++;
                }
                item7 = { id: reaction_num2, type, text };
                item8 = item7;
                this.items.push(item7);
                setTimeout(() => this.items.remove(item7), 9999999);
                count_other2 = 1;
                break;

            case user[3]:
                if (count_other3 != 0) {
                    item12 = item11;
                    setTimeout(() => this.items.remove(item12), 0);
                    reaction_num3++;
                }
                item10 = { id: reaction_num3, type, text };
                item11 = item10;
                this.items.push(item10);
                setTimeout(() => this.items.remove(item10), 9999999);
                count_other3 = 1;
                break;

            case user[4]:
                if (count_other4 != 0) {
                    item15 = item14;
                    setTimeout(() => this.items.remove(item15), 0);
                    reaction_num4++;
                }
                item13 = { id: reaction_num4, type, text };
                item14 = item13;
                this.items.push(item13);
                setTimeout(() => this.items.remove(item13), 9999999);
                count_other4 = 1;
                break;

            case user[5]:
                if (count_other5 != 0) {
                    item18 = item17;
                    setTimeout(() => this.items.remove(item18), 0);
                    reaction_num5++;
                }
                item16 = { id: reaction_num5, type, text };
                item17 = item16;
                this.items.push(item16);
                setTimeout(() => this.items.remove(item16), 9999999);
                count_other5 = 1;
                break;

            default:
        }
    }
}



// @ts-ignore: to use private accessor
decorate(NotificationStore, {
    items: observable.shallow,
    showInfo: action,
    showChat: action,
    showReaction: action,
    showJoin: action,
    showLeave: action,
    show: action,
});

export default NotificationStore;

```

</div></details>

<details><summary>reaction-layout.tsx</summary><div>

```TypeScript:reaction-layout.tsx
import * as React from "react";
import { FunctionComponent } from "react";
import { css } from "@emotion/core";
import { globalColors } from "../../shared/global-style";
import { rightMenuTogglerHeight, zIndex } from "../utils/style";

const reactions = ["🆗", "✋", "👍", "❓", "🙆‍♂️", "🤷‍♂️"];//リアクション編集

//略
//
//
```

</div></details>


<details><summary>bootstrap.ts</summary><div>

```TypeScript:bootstrap.ts
const disposers = [
    reaction(
      () => room.isJoined,
      (isJoined) =>
        isJoined && notification.showInfo(`You joined the room ${room.name}`)
    ),
    reaction(
      () => media.isAudioTrackMuted,
      (muted) =>
        notification.showInfo(`Mic input was ${muted ? "muted" : "unmuted"}`)
    ),
    reaction(
      () => media.isVideoTrackMuted,
      (muted) =>
        notification.showInfo(`Video was ${muted ? "muted" : "unmuted"}`)
    ),
    observe(media, "audioDeviceId", (change) => {
      if (change.oldValue === null) {
        // skip initial value
        return;
      }
      notification.showInfo("Mic input was changed");
    }),
    observe(media, "videoDeviceId", (change) => {
      if (change.oldValue === null) {
        notification.showInfo("Video input was enabled");
        return;
      }
      if (change.newValue !== null) {
        notification.showInfo("Video input was changed");
      } else {
        notification.showInfo("Video input was disabled");
      }
    }),
    reaction(
      () => room.castRequestCount,
      () => notification.showInfo("Your video was casted to everyone")
    ),
    reaction(
      () => room.myLastReaction,
      (reaction) =>
        reaction &&
        notification.newShowReaction(`You reacted with ${reaction.reaction}`,reaction.from)
        //fromを引数として使うために変更
    ),
    reaction(
      () => client.displayName,
      (name) => {
        localStorage.setItem("SkyWayConf.dispName", name.trim());
        notification.showInfo("Display name saved");
      },
      { delay: 2000 }
    ),
  ];
```

</div></details>


#実際にWeb会議をやってみた

聞き手のリアクションを常に表示する機能を実装したWeb会議アプリケーションで
実際にWeb会議をしてみました。
下図はその時の様子です。リアクションが見えやすいように拡大しております。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/b873bd1e-a993-dbec-521a-b9ef3c0f4b6c.png)

以下に実施概要を記します。

参加人数：部のメンバー5人
実施回数：2回
実施時間：15分×2回

2回実施したうち、1回目は一つのテーマに対して自由にディスカッションをしてもらう形式、
2回目は、5人のうち1人があるテーマについて他の4人に説明する形式をとりました。
ディスカッション形式では、あまりリアクション機能を使用せず
1対多形式では、リアクション機能を頻繁に使うだろうという仮説のもと2回Web会議を行いました。
予め、参加者には機能の使い方を説明し、Web会議中に自由に使うよう指示しました。
会議後には、簡単なアンケートを実施しました。

結果として、リアクション機能の使用に関して興味深い現象が観測できました。

まず、仮説で立てた通り、ディスカッション形式より1対多形式のWeb会議の方が、
参加者はリアクションを頻繁に使用していました。
具体的には、聞き手の4人中3人が11回以上使用したとアンケートで回答しました。
自分が会議の様子を観察しているときも、
発言者の話に対して頻繁にリアクション機能を使用している様子が確認できました。
逆に、ディスカッション形式では、5人中3人が使用回数5回以下にとどまる結果となりました。
これは参加者の注意が、リアクション機能よりも発言することに向いてしまったことが原因と考えられます。

一方、2回目のWeb会議で全員に向けて話をしていた参加者によると、
「難しい話をして聞き手が理解できているかわからないときに、
話の内容に反応するリアクションが表示されたため、<b>話を理解してるんだなと判断することができて安心した</b>」とのことでした。

他の参加者の意見を確認すると、

* リアクション機能により、相手が求めている内容を自分が話しているのか確認できた
* 自分の状態を伝えられる安心感もあった

のようにリアクション機能に関してポジティブな意見が得られました。
よって、<b>1対多のWeb会議では、聞き手のリアクションを常に表示する機能はかなり有用</b>そうです。
逆に、ディスカッション形式でのリアクション表示については、まだ議論の余地があるように感じました。

<br>
今回は、既存のWeb会議ツールにも存在するリアクション機能を

* <b>常に</b>発言者に見えるよう表示する
* アイコンの種類を増やす

ということを重視したうえで実装しました。
このように、既存の機能を少し工夫するだけでもユーザの課題を解決できることが示唆できましたね。



#まとめ

Web会議に関する問題を提起し、実際に実装まで行ってみました。
今回は、新人研修の一環で記事執筆を行った都合上、
時間に限りがあったため提案機能すべては実装できておりません。
とはいえ、既存のWeb会議アプリケーションにはない聞き手のリアクションを常に表示する機能は、
発言者にとって有用でありそうと示唆できました。
また、<b>Web会議に関して問題を抱えている人は思った以上にいる</b>、ということが今回の調査で判明したので、
どうにかしてこの問題は解決していきたいですね…。

以下に、本稿のまとめについて図示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696879/eab9e097-dd5b-d194-9d12-d1a3a0f64349.png)







講義形式のWeb会議では、提案機能により問題解決が図れそうでしたが、
ディスカッション形式のWeb会議にはまだまだ課題があると考えられます。
たとえば、そもそも機能が使われなかったり、
誰に対するリアクションなのかわかりづらいといったことが挙げられます。
これらの課題に対し、UI設計を工夫したりXR技術を使用するなどが展望案として考えられます。
次に実装する機会があれば、是非この辺りに注意して取り組みたいですね。





また、Web会議アプリケーションの実装と言うと難しく聞こえる人もいるかと思いますが、
Webアプリ初学者の私でも実装できたので、
興味があれば是非実装してオリジナルアプリを作ってみてください。
今後、オンラインコミュニケーション技術は、XR技術など先端技術が導入されさらに発展していくでしょう。
これからも常に最新技術をキャッチアップし、先駆者になれるよう頑張りマッス。


#参考資料
* https://webrtc.ecl.ntt.com/
* https://qiita.com/yusuke84/items/e86501810acaa146195d