23. video chat with react - hpscript/laravel GitHub Wiki

# 前準備 $ php composer.phar create-project --prefer-dist laravel/laravel chatapp
$ cp composer.phar chatapp/
$ cd chatapp
// JavaScriptアプリケーションの構築に、Reactを使いたければ、VueのスカフォールドをReactのスカフォールドへ簡単に変更できる
// presetを実行すると、package.json, webpack.mix.jsなどが書き換えられる
$ php artisan preset --help
$ php artisan preset react
$ npm install

$ npm install --save simple-peer pusher-js
$ php composer.phar require pusher/pusher-php-server

$ touch database/database.sqlite
.env

DB_CONNECTION=sqlite

$ php artisan migrate

// make auth
$ php composer.phar require laravel/ui --dev
$ php artisan ui react --auth
$ npm install
$ npm run dev
$ npm run watch

実装

resources/js/MediaHandler.js

class MediaHandler {
	getPermissions(){
		return new Promise((res, rej) => {
			navigator.MediaDevices.getUserMedia({video: true, audio: true})
				.then((stream) =>{
					resolve(stream);
				})
				.catch(err => {
					throw new Error('Unable to fetch stream ${err}');
				})
		});
	}
}

resources/js/components/App.js

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
    return (
        <div className="app">This is my video chat</div>
    );
}

export default App;

if (document.getElementById('app')) {
    ReactDOM.render(<App />, document.getElementById('app'));
}

resources/js/app.js

require('./components/App');

resources/views/layouts/app.blade.php

<body>
    <div id="container">
    // 省略

resources/views/home.blade.php

@section('content')
<div id="app"></div>
@endsection
import React from 'react';
import ReactDOM from 'react-dom';
import MediaHandler from '../MediaHandler';
import Pusher from 'pusher-js';
import peer from 'simple-peer';

const APP_KEY = '';

function App(){
    constructor(props) {
        super(props);

        this.state = {
            hasMedia: false,
            otherUserId: null;
        };

        this.user = window.user;
        this.user.stream = null;
        this.peers = {};

        this.mediaHandler = new MediaHandler();
        this.setupPusher();

        this.callTo = this.callTo.bind(this);
        this.setupPusher = this.setupPusher.bind(this);
        this.startPeer = this.startPeer.bind(this);
    }

    componentWillMount(){
        this.mediaHandler.getPermissions()
            .then((stream) => {
                this.setState(hasMedia: true);
                this.user.stream = stream;

                try {
                    this.myVideo.srcObject = stream;
                } catch(e){
                    this.myVideo.src = URL.createObjectURL(stream);
                }   

                this.myVideo.play();
            })
    }

    setupPusher(){
        this.pusher = new Pusher(APP_KEY, {
            authEndpoint: '/pusher/auth',
            cluster: 'ap2',
            auth: {
                params: this.user.id,
                headers: {
                    'X-CSRF-Token': window.csrfToken
                }
            } 
        });

        this.channel = this.pusher.subscribe('presence-video-channel');
        this.channel.bind('client-signal-${this.user.id}', (signal)=>{
            let peer = this.peers[signal.userId];

            // if peer is not already exists, we got an incomming call
            if(peer == undefined){
                this.setState({otherUserId: signal.userId});
                peer = this.startPeer(signal.userId, false);
            }

            peer.signal(signal.data);
        });
    }

    startPeer(userId, initiator = true){
        const peer = new Peer({
            initiator,
            stream: this.user.stream,
            trickle: false;
        });

        peer.on('signal', (data) => {
            this.channel.trigger('client-signal-${userId}', {
                type: 'signal',
                userId: this.user.id,
                data: data
            });
        });

        peer.on('stream', (stream) => {
                try {
                    this.userVideo.srcObject = stream;
                } catch(e){
                    this.userVideo.src = URL.createObjectURL(stream);
                }   
                this.userVideo.play();
        });

        peer.on('close', () => {
            let peer = this.peers[userId];
            if(peer == undefined){
                peer.destroy();
            }
            this.peers[userId] = undefined;

        });
        return peer;
    }

    callTo(userId){
        this.peers[userId] = this.startPeer(userId);
    }

    return (
        <div className="app">
            {[1,2,3,4].map((userId)=> {
                return this.user.id !== userId ? <button key={userId} onClick={() => this.callTo(UserId)}>Call {userId}</button> : null;
            })}
            <div className="video-container">
                <video className="my-video" ref={(ref) => {this.myVideo = ref;}}></video>
                <video className="user-video" ref={(ref) => {this.userVideo = ref;}}></video>
            </div>
        </div>
    );
}

export default App;
    

if (document.getElementById('app')) {
    ReactDOM.render(<App />, document.getElementById('app'));
}

Route

Route::post('/pusher/auth', 'HomeController@authenticate');

Controller

use Pusher\Pusher;
public function authenticate(Request $request){
        $socketId = $request->socket_id;
        $channelName = $request->channel_name;

        $pusher = new Pusher('App key', 'App Secrete', 'App id', [
            'cluster' => 'ap2',
            'encrypted' => true
        ]);

        $presense_data = ['name' => auth()->user()->name];
        $key = $pusher->presence_auth($channelName, $socketId, auth()->id(), $presence_data);

        return response($key);
    }

https://www.youtube.com/watch?v=5pnsloZzYQM

⚠️ **GitHub.com Fallback** ⚠️