意外と簡単!Laravel + Vueでビデオチャットを実装する(ダウンロード可)

さてさて、この間「Laravel+Vueでリアルタイム・チャットをつくる」というテキストのチャットを実現する記事を公開しました。(実はこの記事のソースコードはブログ内でダウンロード人気TOP3だったりします。ありがとうございます😊✨)

そして、この機能を実現するために利用したのが Pusher というサービスで、Laravelの公式ドキュメントでもLaravel EchoPusherを連携させる例が紹介されていたりします。

ただ、Pusherでできることは(テキストの)チャットシステムだけでなく、なんとビデオチャットも開発することだってできるんですね。

そこで!

今回はLaravel + Vueでビデオチャット・サービスを実装してみたいと思います。

ぜひ皆さんのお役に立てると嬉しいです!
最後にソースコード一式をダウンロードすることができます。

開発環境:Laravel 5.8、Vue 2.6、Google Chrome 75

ビデオチャットを実現する仕組み

今回は、ブラウザ間で直接データのやりとりをする「WebRTC」という技術を使って実装します。

本格的には難しいので、WebRTCを簡単に説明すると、次のようにマッチングサービスのようになっています。

  1. PC間の通信に必要なデータをサーバーが準備する(紹介する段取りを決める)
  2. サーバーからデータを受け取って通信を確立する(2人を会わせる)
  3. あとは自分たちでデータ送信する(勝手にメールのやりとりなどをする)

前提として

この記事の前提として、php artisan make:authでLaravelのログイン機能がインストール済みであるものとします。もしまだの方は以下の記事を参考にして準備しておいてください。

【Laravel5.6】インストール直後にやること3点

また、ビデオチャットをテストするために2人以上のユーザーを用意しおいてください。以下のページにある「テストデータをつくる」が参考になると思います。

意外と簡単!Laravel で全文検索をつくる(Laravel Scout + Algolia)

また、Google Chromeはカメラや音声にアクセスする場合は必ずHTTPS環境であることが必要になってきます。もしローカルにHTTPSを導入していない場合は以下を参照してみてください。

コピペでOK!ローカル環境にHTTPSを導入する(nginx編)

準備する

必要なパッケージをインストールする

今回はPushersimple-peer というパッケージを使ってビデオチャットを実装するので、はじめにこれらの提供するパッケージをインストールしておきましょう。

まずはJavaScript側です。(まだビルドできる環境が整っていない場合は事前にnpm installを実行してから実行してください)

Laravelのルートフォルダに移動して以下のコマンドを実行してください。

npm install simple-peer --save-dev
npm install pusher-js --save-dev

パッケージがインストールされたらビルドJavaScriptコードから利用できるようにします。

resources/js/bootstrap.jsを開いて以下のように変更してください。

window.Peer = require('simple-peer');
window.Pusher = require('pusher-js');

※ window.Pusherは最初から記述されていますが、コメントアウトされているので、外して有効にしておいてください。

なお、今回はVueは独自にインスタンスを作りますので、resources/js/app.js内の以下の部分はコメントアウトしておきましょう。

// const app = new Vue({
//     el: '#app'
// });

では、以下のコマンドでビルドします。

npm run dev

// もしくは
npm run production

完了したら、/js/app.js/css/app.cssに全てのコードがまとまってます。

では最後にcomposerLaravel側に必要なパッケージをインストールして準備は完了です。

composer require pusher/pusher-php-server

Pusherに登録する

もちろんPusherが利用できないと今回のビデオチャットは実装できませんので、アカウントを持っていない人は以下のページを参考にして作っておいてください。

Laravel+Vueでリアルタイム・チャットをつくる(Pusherに登録する)

登録が完了したら、ログインして画面左側にある「Create new app」ボタンをクリックします。

すると、新しいアプリの登録フォームが表示されるので次のように適当な情報で登録します。

登録が完了したらPusherへのアクセスに必要な情報が表示されるので、.envにそれぞれ登録しておきましょう。

(.envの内容)

PUSHER_APP_ID=******
PUSHER_APP_KEY=********************
PUSHER_APP_SECRET=********************
PUSHER_APP_CLUSTER=ap3

さらに、クライアントからのイベントを実行するには、設定でクライアント・イベントを有効にする必要があります。

まず「App Settings」タブをクリックし、「Enable client events」にチェックを入れて「Update」ボタンをクリックします。

これでPusherの準備は完了です。

ビデオチャットするページを作る

では、Laravelでビデオチャットをするための専用ページを作っていきましょう。

ルートをつくる

まずはroutes/web.phpにルートを追加します。

Route::group(['middleware' => 'auth'], function(){

    Route::get('video_chat', 'VideoChatController@index');      // チャットページ
    Route::post('auth/video_chat', 'VideoChatController@auth'); // 認証ページ

});

※ ユーザー情報が必要なので、「authミドルウェア」でログインが必須なページにしています。

コントローラーをつくる

続いてはコントローラーです。以下のコマンドでVideoChatControllerを作成してください。

php artisan make:controller VideoChatController

app\Http/Controllers/VideoChatController.phpが作成されるので、中に2つのメソッドを追加してください。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Pusher\Pusher;

class VideoChatController extends Controller
{
    public function index(Request $request) {   // ビデオチャットページ

        $user = $request->user();
        $others = \App\User::where('id', '!=', $user->id)->pluck('name', 'id');
        return view('video_chat.index')->with([
            'user' => collect($request->user()->only(['id', 'name'])),
            'others' => $others
        ]);

    }

    public function auth(Request $request) {    // Pusherの認証

        $user = $request->user();
        $socket_id = $request->socket_id;
        $channel_name = $request->channel_name;
        $pusher = new Pusher(
            config('broadcasting.connections.pusher.key'),
            config('broadcasting.connections.pusher.secret'),
            config('broadcasting.connections.pusher.app_id'),
            [
                'cluster' => config('broadcasting.connections.pusher.options.cluster'),
                'encrypted' => true
            ]
        );
        return response(
            $pusher->presence_auth($channel_name, $socket_id, $user->id)
        );

    }
}

1つめのindex()はビデオチャットが表示されるページになるので、次のデータをビューに送っています。

  • ログイン中のユーザー情報
  • ログインしているユーザー以外のデータ(これから通話する人たち)

そして、2つ目のauth()Pusherの認証を実行するメソッドで、これから作るJavaScriptが送信してくる以下のデータで認証することになります。

  • socket_id ・・・ 接続ID
  • channel_name ・・・ チャンネル名

ビューをつくる

最後にresources/views/video_chat/index.blade.phpを作って中身を以下のように変更しましょう。

<html>
<head>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <style>

        video {
            width: 100%
        }

    </style>
</head>
<body>
    <div id="app" class="container">
        <h1 class="text-center">ビデオチャットのサンプル</h1>
        <br>
        <div class="row">
            <div class="col-12">
                <div class="card" style="padding:15px;">
                    <div v-for="(name,userId) in others">
                        <a href="#" @click.prevent="startVideoChat(userId)">「@{{ name }}」さんと通話を開始する</a>
                    </div>
                </div>
            </div>
        </div>
        <br>
        <div class="row">
            <div class="col-5">
                <div class="text-center">自分の映像</div>
                <video ref="video-here" autoplay></video>
            </div>
            <div class="col-2 text-center">
                ⇔<br>
                ビデオチャット
            </div>
            <div class="col-5">
                <div class="text-center">相手の映像</div>
                <video ref="video-there" autoplay></video>
            </div>
        </div>
    </div>
    <script src="/js/app.js"></script>
    <script>

        new Vue({
            el: '#app',
            data: {
                pusher: {
                    key: '{{ config('broadcasting.connections.pusher.key') }}',
                    cluster: '{{ config('broadcasting.connections.pusher.options.cluster') }}'
                },
                user: {!! $user !!},
                others: {!! $others !!},
                channel: null,
                stream: null,
                peers: {}
            },
            methods: {
                startVideoChat(userId) {

                    this.getPeer(userId, true);

                },
                getPeer(userId, initiator) {

                    if(this.peers[userId] === undefined) {

                        let peer = new Peer({
                            initiator,
                            stream: this.stream,
                            trickle: false
                        });
                        peer.on('signal', (data) => {

                                this.channel.trigger('client-signal-'+ userId, {
                                    userId: this.user.id,
                                    data: data
                                });

                            })
                            .on('stream', (stream) => {

                                const videoThere = this.$refs['video-there'];
                                videoThere.srcObject = stream;

                            })
                            .on('close', () => {

                                const peer = this.peers[userId];

                                if(peer !== undefined) {

                                    peer.destroy();

                                }

                                delete this.peers[userId];
                            });

                        this.peers[userId] = peer;

                    }

                    return this.peers[userId];

                }
            },
            mounted() {

                // エラー表示できます。
                // Pusher.logToConsole = true;

                // カメラ、音声にアクセス
                navigator.mediaDevices.getUserMedia({ video: true, audio: true })
                    .then((stream) => {

                        const videoHere = this.$refs['video-here'];
                        videoHere.srcObject = stream;
                        this.stream = stream;

                        // Pusher の準備
                        const pusher = new Pusher(this.pusher.key, {
                            authEndpoint: '/auth/video_chat',
                            cluster: this.pusher.cluster,
                            auth: {
                                headers: {
                                    'X-CSRF-Token': document.head.querySelector('meta[name="csrf-token"]').content
                                }
                            }
                        });
                        this.channel = pusher.subscribe('presence-video-chat');
                        this.channel.bind('client-signal-'+ this.user.id, (signal) => {

                            const userId = signal.userId;
                            const peer = this.getPeer(userId, false);
                            peer.signal(signal.data);

                        });

                    });

            }
        });

    </script>
</body>
</html>

では、この中でやっていることを順を追って説明します。

HTML部分

まず、以下の部分では自分「以外」のユーザー名のリンクが作成されて、クリックするとビデオ通話を試みます。

<div v-for="(name,userId) in others">
    <a href="#" @click.prevent="startVideoChat(userId)">「@{{ name }}」さんと通話を開始する</a>
</div>

また、その下ではHTML5<video></video>タグを2つ作りそれぞれ以下の内容で設定しています。

  • video-here ・・・ 自分の映像を表示するvideoタグ
  • video-there ・・・ 相手の映像を表示するvideoタグ

カメラ、マイクへのアクセス部分

mounted()はページが読み込まれたらすぐに実行されるメソッドなので、まずカメラ&マイクへアクセスします。(実際には、ユーザーが許可を出したらアクセスができるようになります)

カメラ&マイクへのアクセス権限が取得できたら、video-herevideoタグへ映像&音声を流します。

Pusher部分

次にPusherを起動します。Pusherの設定内容は以下のとおりです。

  • authEndpoint ・・・ 先ほどコントローラーで設定したauth()のURL
  • this.pusher.key、cluster ・・・ Pusher のアクセスデータ。Vueの “data” 内から呼び出していますが、元々は.envを参照したものです。
  • headers ・・・ 認証はPost送信になるので、CSRF対策でアクセス拒否されないように “headers” にトークンを追加しています。

そして、Pusherの認証に成功したらシグナルを実行するイベントをバインディングすることになります。

simple-peer部分

simple-peergetPeer()メソッドの中で記述されています。

やっていることは、まずインスタンスを作成し(自分側と相手側の2パターンがあります)、後はそれぞれ「シグナル」「ストリーム」「閉じる」イベントを作成しているだけです。(ストリームは相手側の映像&音声なので、video-therevideoタグにデータを流しています)

ちなみに

元々は、ブロックごとに説明しようと考えていたのですが、なにせ結構複雑な内容なので分けてしまうと逆にわかりにくくなると思い、一気にコードを紹介することにしました。これでもわかりにくかったらゴメンナサイ😅

そして、ブラウザで見るとこうなります。(まだ通話していないので自分の映像しか写っていません。ウェブカメラにはシールを貼ってるので灰色ですが。。)

テストしてみる

では、実際にPCとスマホのブラウザでアクセスして両者でビデオ通話をしてみましょう。

ちなみに、今回はテストとして昔UFOキャッチャーでとった「ぼのぼの」のシマリス君を使ってみたいと思います。

準備としては、まずPCとスマホどちら側からも別のユーザーでログインしておく必要があります。

そして、どちらか片方から名前の書かれたリンクをクリックすると自動的に通話が開始されます。

では、実際にやってみましょう!

↓↓

 

↓↓

 

↓↓

 

はい!
ちょっと分かりにくいかもしれませんが、うまく2方向からのビデオ通話を実現することができました。

お疲れ様でした!

[参考URL]:
https://www.youtube.com/watch?v=5pnsloZzYQM

ソースコードをダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ ただし、パッケージのインストールやPusherの作業、事前設定等はご自身で行っていただく必要があります。また、今回はテストなのでエラー処理は全く行っていません。

※ また、テストのときの音声には気をつけてください。音量がMAXの状態でハウリングがおこって気絶しそうになりました💦

Laravel + Vueでビデオチャット

おわりに

ということで、今回はPusherを使ってビデオチャットを実装してみました。近年のブラウザはできることが増えてきて、今やスカイプがなくてもこんなことができるようになっているなんて、ちょっとテンションが上がってしまいました。

なお、WebRTCはブラウザ間だけでなくスマホのアプリやネイティブアプリとも通信ができるので、より可能性が大きいテクノロジーといっていいでしょう。

みなさんもぜひ取り入れてみてはいかがでしょうか。

ではでは〜!