Next.jsと100msで作るVideo Chat App
はじめに
皆さん、こんにちは。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」というページに遷移すると思うので、下記のように質問に答えていってください。
- 「Virtual Events」を選択し、Next
- 好きなドメインを入力してSet up App (Regionも選べます)
- 設定終了です!
ここまできたら右下の「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に参加するにはhmsActions
のjoin
メソッドを使用します。
このメソッドを使用するにはuserName
とauthToken
が必要になります。
userName
は参加者が任意で決められる文字列です。
authToken
はダッシュボードからコピーしてきます。
authTokenのコピー
- ダッシュボード左側のメニューからRoomを選択し、RoomIdを選択
- ページ右上のJoin roomを選択
- 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に参加できるようになったので、退出できるようにします。
退出にはhmsActions
のleave
メソッドを使用します。
ページを離れたタイミングで退出できるように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
を渡して、返却されるref
をvideo
タグに接続すると表示されます。
// 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>
);
};
状態に応じてコンポーネントを出し分ける
最低限の実装が完了したので作成したコンポーネントをトップページに接続します。
useHMSStore
にselectIsConnectedToRoom
を渡すことで取得できる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 (
// 省略
);
};
画面共有のオンオフ
画面共有も簡単に実現可能です。hmsActions
のsetScreenShareEnabled
メソッドにフラグを渡してあげるだけです。現在の状態はuseHMSStore
にselectIsLocalScreenShared
を渡してあげることで取得できます。
// 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 (
// 省略
);
};
スピーカー・マイク・カメラのデバイスの変更
使用できるデバイスの取得
使用できるデバイスはuseHMSStore
にselectDevices
を渡すことで取得できます。
返り値はDeviceMap
という型で、それぞれのキーに対し配列で取得できます。
const devices = useHMSStore(selectDevices);
interface DeviceMap {
audioInput: MediaDeviceInfo[];
audioOutput: MediaDeviceInfo[];
videoInput: MediaDeviceInfo[];
}
現在使用してるデバイスの取得
現在使用してるデバイスはuseHMSStore
にselectLocalMediaSettings
を渡すことで取得できます。
返り値は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 });
};
退出ボタン
ページをリロードすることでしか退出できないので退出用のボタンも作成します。
これは先ほど紹介したhmsActions
のleave
メソッドを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