💬

Next.jsと100msで作るVideo Chat App

2022/05/01に公開

はじめに

皆さん、こんにちは。GWいかがお過ごしでしょうか。

不定期で前の会社の同期とオンラインランチをしてるんですが、ZoomやMeetって無料アカウントで3人以上だと時間制限があるじゃないですか。その制限が嫌で、だったら作ったらいいじゃん、ということでビデオチャットアプリを作った話を書きます。
(会社の休憩中にランチしてるので1時間あれば問題ないんですけどね。)

100msとは

今回 100ms を使用するんですが、ご存知でしょうか?

つい先日このブログを読んで初めて知って使ってみようと思ったんですが、

100msは、開発者がWeb、Android、iOSアプリケーションにビデオ・音声会議を追加するためのクラウドプラットフォーム、というものだそうです。

100ms is a cloud platform that allows developers to add video and audio conferencing to Web, Android and iOS applications.

トップページでは「数時間でライブアプリを作成できる」と謳っていますが、私でも一日程度でビデオチャットアプリの作成ができたので、間違ってなかったです。

早速100msを使ってビデオチャットアプリを実装していきましょう!

事前準備

まずは100msでアカウントを作成する必要があります。こちらのページから登録をしてください。

フリープランでは月10,000分の使用が可能なので、今回の目的としては十分です。

登録をしたら「Create a new app」というページに遷移すると思うので、下記のように質問に答えていってください。

  1. 「Virtual Events」を選択し、Next

  1. 好きなドメインを入力してSet up App (Regionも選べます)

  1. 設定終了です!

ここまできたら右下の「Go to Dashboard」でダッシュボードに移動します。

備考

実はもうこれでビデオチャットはできちゃいます。上の画面の「Join as statge」をクリックして先のページに進み、Roomに入ってもらうとビデオチャットが始まります!

画面構築とかカスタマイズとか不要な人はこれを使用すれば良いと思います。

GOAL

これから実装していくコードの完成形はここにあるので、記事読むの面倒な方は直接コードを参照ください。

実装する機能としては下記のとおりです。

・ビデオチャットへの参加・退出

・マイクのオンオフ

・カメラのオンオフ

・画面共有のオンオフ

・スピーカー・マイク・カメラのデバイス変更

前提

  • Next.js * React を使用
  • 100msが提供するSDKがReact 18に対応できてないので17を使用
  • スタイルはTailwind CSSを使用
  • UI ライブラリとしてHeadless UI を使用
  • 100msの使用方法以外の説明は省略
  • 公式ドキュメントのReact Quickstart Guideの内容を翻訳しつつ解説

Install

まずは環境を整えていきます。

npx create-next-app --ts demo_100ms
cd demo_100ms
npm i @100mslive/react-sdk react@17 react-dom@17 

ライブラリの初期化

まずは作成するReactアプリで100msを使用できるように初期化します。

HMSRoomProviderをimportしてラップするだけです。

// src/pages/_app.tsx
import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { HMSRoomProvider } from '@100mslive/react-sdk';

function MyApp({ Component, pageProps }: AppProps) {
  return (
    {/* HMSRoomProviderでラップする */}
    <HMSRoomProvider>
      <Component {...pageProps} />
    </HMSRoomProvider>
  );
}
export default MyApp;

コンセプト

具体的な実装に入る前に必要なキーワードを紹介します。

  • Room: 電話会議に参加するとき、参加者はビデオ通話Roomにいると言います。
  • Peer: ビデオ通話の参加者。自分がローカルPeerで、他の人がリモートPeerです。
  • Track: メディア。ピアが持つことのできるトラックには、オーディオとビデオの2種類があります。

Roomへ参加する

Roomに参加するにはhmsActionsjoinメソッドを使用します。

このメソッドを使用するにはuserNameauthTokenが必要になります。

userNameは参加者が任意で決められる文字列です。

authTokenはダッシュボードからコピーしてきます。

authTokenのコピー

  1. ダッシュボード左側のメニューからRoomを選択し、RoomIdを選択

  1. ページ右上のJoin roomを選択

  1. Stageの鍵アイコンをクリックし、Tokenをコピー

※Roleによってできることできないことが制限されているので、必要に応じて違うRoleのTokenをコピーしてください。今回は参加者全員同じ機能が使用できるようにするために決め打ちとしています。

実装

// src/components/JoinForm/index.tsx
import { ChangeEvent, FormEvent, useState, VFC } from 'react';
import { useHMSActions } from '@100mslive/react-sdk';

export const JoinForm: VFC = () => {
  const hmsActions = useHMSActions();
  const [userName, setUserName] = useState('');

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setUserName(e.target.value);
  };

 const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // joinメソッドにuserNameと先ほどコピーしたauthTokenを渡す
    hmsActions.join({
      userName,
      // tokenはenvファイルから呼び出してます
      authToken:  process.env.NEXT_PUBLIC_STAGE_TOKEN || "",
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <h2>Join Room</h2>
      <div>
        <label htmlFor="user_name">User Name:</label>
        <input
          type="text"
          id="user_name"
          name="name"
          required
          value={userName}
          onChange={handleChange}
        />
      </div>
      <button type="submit">Join</button>
    </form>
 );
};

Roomから退出

Roomに参加できるようになったので、退出できるようにします。

退出にはhmsActionsleaveメソッドを使用します。

ページを離れたタイミングで退出できるようにonunload時に発火するようにします。

// src/components/Home/index.tsx
import { useHMSActions } from '@100mslive/react-sdk';
import { useEffect, VFC } from 'react';

export const Home: VFC = () => {
  const hmsActions = useHMSActions();

  useEffect(() => {
    window.onunload = () => {
      hmsActions.leave();
    };
  }, [hmsActions]);

  return <div>Home</div>;
};

参加者のビデオを表示

次は参加者のビデオを表示していきます。

参加者の情報はuseHMSStoreの引数にselectPeersを渡すことで配列として取得できます。

一人ひとりの映像はuseVideoの引数にvideoTrackを渡して、返却されるrefvideoタグに接続すると表示されます。

// src/components/VideoChat/index.tsx
import { selectPeers, useHMSStore } from '@100mslive/react-sdk';
import type { VFC } from 'react';
import { Peer } from './Peer';

export const VideoChat: VFC = () => {
  const peers = useHMSStore(selectPeers);

  return (
    <div>
      {peers.map((peer) => (
        <Peer key={peer.id} peer={peer} />
      ))}
    </div>
  );
};
// src/components/VideoChat/Peer/index.tsx
import { HMSPeer, useVideo } from '@100mslive/react-sdk';
import type { VFC } from 'react';

type Props = {
  peer: HMSPeer;
};
export const Peer: VFC<Props> = ({ peer }) => {
  const { videoRef } = useVideo({
    trackId: peer.videoTrack,
  });

  return (
    <div>
      <video
        ref={videoRef}
        autoPlay
        muted
        playsInline
        // 映像が反転するのでcssで対処
        style={{ transform: 'scaleX(-1)' }}
      />
      <span>
        {/* ユーザ名はnameプロパティを参照することで取得可能 */}
        {peer.name} {peer.isLocal && '(You)'}
      </span>
    </div>
  );
};

状態に応じてコンポーネントを出し分ける

最低限の実装が完了したので作成したコンポーネントをトップページに接続します。

useHMSStoreselectIsConnectedToRoomを渡すことで取得できるisConnectedをもとにRoomに参加してるかしてないかを判断しコンポーネントを出し分けます。

import { useHMSStore, selectIsConnectedToRoom, useHMSActions } from '@100mslive/react-sdk';
import { useEffect, VFC } from 'react';
import { VideoChat } from '../VideoChat';
import { JoinForm } from '../JoinForm';

export const Home: VFC = () => {
  const hmsActions = useHMSActions();
  const isConnected = useHMSStore(selectIsConnectedToRoom);

  useEffect(() => {
    window.onunload = () => {
      // Roomに参加している時、という条件分岐追加
      if (isConnected) {
        hmsActions.leave();
      }
    };
  }, [hmsActions, isConnected]);
  // Roomに参加していればVideoChat、そうでなければJoinFormを表示する
  return <>{isConnected ? <VideoChat /> : <JoinForm />}</>;
};

ここまでできたら一度Roomへの参加を試してみてください!

JoinFormでユーザ名を入力し参加すると、自分のカメラの映像が表示されると思います。

(初回はブラウザからマイク・カメラのアクセス可否ダイアログが出るので許可してください)

コントローラの追加

ここからは追加機能の実装を行っていきます。

マイクのオンオフ

マイクのオンオフはuseAVToggleの返り値であるtoggleAudioメソッドを使用し、現在の状態はisLocalAudioEnabledで確認できます。

// src/components/VideoChat/Controller/AudioController/index.tsx
import { useAVToggle } from '@100mslive/react-sdk';
import type { VFC } from 'react';

export const AudioController: VFC = () => {
  const { isLocalAudioEnabled, toggleAudio } = useAVToggle();

  return (
    // 省略
  );
};

カメラのオンオフ

カメラのオンオフはuseAVToggleの返り値であるtoggleVideoメソッドを使用し、現在の状態はisLocalVideoEnabledで確認できます。

// src/components/VideoChat/Controller/index.tsx
import { useAVToggle } from '@100mslive/react-sdk';
import type { VFC } from 'react';

export const VideoController: VFC = () => {
  const { isLocalVideoEnabled, toggleVideo } = useAVToggle();

  return (
    // 省略
  );
};

画面共有のオンオフ

画面共有も簡単に実現可能です。hmsActionssetScreenShareEnabledメソッドにフラグを渡してあげるだけです。現在の状態はuseHMSStoreselectIsLocalScreenSharedを渡してあげることで取得できます。

// src/components/VideoChat/Controller/ScreenShareController/index.tsx
import { selectIsLocalScreenShared, useHMSActions, useHMSStore } from '@100mslive/react-sdk';
import type { VFC } from 'react';
import { Switch } from '@headlessui/react';

export const ScreenShareController: VFC = () => {
  const hmsActions = useHMSActions();
  // 自分が画面共有しているかどうか
  const amIScreenSharing = useHMSStore(selectIsLocalScreenShared);

  const handleToggleShareScreen = async () => {
    try {
      // 現在の状態を反転したものを引数に渡してtoggleする
      await hmsActions.setScreenShareEnabled(!amIScreenSharing);
    } catch (error) {
      console.error({ error });
    }
  };

  return (
    // 省略
  );
};

画面共有された内容はvideoタグに接続することで表示できます。

そのために、画面共有された内容のtrackを取得する必要があります。selectScreenShareByPeerIDを使用することでidをキーに画面共有の情報を取得できるのでそれをuseHMSStoreに渡します。

今回の実装では画面共有されたらカメラの映像ではなく画面共有を移すような実装にしてます。

// src/components/VideoChat/Peer/index.tsx
import { HMSPeer, selectScreenShareByPeerID, useHMSStore, useVideo } from '@100mslive/react-sdk';
import type { VFC } from 'react';

type Props = {
  peer: HMSPeer;
};
export const Peer: VFC<Props> = ({ peer }) => {
  const screenshareVideoTrack = useHMSStore(selectScreenShareByPeerID(peer.id));
  const { videoRef } = useVideo({
    // 画面供されたらそのidを渡して、されてなければ今まで通りvideoTrackを渡す
    trackId: screenshareVideoTrack?.id ?? peer.videoTrack,
  });

  return (
    // 省略
  );
};

スピーカー・マイク・カメラのデバイスの変更

使用できるデバイスの取得

使用できるデバイスはuseHMSStoreselectDevicesを渡すことで取得できます。

返り値はDeviceMapという型で、それぞれのキーに対し配列で取得できます。

const devices = useHMSStore(selectDevices);

interface DeviceMap {
    audioInput: MediaDeviceInfo[];
    audioOutput: MediaDeviceInfo[];
    videoInput: MediaDeviceInfo[];
}

現在使用してるデバイスの取得

現在使用してるデバイスはuseHMSStoreselectLocalMediaSettingsを渡すことで取得できます。

返り値はHMSMediaSettingsという型で、それぞれのキーに対しidが取得できます。

const selectedDevices = useHMSStore(selectLocalMediaSettings);

interface HMSMediaSettings {
    audioInputDeviceId: string;
    videoInputDeviceId: string;
    audioOutputDeviceId?: string;
}

デバイスの変更

デバイスを変更するにはhmsActionsのメソッドにdeviceIdを渡します。

スピーカーの変更にはsetAudioOutputDevice、マイクの変更にはsetAudioSettings、カメラの変更にはsetVideoSettingsメソッドを使用します。引数の渡し方が異なるので注意してください。

const hmsActions = useHMSActions();

// スピーカーの変更
const handleChangeAudioOutput = (deviceId: string) => {
  // 引数は文字列で渡す
  hmsActions.setAudioOutputDevice(deviceId);
};
// マイクの変更
const handleChangeAudioInput = async (deviceId: string) => {
  // 引数はオブジェクトで渡す
  await hmsActions.setAudioSettings({ deviceId });
};
// カメラの変更
const handleChangeVideo = async (deviceId: string) => {
  // 引数はオブジェクトで渡す
  await hmsActions.setVideoSettings({ deviceId });
};

退出ボタン

ページをリロードすることでしか退出できないので退出用のボタンも作成します。

これは先ほど紹介したhmsActionsleaveメソッドをonClickハンドラに渡してあげるだけです。

// src/components/VideoChat/Controller/LeaveButton/index.tsx
import { useHMSActions } from '@100mslive/react-sdk';
import type { VFC } from 'react';

export const LeaveButton: VFC = () => {
  const hmsActions = useHMSActions();

  const handleClick = () => {
    hmsActions.leave();
  };

  return (
    <button type="button" onClick={handleClick}>
      Leave
    </button>
  );
};

ビデオチャット画面にコントローラの接続

最後に作成したコントローラを一つのコンポーネントにまとめて、ビデオチャット画面に接続すれば完了です。

// src/components/VideoChat/Controller/VideoController/index.tsx
import { selectPeers, useHMSStore } from '@100mslive/react-sdk';
import type { VFC } from 'react';
import { Controller } from './Controller';
import { Peer } from './Peer';

export const VideoChat: VFC = () => {
  const peers = useHMSStore(selectPeers);

  return (
    <div>
      <div>
        {peers.map((peer) => (
          <Peer key={peer.id} peer={peer} />
        ))}
      </div>
      {/* ページ下部に表示 */}
      <Controller />
    </div>
  );
};

終わりに

いかがでしたでしょうか。

こんな簡単にビデオチャットアプリが作成できると思ってなかったので、数時間で作成できるというだけあるなーと思いました。

業務でWebRTCを扱うので勉強しておこうという意味もあったけど、そこら辺はすべてよしなにやってくれていたので、何も勉強にはならなかった気がします。。。

それはともかく、同期からFBをもらい快適なオンラインランチ環境を整えていきたいと思います。

参考

Discussion