はじめに
アプリやWebサービスにビデオ会議や音声通話などを導入するためのSDKであるSkyWayのチュートリアルをReactとTypeScriptの環境でやってみました。冗長な部分やナンセンスな部分など多々あるかと思いますが、温かく見守ってください。そういう部分や、間違いががあればどしどしダメ出ししてください!
下記サイト(JavaScriptSDK)に沿って進めていくので、別タブで開いて、見比べると見やすいかなと思います。
ひとまず、create-react-app
でプロジェクト作成しておきます。
SDKのダウンロード
yarn
でやりました。
yarn add skyway-js
チュートリアル
1,準備
ここはチュートリアルに従ってすすめるだけ。
ローカルサーバーの準備
「1-2のローカルサーバーの準備」ではLiveServerとか色々書いてますが、今回はyarn start
をしました。
2,アプリケーションの作成
JSX作成
公式ではhtmlを編集していますが、今回はReactなのでApp.tsx
にJSXで書いていきます。
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要素を追加します。
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
のようにキャメルケースで記載するとエラーが解消できます。
次に、Webブラウザでカメラ映像、マイク音声を取得するための処理なのですが、公式サイトでは素のJavaScriptで書かれているため、`getElementById`でvideo要素を取得していますが、Reactでこれをやるのはご法度?なので、Refを使用してvideo要素にアクセスできるようにします。
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
型を指定しています。
ここまで実装し、localhostにアクセスするとPCのカメラで撮影された画像が表示されていると思います。
Peerの作成
上部のreact
のimport文の下でPeer
をskyway-js
からimport。
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として保存する。
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
の中に処理を書き、第2引数として空配列を与え、初回レンダリング時にしか実行されないようにしています。
この時点でyarn start
すると映像の下に、取得したidが表示されているはず。
発信処理
すでに作成しているJSXに下記を追加。
<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メソッドは、第1引数にpeerId(ここではtheirId
という変数)、第2引数に最初に用意したカメラ映像(localstream)を指定する。
const handleCall = () => {
const mediaConnection = peer.call(theirId, localStream);
setEventListener(mediaConnection);
};
callメソッドは、相手に接続したときに、相手の映像が含まれるMediaConnection
オブジェクトを取得する。上記のコードではそれをmediaConnection
変数に代入し、その変数を引数としてsetEventListener
関数に渡している。
setEventListener
関数では、相手の映像を取得したときに発生する、mediaConnection
のon
メソッドでstream
イベントを指定する。
const setEventListener = (mediaConnection: MediaConnection) => {
mediaConnection.on("stream", (stream: MediaStream) => {
const videoElm = theirRef.current;
if (videoElm) {
videoElm.srcObject = stream;
videoElm.play();
}
});
};
コールバック関数内ではsetEventListener
関数の引数mediaConnection
の型は,: MediaConnection
を指定。何もしないとエラーが発生するので、下記のようにコード上部でimportしておく。
import Peer, { MediaConnection } from "skyway-js";
着信処理
Peerオブジェクトのon
メソッドで、call
イベントを指定し、相手からの着信に備える。ここに関しては、チュートリアルのコードのままで良い。
最終形
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;
終わりに
長くなりましたが、最期まで読んでいただけのであればありがとうございました!!
先輩方にアドバイス、ダメ出しなど頂けると非常にうれしく思います!
お願いいたします!