SkyWayチュートリアル　〜React,TypeScript仕立て〜


#はじめに
アプリやWebサービスにビデオ会議や音声通話などを導入するためのSDKであるSkyWayのチュートリアルをReactとTypeScriptの環境でやってみました。冗長な部分やナンセンスな部分など多々あるかと思いますが、温かく見守ってください。そういう部分や、間違いががあればどしどしダメ出ししてください！

下記サイト(JavaScriptSDK)に沿って進めていくので、別タブで開いて、見比べると見やすいかなと思います。

https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#javascript-sdk

ひとまず、`create-react-app`でプロジェクト作成しておきます。
#SDKのダウンロード
`yarn`でやりました。

```
yarn add skyway-js
```

#チュートリアル
##1,準備
ここはチュートリアルに従ってすすめるだけ。


###ローカルサーバーの準備
「1-2のローカルサーバーの準備」ではLiveServerとか色々書いてますが、今回は`yarn start`をしました。

##2,アプリケーションの作成
###JSX作成
公式ではhtmlを編集していますが、今回はReactなので`App.tsx`にJSXで書いていきます。

```React
import React from "react";

function App() {
  return (
    <div className="App">
      <div>React TypeScript SkyWay</div>
    </div>
  );
}

export default App;
```
###CDNのインポート
公式サイトでは`2-2`でCDNでSDKをインポートしていますが、今回は使わないのでスルーします。
###カメラ映像、マイク音声の取得

まずはカメラ映像を表示する領域としてvideo要素を追加します。

```React
import React from "react";

function App() {
  return (
    <div className="App">
      <div>React TypeScript SkyWay</div>
      <video ref={ref} width="400px" autoPlay muted playsInline></video>
    </div>
  );
}

export default App;
```

公式サイトで示されているvideoタグをコピーして貼り付けるだけなのですが、そのままだと`autoplay`属性と`playsinline`属性に赤線でエラーが表示されます。それぞれ`autoPlay`、`playsInline`のようにキャメルケースで記載するとエラーが解消できます。

https://ja.reactjs.org/docs/introducing-jsx.html#specifying-attributes-with-jsx

<br>
次に、Webブラウザでカメラ映像、マイク音声を取得するための処理なのですが、公式サイトでは素のJavaScriptで書かれているため、`getElementById`でvideo要素を取得していますが、Reactでこれをやるのはご法度?なので、Refを使用してvideo要素にアクセスできるようにします。

```React
import React, { useState, useRef } from "react";
let localStream: MediaStream;

function App() {
  const ref = useRef<HTMLVideoElement>(null);
  let [localStream, setLocalStream] = useState<MediaStream>();

  const getMedia = () => {
    navigator.mediaDevices
      .getUserMedia({ video: true, audio: true })
      .then((stream) => {
        const videoElm = ref.current;
        if (videoElm) {
          videoElm.srcObject = stream;
          videoElm.play();
        }

        localStream = stream;
      })
      .catch((error) => {
        console.error("mediaDevice.getUserMedia() error:", error);
        return;
      });
  };
  getMedia();

  return (
    <div className="App">
      <div>React TypeScript SkyWay</div>
      <video ref={ref} width="400px" autoPlay muted playsInline></video>
    </div>
  );
}

```
`tsconfig.json`で`"noImplicitAny": true`に設定している場合、`localStream`を発信処理の引数として使用するときに`any`ではないかと指摘されるので、`MediaStream`型を指定しています。
<br>
ここまで実装し、localhostにアクセスするとPCのカメラで撮影された画像が表示されていると思います。

###Peerの作成
上部の`react`のimport文の下で`Peer`を`skyway-js`からimport。

```React
import React, { useState, useRef, useEffect } from "react";
import Peer from "skyway-js";
const peer = new Peer({
  key: "※※※※※※※※※※※※※※※※※※※※※※※※※※※",   //チュートリアルの1,準備で取得したkey
  debug: 3,
});
```

###PeerID取得
skywayにおいて電話番号にあたるPeerIDを取得し、useStateを使いstateとして保存する。


```React

import React, { useState, useRef, useEffect } from "react";
import Peer from "skyway-js";
const peer = new Peer({
  key: "※※※※※※※※※※※※※※※※※※※※※※※※※※※",
  debug: 3,
});
let localStream;

function App() {
  const ref = useRef<HTMLVideoElement>(null);
  const [peerId, setPeerId] = useState<string>("");

  useEffect(() => {
    const getMedia = () => {
     navigator.mediaDevices
        .getUserMedia({ video: true, audio: true })
        .then((stream) => {
          const videoElm = ref.current;
          if (videoElm) {
            videoElm.srcObject = stream;
            videoElm.play();
          }
          localStream = stream;
        })
        .catch((error) => {
          console.error("mediaDevice.getUserMedia() error:", error);
          return;
        });
    };
    getMedia();

    peer.on("open", () => {
      setPeerId(peer.id);
    });
  }, []);

  return (
    <div className="App">
      <div>React TypeScript SkyWay</div>
      <video ref={ref} width="400px" autoPlay muted playsInline></video>
      <p>{peerId}</p>
    </div>
  );
}

export default App;

```
`useEffect`を使用していないと、peeridをstateとして保存しているため、getMedia関数の実行中に、stateが更新されると、Appコンポーネントが再レンダリングされ、`The play() request was interrupted by a new load request.`というエラーが発生しました。そのため、`useEffect`の中に処理を書き、第２引数として空配列を与え、初回レンダリング時にしか実行されないようにしています。

この時点で`yarn start`すると映像の下に、取得したidが表示されているはず。

###発信処理
すでに作成しているJSXに下記を追加。

```React
      <textarea value={theirId} onChange={handleChange}></textarea>
      <button onClick={handleCall}>発信</button>
      <video ref={theirRef} width="400px" autoPlay muted playsInline></video>
```
video要素は相手の映像を配置する場所で、後からDOM操作するためref属性に新たな`ref`を指定しておく。

textarea要素は発信相手のpeerIdを入力するための要素で、入力された値は`useState`を使い`state`として管理する。

button要素をクリックすると、発信処理を行うcallメソッドが呼ばれるように`handleCall`関数を作成。callメソッドは、第１引数にpeerId(ここでは`theirId`という変数)、第２引数に最初に用意したカメラ映像(localstream)を指定する。

```React
  const handleCall = () => {
    const mediaConnection = peer.call(theirId, localStream);
    setEventListener(mediaConnection);
  };
```

callメソッドは、相手に接続したときに、相手の映像が含まれる`MediaConnection`オブジェクトを取得する。上記のコードではそれを`mediaConnection`変数に代入し、その変数を引数として`setEventListener`関数に渡している。

`setEventListener`関数では、相手の映像を取得したときに発生する、`mediaConnection`の`on`メソッドで`stream`イベントを指定する。

```React
  const setEventListener = (mediaConnection: MediaConnection) => {
    mediaConnection.on("stream", (stream: MediaStream) => {
      const videoElm = theirRef.current;
      if (videoElm) {
        videoElm.srcObject = stream;
        videoElm.play();
      }
    });
  };
```
コールバック関数内では`setEventListener`関数の引数`mediaConnection`の型は,`: MediaConnection`を指定。何もしないとエラーが発生するので、下記のようにコード上部でimportしておく。

```React
import Peer, { MediaConnection } from "skyway-js";
```

###着信処理
Peerオブジェクトの`on`メソッドで、`call`イベントを指定し、相手からの着信に備える。ここに関しては、チュートリアルのコードのままで良い。

#最終形
```React
import React, { useState, useRef, useEffect } from "react";
import Peer, { MediaConnection } from "skyway-js";
const peer = new Peer({
  key: "※※※※※※※※※※※※※※※※※※※※※※※※※※※",
  debug: 3,
});
let localStream: MediaStream;

function App() {
  const myRef = useRef<HTMLVideoElement>(null);
  const theirRef = useRef<HTMLVideoElement>(null);
  const [peerId, setPeerId] = useState<string>("");
  const [theirId, setTheirId] = useState<string>("");

  useEffect(() => {
    const getMedia = () => {
      navigator.mediaDevices
        .getUserMedia({ video: true, audio: true })
        .then((stream) => {
          const videoElm = myRef.current;
          if (videoElm) {
            videoElm.srcObject = stream;
            videoElm.play();
          }
          localStream = stream;
        })
        .catch((error) => {
          console.error("mediaDevice.getUserMedia() error:", error);
          return;
        });
    };
    getMedia();

    peer.on("open", () => {
      console.log("open");
      setPeerId(peer.id);
    });
  }, []);

  const handleCall = () => {
    const mediaConnection = peer.call(theirId, localStream);
    setEventListener(mediaConnection);
  };

  const setEventListener = (mediaConnection: MediaConnection) => {
    mediaConnection.on("stream", (stream: MediaStream) => {
      const videoElm = theirRef.current;
      if (videoElm) {
        videoElm.srcObject = stream;
        videoElm.play();
      }
    });
  };

  peer.on("call", (mediaConnection) => {
    mediaConnection.answer(localStream);
    setEventListener(mediaConnection);
  });

  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    setTheirId(e.target.value);
  };

  return (
    <div className="App">
      <div>React TypeScript SkyWay</div>
      <video ref={myRef} width="400px" autoPlay muted playsInline></video>
      <p>{peerId}</p>
      <textarea value={theirId} onChange={handleChange}></textarea>
      <button onClick={handleCall}>発信</button>
      <video ref={theirRef} width="400px" autoPlay muted playsInline></video>
    </div>
  );
}

export default App;

```

#終わりに
長くなりましたが、最期まで読んでいただけのであればありがとうございました！！
先輩方にアドバイス、ダメ出しなど頂けると非常にうれしく思います！
お願いいたします！