九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、最近は「リモートワーク」という名前もよく耳にするようになり、世の中の変化を感じずにはいられない今日この頃です。
そして、このリモートワークを実現するために必要だったのが、
ビデオ通話
といってもいいんじゃないでしょうか。
さすがに、音声のみの会話ではあまり知らない人とコミュニケーションがしにくいですし、表情を見て話したほうがより話が伝わりやすいですよね。
そう考えると、テクノロジーの進化によって私達の生活が変化していることを実感できる身近な例かもしれません。
そこで❗
今回はこのビデオ通話機能を「Laravel + React」で実装してみたいと思います。
ぜひ何かの参考になりましたら嬉しいです。
「記事の中で
好きなポテトチップス
を紹介しています」
開発環境: Laravel 9.x、React、Vite、Intertia.js、TailwindCSS
目次
前提として
ログイン機能がインストールされていて、ユーザーが以下のように2人以上登録されていることが前提です。
もしログイン機能をインストールしていない方は、以下を参考にして準備しておいてください。
📝【Laravel】Vite + Inertia + React でログイン機能をインストールする
また、今回はウェブカメラを利用しますので、HTTPS
での接続が必要になります。HTTPS
でVite
を使う方法は、「ちなみに1:Vite を HTTPS 対応にするには」を参考にしてください。
Pusher Channels にプロジェクトをつくる
では、まずはコードを書いていく前にPusher
で専用プロジェクトをつくり、アクセストークンなどを取得します。
Pusher
へユーザー登録&ログインし、Channels
の中にある「Manage」ボタンをクリックします。
ページ移動すると「Create app」ボタンがあるのでこれをクリック。
すると、プロジェクト(Channels app)の詳細入力フォームが表示されるので、以下を参考にして適宜入力&「Create app」ボタンをクリックしてください。
- Name your app: プロジェクト名
- Select a cluster: ap3(東京)
- Choose your tech stack:(任意。選択するとサンプルコードが表示されます)
※ なお、サンプルコードは以下のようになります。
これでプロジェクトが作成されたので、次は「App Settings」メニューのリンクをクリックします。
ページ中程にある「Enable client events」にチェックを入れます。
では、最後にAPI
にアクセスするためのキー(トークン)を取得します。
同じくメニューから「App Keys」リンクをクリックしてください。
すると、以下のように各種キーが表示されます。
では、取得した各種キーを.env
へ追加しておきましょう。(おそらくすでにPUSHER_
で始まる項目は存在しています)
.env
PUSHER_APP_ID=****** PUSHER_APP_KEY=****************** PUSHER_APP_SECRET=****************** PUSHER_HOST= PUSHER_PORT=443 PUSHER_SCHEME=https PUSHER_APP_CLUSTER=***
そして、ついでですのでブロードキャストのドライバーがpusher
になっているか確認しておいてください。
BROADCAST_DRIVER=pusher
また、先ほど.env
にセットしたキーがコンフィグから取得できるようになっているか確認しておいてください。
config/broadcasting.php
// 省略 'connections' => [ 'pusher' => [ 'driver' => 'pusher', 'key' => env('PUSHER_APP_KEY'), 'secret' => env('PUSHER_APP_SECRET'), 'app_id' => env('PUSHER_APP_ID'), 'options' => [ 'host' => env('PUSHER_HOST', 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com', 'port' => env('PUSHER_PORT', 443), 'scheme' => env('PUSHER_SCHEME', 'https'), 'encrypted' => true, 'useTLS' => env('PUSHER_SCHEME', 'https') === 'https', ], 'client_options' => [ // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html ], 'cluster' => env('PUSHER_APP_CLUSTER'), ], // 省略
※ もしかすると、cluster
は抜けてるかもです🤔
これでPusher
側の作業は完了です。
パッケージをインストールする
次に、今回必要なパッケージをインストールします。
まずはphp
側です。
以下のコマンドを実行してください。
composer require pusher/pusher-php-server
そして、同じくJavaScript
側にも以下3つのnpm
パッケージをインストールします。
npm i --save-dev laravel-echo pusher-js simple-peer
そして、Laravel Echo
とsimple-peer
がVite
で使えるようにしておきましょう。
resources/js/bootstrap.js
// 初期状態ではコメントアウトされています import Echo from 'laravel-echo'; import Pusher from 'pusher-js'; window.Pusher = Pusher; window.Echo = new Echo({ broadcaster: 'pusher', key: import.meta.env.VITE_PUSHER_APP_KEY, wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', enabledTransports: ['ws', 'wss'], }); // simple-peer を追加しました import Peer from 'simple-peer/simplepeer.min.js' window.Peer = Peer;
※ なお、import Peer from 'simple-peer';
とするとVite
ではエラーが出るようでしたので、こちら を参考に変更しています。(今後修正されるかもです👍)
ルートをつくる
続いて、ルートです。
まずはブラウザから直接アクセスするためのものを作ります。
routes/web.php
// 省略 use App\Http\Controllers\VideoCallController; // 省略 Route::prefix('video_call')->controller(VideoCallController::class)->group(function(){ Route::get('/', 'index')->name('video_call.index'); });
そして、もうひとつPusher
の認証に使うルートをchannels.php
にセットします。
routes/channels.php
// 省略 Broadcast::channel('video-call', function ($user) { return ['id' => $user->id, 'name' => $user->name]; });
コントローラーをつくる
次に、コントローラーです。
以下のコマンドを実行してください。
pa make:controller VideoCallController
すると、ファイルが作成されるので中身を以下のようにします。
app/Http/Controllers/VideoCallController.php
<?php namespace App\Http\Controllers; use App\Models\User; use Illuminate\Http\Request; use Inertia\Inertia; class VideoCallController extends Controller { public function __construct() { $this->middleware('auth'); // 要ログインにする } public function index(Request $request) { $user = $request->user(); $other_users = User::select('id', 'name') ->where('id', '!=', $user->id) ->get(); // 自分以外のユーザーを取得 return Inertia::render('VideoCall/Index', [ 'user' => $user->only('id', 'name'), 'otherUsers' => $other_users, ]); } }
ビューをつくる
では、ここがメインで(ちょっとハマった)部分です。
resources/js/Pages/VideoCall/Index.jsx
import {useEffect, useRef} from "react"; export default function Index(props) { // User const user = props.user; const otherUsers = props.otherUsers; // Pusher const pusherKey = import.meta.env.VITE_PUSHER_APP_KEY; const pusherCluster = import.meta.env.VITE_PUSHER_APP_CLUSTER; // Video const myVideoRef = useRef(null); const otherVideoRef = useRef(null); const videoStreamRef = useRef(null); const videoChannelRef = useRef(null); const peers = useRef({}); const getEventName = userId => `client-signal-${userId}`; const getPeer = (targetUserId, initiator) => { if(peers.current[targetUserId] === undefined) { const peer = new Peer({ initiator, stream: videoStreamRef.current, trickle: false, }); peer .on('signal', data => { const eventName = getEventName(targetUserId); videoChannelRef.current.trigger(eventName, { userId: user.id, data: data, }); }) .on('stream', stream => otherVideoRef.current.srcObject = stream) .on('close', () => deletePeer(targetUserId)); // 通話するユーザーを追加 peers.current = { ...peers.current, ...{ [targetUserId]: peer } }; } return peers.current[targetUserId]; }; const deletePeer = userId => { const peer = peers.current[userId]; if(peer !== undefined) { peer.destroy(); } const { [userId]: _, ...newPeers } = peers.current; // 該当するユーザーを削除 peers.current = newPeers; }; useEffect(() => { navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then(stream => { myVideoRef.current.srcObject = stream; videoStreamRef.current = stream; const pusher = new Pusher(pusherKey, { authEndpoint: '/broadcasting/auth', cluster: pusherCluster, }); const channel = pusher.subscribe('private-video-call'); const eventName = getEventName(user.id); channel.bind(eventName, signal => { const userId = signal.userId; const peer = getPeer(userId, false); peer.signal(signal.data); }); videoChannelRef.current = channel; }); }, []); return ( <div className="p-5 bg-gray-100"> <div className="p-4 rounded-lg bg-white mb-3"> {otherUsers && otherUsers.map(otherUser => ( <div key={otherUser.id}> <a href="#" className="text-blue-500" onClick={() => getPeer(otherUser.id, true)}> {otherUser.name} さんと通話する </a> </div> ))} </div> <div className="flex"> <div className="flex-1 p-3 border bg-white"> <div className="text-center">自分の映像</div> <video ref={myVideoRef} autoPlay></video> </div> <div className="px-5 pt-3"> <div className="text-center font-bold">↔</div> </div> <div className="flex-1 p-3 border bg-white"> <div className="text-center">相手の映像</div> <video ref={otherVideoRef} autoPlay></video> </div> </div> </div> ); }
基本的には2つvideo
タグを用意して、そこへそれぞれのストリームをセットする形なのですが、当初はそれぞれ値をuseState
で保持していたためうまくいきませんでした。
というのも、useState
の書き換えは非同期な処理(≒別世界の出来事)のため、ほしいタイミングでデータ取得できないからです。
そのため、今回のケースではuseRef
をふんだんに使っています。
useRef
はxxxxxxRef.current
で最新状態のデータを取得することができるんですね。(ただし、useRef
は再レンダリングされないので見た目の変更用には使えません)
以上で作業は完了です。
おつかれさまでした❗
ちなみに1: Vite を HTTPS 対応にするには
Vite
は初期状態では証明書が入っていないため、ブラウザではhttps
としてアクセスができません。
そのため、必要なパッケージをインストールする必要があります。
では、以下のコマンドを実行してください。
npm i --save-dev vite-plugin-mkcert
そして、コンフィグを以下のように変更します。
vite.config.js
import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import react from '@vitejs/plugin-react'; import mkcert from'vite-plugin-mkcert' // 👈 ここを追加しました export default defineConfig({ plugins: [ laravel({ input: 'resources/js/app.jsx', ssr: 'resources/js/ssr.jsx', refresh: true, }), react(), mkcert(), // 👈 ここを追加しました ], ssr: { noExternal: ['@inertiajs/server'], }, server: { https: true, // 👈 ここを追加しました } });
これでVite
を起動し、「https://127.0.0.1:5173/@vite/client」へアクセスします。(環境によって127.0.0.1:5173
は変更してください)
すると、以下のようなページが表示されます。
そして、「詳細設定」ボタンをクリックすると以下のようなコンテンツが表示されますので、「127.0.0.1 にアクセスする(安全ではありません)」リンクをクリックします。
※ 確かにサーバー上でやると危険な場合がありますが、ここはローカルの環境ですので問題があることは少ないでしょう。(とはいえ自己責任でお願いしますね😄)
すると、Vite
のサーバーページが表示されますので、これでhttps
が有効になります👍
ちなみに2:ビルドする時「Some chunks are larger…」という警告が出たら
私の環境では、Vite
でビルドするとき以下のような警告がでました。
(!) Some chunks are larger than 500 KiB after minification.
ビルド自体はちゃんとできるので問題ないのですが、なんか気持ち悪いので設定を変更しました。
vite.config.js
// 省略 export default defineConfig({ // 省略 build: { // 👈 このブロックを追加しました chunkSizeWarningLimit: 1000 } });
どうやら各ファイルのサイズがこの値(デフォルト: 500 kb
)を超えると警告がでるようです。
テストしてみる
では、実際にテストしてみましょう❗
まずVite
はdev
バージョンでは外部からのアクセスに対応できませんので、npm run build
でビルドを実行します。
そして、この状態のまま(開発コマンドを実行せず)2つの環境から「https://******/video_call」へアクセスします。
※ ちなみに、私の環境では1つはメインPCで実行し、もう一方はローカルネットワークでスマホからアクセスしました。
すると、ログインフォームが表示されるので、それぞれ別のユーザーでログインします。(今回のケースでは、「太郎」さんと「次郎」さんでログインします)
ログインすると、以下のように「(自分以外の)ユーザーリスト」「自分用のカメラ」が表示されます。
では、「太郎」さんでログインした方(メインPC)から「次郎」さんのリンクをクリックして通話してみます。
※ なお、今回は私の大好物の海外製ポテトチップスLay's
を双方向から撮影してみます。(ホントはoriginal
が一番好きですが食べちゃったのでsour cream & onion
です)
どうなったでしょうか・・・・・・
はい❗
(ちょっとわかりにくいかもですが…)うまく双方向からの映像が表示されました。
また、キィーーーーン!!!とハウリングしていたのでおそらく音声の方もうまくいっていると思います。(お気をつけください。m(_ _)m)
成功です😄✨
企業様へのご提案
今回の技術を使うと、独自のビデオ通話機能をつくることができますので、例えば社内ツールへ組み込んだり、タブレットなどを設置しておいてインターホンのように使うこともできるでしょう。
また、今回利用したPusher
は月間200,000回までのメッセージは無料(2022.10.28現在)で使えますのでそれほど利用回数が多くない場合は経費を抑えることもできます。詳しくは こちら をご参照ください。
なお、もしこういったご相談がございましたら、ぜひお問い合わせからご連絡ください。
お待ちしております。😄✨
おわりに
ということで、今回は「Laravel + React」でビデオ通話機能を作ってみました。
実はこのブログでは過去にVue
で同じ機能を作ってましたが、3年前(2019年)だったので、フレームワークやパッケージのバージョン違いでコードが変更になっている部分がありました。
また、当時はPusher
の認証をするルートを独自につくっていましたが、今回はLaravel
公式の方法でセットできましたので、ちょっと嬉しかったりもしています👍
ぜひ皆さんも(ちょっと複雑ですが…)楽しみながらやってみてくださいね。
ではでは〜❗
「寝る前なのにポテトチップス
食べちゃった(笑)」