LoginSignup
6
4

More than 3 years have passed since last update.

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

Posted at

はじめに

アプリや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属性に赤線でエラーが表示されます。それぞれautoPlayplaysInlineのようにキャメルケースで記載するとエラーが解消できます。



次に、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文の下でPeerskyway-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関数では、相手の映像を取得したときに発生する、mediaConnectiononメソッドで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;

終わりに

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

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4