あなたのLaravelアプリにビデオチャットを追加する


導入


私はVuejsとLaravelプロジェクトのカスタムビデオチャットアプリケーションを構築する必要がありました.私は、それを作動させるために、たくさんのフープを経験しました.私は、私がここの上でプロセスを通して学んだすべてを共有します.
最終プロジェクトリポジトリ https://github.com/Mupati/laravel-video-chat

要件

  • このチュートリアルではLaravel プロジェクトVueJs 認証.プロジェクトの設定後にユーザーを作成します.あなたはLaravelの放送メカニズムに精通していなければならなくて、WebSocketsがどのように働くかについての公正な考えを持っていなければなりません.
  • 無料プッシャーアカウントを設定するpusher.com
  • あなたのICEサーバーを設定します.このチュートリアルは良いガイドです.HOW TO INSTALL COTURN .
  • プロジェクト設定


    # Install needed packages
    composer require pusher/pusher-php-server "~4.0"
    npm install --save laravel-echo pusher-js simple-peer
    

    バックエンドの設定

  • のビデオページのルートを追加routes/web.php .
    ルートは、ビデオコールページを訪問し、呼び出しを行い、呼び出しを受け取るために使用されます.
  • Route::get('/video-chat', function () {
        // fetch all users apart from the authenticated user
        $users = User::where('id', '<>', Auth::id())->get();
        return view('video-chat', ['users' => $users]);
    });
    
    // Endpoints to call or receive calls.
    Route::post('/video/call-user', 'App\Http\Controllers\VideoChatController@callUser');
    Route::post('/video/accept-call', 'App\Http\Controllers\VideoChatController@acceptCall');
    
  • コメントBroadcastServiceProvider インconfig/app.php . これによりララベルの放送システムを利用できる.
  • + App\Providers\BroadcastServiceProvider::class
    - //App\Providers\BroadcastServiceProvider::class 
    
  • ビデオチャットアプリケーションのプレゼンスチャンネルを作成するroutes/channels.php . 認証されたユーザーがチャンネル(存在ビデオチャンネル)を購読するとき、我々はユーザーのものを返しますid and name . これにより、ログインしているユーザを得ることができます.
  • Broadcast::channel('presence-video-channel', function($user) {
        return ['id' => $user->id, 'name' => $user->name];
    });
    
  • クリエイトStartVideoChat イベント.このイベントは、呼び出しを置くか、呼び出しを受け入れるとき、呼ばれます、そして、それは存在ビデオ通話チャンネルで放送されます.チャンネルを購読したユーザーは、着信通知が引き起こされることができるようにフロントエンドでこのイベントを聞いています.
  • php artisan make:event StartVideoChat
    
  • 次のコードを追加しますapp/Events/StartVideoChat.php . StartVideochatイベントを放送するpresence-video-channel ビデオ・コールを始めるのに必要なデータがチャンネルで共有されるように.
  • <?php
    
    namespace App\Events;
    
    use Illuminate\Broadcasting\InteractsWithSockets;
    use Illuminate\Broadcasting\PresenceChannel;
    use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
    use Illuminate\Foundation\Events\Dispatchable;
    use Illuminate\Queue\SerializesModels;
    
    class StartVideoChat implements ShouldBroadcast
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;
    
        public $data;
        /**
         * Create a new event instance.
         *
         * @return void
         */
        public function __construct($data)
        {
            $this->data = $data;
        }
    
        /**
         * Get the channels the event should broadcast on.
         *
         * @return \Illuminate\Broadcasting\Channel|array
         */
        public function broadcastOn()
        {
            return new PresenceChannel('presence-video-channel');
        }
    }
    
  • クリエイトVideoChatController 呼び出しをして、受け入れるために.
  • php artisan make:controller VideoChatController
    
  • 以下を加えるVideoChatController
  • <?php
    
    namespace App\Http\Controllers;
    
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Http\Request;
    use App\Events\StartVideoChat;
    
    class VideoChatController extends Controller
    {
    
        public function callUser(Request $request)
        {
            $data['userToCall'] = $request->user_to_call;
            $data['signalData'] = $request->signal_data;
            $data['from'] = Auth::id();
            $data['type'] = 'incomingCall';
    
            broadcast(new StartVideoChat($data))->toOthers();
        }
        public function acceptCall(Request $request)
        {
            $data['signal'] = $request->signal;
            $data['to'] = $request->to;
            $data['type'] = 'callAccepted';
            broadcast(new StartVideoChat($data))->toOthers();
        }
    }
    

    ビデオチャットコントローラのメソッド


    つのことを理解するVideoChatアプリケーションは、Webソケットで動作するリアルタイムアプリケーションです.エンドポイントは、通信データがWebSocketを通して交換される2つの呼び出しパーティーの間のリンクを確立するためにちょうど必要です.
    コントローラの2つの方法が何をしているかを理解しようとしましょう.

    callUserメソッド

  • user_to_call : id ユーザーの呼び出しの開始元に到達したい.
  • signal_data : WEBRTCクライアントからの発信者によって送られる初期の信号データ(申し出)(単純なpeerjsは我々が使用しているwebrtcラッパーです).
    これらは受信したパラメータです.
    私たちはdata 2つの追加プロパティを持つオブジェクトfrom and type その後、データをStartVideoChat フロントエンドで聴くイベント.
  • from : はid 呼び出しを置くユーザの.認証されたユーザIDを使用します.
  • type : チャネルに着信があることを示すデータのプロパティです.通知はユーザーに表示されますid の値user_to_call .
  • アクセプタメソッド

  • signal : これがカリーのanswer データ.
  • to : 呼び出し元の呼び出し元id . 応答した呼のための信号データは、IDが一致するユーザに送信されるto そして、これは発信者のIDであると思われます.
  • type : 呼び出し受取人が呼び出しを受け入れたことを示しているチャンネルの上に送られるデータに加えられる資産.
  • フロントエンドの設定

  • インスタンス化するLaravel Echo and Pusher インresources/js/bootstrap.js コードの次のブロックをコメントアウトします.
  • + import Echo from 'laravel-echo';
    + window.Pusher = require('pusher-js');
    + window.Echo = new Echo({
    +     broadcaster: 'pusher',
    +     key: process.env.MIX_PUSHER_APP_KEY,
    +     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    +     forceTLS: true
    + });
    - import Echo from 'laravel-echo';
    - window.Pusher = require('pusher-js');
    - window.Echo = new Echo({
    -     broadcaster: 'pusher',
    -     key: process.env.MIX_PUSHER_APP_KEY,
    -     cluster: process.env.MIX_PUSHER_APP_CLUSTER,
    -     forceTLS: true
    -});
    
  • クリエイトresources/js/helpers.js . 加えるgetPermissions 機能は、マイクやビデオのアクセス許可を支援する.このメソッドは、ビデオ呼び出しをするためにブラウザによって必要とされるビデオとオーディオ許可を扱います.ビデオコールを進める前に、ユーザがパーミッションを受け入れるのを待ちます.我々は両方のオーディオとビデオを許可します.
    続きを読むMDN Website .
  • export const getPermissions = () => {
        // Older browsers might not implement mediaDevices at all, so we set an empty object first
        if (navigator.mediaDevices === undefined) {
            navigator.mediaDevices = {};
        }
    
        // Some browsers partially implement mediaDevices. We can't just assign an object
        // with getUserMedia as it would overwrite existing properties.
        // Here, we will just add the getUserMedia property if it's missing.
        if (navigator.mediaDevices.getUserMedia === undefined) {
            navigator.mediaDevices.getUserMedia = function(constraints) {
                // First get ahold of the legacy getUserMedia, if present
                const getUserMedia =
                    navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
    
                // Some browsers just don't implement it - return a rejected promise with an error
                // to keep a consistent interface
                if (!getUserMedia) {
                    return Promise.reject(
                        new Error("getUserMedia is not implemented in this browser")
                    );
                }
    
                // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
                return new Promise((resolve, reject) => {
                    getUserMedia.call(navigator, constraints, resolve, reject);
                });
            };
        }
        navigator.mediaDevices.getUserMedia =
            navigator.mediaDevices.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia;
    
        return new Promise((resolve, reject) => {
            navigator.mediaDevices
                .getUserMedia({ video: true, audio: true })
                .then(stream => {
                    resolve(stream);
                })
                .catch(err => {
                    reject(err);
                    //   throw new Error(`Unable to fetch stream ${err}`);
                });
        });
    };
    
  • ビデオチャットコンポーネントを作成します.resources/js/components/VideoChat.vue .
  • <template>
      <div>
        <div class="container">
          <div class="row">
            <div class="col">
              <div class="btn-group" role="group">
                <button
                  type="button"
                  class="btn btn-primary mr-2"
                  v-for="user in allusers"
                  :key="user.id"
                  @click="placeVideoCall(user.id, user.name)"
                >
                  Call {{ user.name }}
                  <span class="badge badge-light">{{
                    getUserOnlineStatus(user.id)
                  }}</span>
                </button>
              </div>
            </div>
          </div>
          <!--Placing Video Call-->
          <div class="row mt-5" id="video-row">
            <div class="col-12 video-container" v-if="callPlaced">
              <video
                ref="userVideo"
                muted
                playsinline
                autoplay
                class="cursor-pointer"
                :class="isFocusMyself === true ? 'user-video' : 'partner-video'"
                @click="toggleCameraArea"
              />
              <video
                ref="partnerVideo"
                playsinline
                autoplay
                class="cursor-pointer"
                :class="isFocusMyself === true ? 'partner-video' : 'user-video'"
                @click="toggleCameraArea"
                v-if="videoCallParams.callAccepted"
              />
              <div class="partner-video" v-else>
                <div v-if="callPartner" class="column items-center q-pt-xl">
                  <div class="col q-gutter-y-md text-center">
                    <p class="q-pt-md">
                      <strong>{{ callPartner }}</strong>
                    </p>
                    <p>calling...</p>
                  </div>
                </div>
              </div>
              <div class="action-btns">
                <button type="button" class="btn btn-info" @click="toggleMuteAudio">
                  {{ mutedAudio ? "Unmute" : "Mute" }}
                </button>
                <button
                  type="button"
                  class="btn btn-primary mx-4"
                  @click="toggleMuteVideo"
                >
                  {{ mutedVideo ? "ShowVideo" : "HideVideo" }}
                </button>
                <button type="button" class="btn btn-danger" @click="endCall">
                  EndCall
                </button>
              </div>
            </div>
          </div>
          <!-- End of Placing Video Call  -->
    
          <!-- Incoming Call  -->
          <div class="row" v-if="incomingCallDialog">
            <div class="col">
              <p>
                Incoming Call From <strong>{{ callerDetails.name }}</strong>
              </p>
              <div class="btn-group" role="group">
                <button
                  type="button"
                  class="btn btn-danger"
                  data-dismiss="modal"
                  @click="declineCall"
                >
                  Decline
                </button>
                <button
                  type="button"
                  class="btn btn-success ml-5"
                  @click="acceptCall"
                >
                  Accept
                </button>
              </div>
            </div>
          </div>
          <!-- End of Incoming Call  -->
        </div>
      </div>
    </template>
    
    <script>
    import Peer from "simple-peer";
    import { getPermissions } from "../helpers";
    export default {
      props: [
        "allusers",
        "authuserid",
        "turn_url",
        "turn_username",
        "turn_credential",
      ],
      data() {
        return {
          isFocusMyself: true,
          callPlaced: false,
          callPartner: null,
          mutedAudio: false,
          mutedVideo: false,
          videoCallParams: {
            users: [],
            stream: null,
            receivingCall: false,
            caller: null,
            callerSignal: null,
            callAccepted: false,
            channel: null,
            peer1: null,
            peer2: null,
          },
        };
      },
    
      mounted() {
        this.initializeChannel(); // this initializes laravel echo
        this.initializeCallListeners(); // subscribes to video presence channel and listens to video events
      },
      computed: {
        incomingCallDialog() {
          if (
            this.videoCallParams.receivingCall &&
            this.videoCallParams.caller !== this.authuserid
          ) {
            return true;
          }
          return false;
        },
    
        callerDetails() {
          if (
            this.videoCallParams.caller &&
            this.videoCallParams.caller !== this.authuserid
          ) {
            const incomingCaller = this.allusers.filter(
              (user) => user.id === this.videoCallParams.caller
            );
    
            return {
              id: this.videoCallParams.caller,
              name: `${incomingCaller[0].name}`,
            };
          }
          return null;
        },
      },
      methods: {
        initializeChannel() {
          this.videoCallParams.channel = window.Echo.join("presence-video-channel");
        },
    
        getMediaPermission() {
          return getPermissions()
            .then((stream) => {
              this.videoCallParams.stream = stream;
              if (this.$refs.userVideo) {
                this.$refs.userVideo.srcObject = stream;
              }
            })
            .catch((error) => {
              console.log(error);
            });
        },
    
        initializeCallListeners() {
          this.videoCallParams.channel.here((users) => {
            this.videoCallParams.users = users;
          });
    
          this.videoCallParams.channel.joining((user) => {
            // check user availability
            const joiningUserIndex = this.videoCallParams.users.findIndex(
              (data) => data.id === user.id
            );
            if (joiningUserIndex < 0) {
              this.videoCallParams.users.push(user);
            }
          });
    
          this.videoCallParams.channel.leaving((user) => {
            const leavingUserIndex = this.videoCallParams.users.findIndex(
              (data) => data.id === user.id
            );
            this.videoCallParams.users.splice(leavingUserIndex, 1);
          });
          // listen to incomming call
          this.videoCallParams.channel.listen("StartVideoChat", ({ data }) => {
            if (data.type === "incomingCall") {
              // add a new line to the sdp to take care of error
              const updatedSignal = {
                ...data.signalData,
                sdp: `${data.signalData.sdp}\n`,
              };
    
              this.videoCallParams.receivingCall = true;
              this.videoCallParams.caller = data.from;
              this.videoCallParams.callerSignal = updatedSignal;
            }
          });
        },
        async placeVideoCall(id, name) {
          this.callPlaced = true;
          this.callPartner = name;
          await this.getMediaPermission();
          this.videoCallParams.peer1 = new Peer({
            initiator: true,
            trickle: false,
            stream: this.videoCallParams.stream,
            config: {
              iceServers: [
                {
                  urls: this.turn_url,
                  username: this.turn_username,
                  credential: this.turn_credential,
                },
              ],
            },
          });
    
          this.videoCallParams.peer1.on("signal", (data) => {
            // send user call signal
            axios
              .post("/video/call-user", {
                user_to_call: id,
                signal_data: data,
                from: this.authuserid,
              })
              .then(() => {})
              .catch((error) => {
                console.log(error);
              });
          });
    
          this.videoCallParams.peer1.on("stream", (stream) => {
            console.log("call streaming");
            if (this.$refs.partnerVideo) {
              this.$refs.partnerVideo.srcObject = stream;
            }
          });
    
          this.videoCallParams.peer1.on("connect", () => {
            console.log("peer connected");
          });
    
          this.videoCallParams.peer1.on("error", (err) => {
            console.log(err);
          });
    
          this.videoCallParams.peer1.on("close", () => {
            console.log("call closed caller");
          });
    
          this.videoCallParams.channel.listen("StartVideoChat", ({ data }) => {
            if (data.type === "callAccepted") {
              if (data.signal.renegotiate) {
                console.log("renegotating");
              }
              if (data.signal.sdp) {
                this.videoCallParams.callAccepted = true;
                const updatedSignal = {
                  ...data.signal,
                  sdp: `${data.signal.sdp}\n`,
                };
                this.videoCallParams.peer1.signal(updatedSignal);
              }
            }
          });
        },
    
        async acceptCall() {
          this.callPlaced = true;
          this.videoCallParams.callAccepted = true;
          await this.getMediaPermission();
          this.videoCallParams.peer2 = new Peer({
            initiator: false,
            trickle: false,
            stream: this.videoCallParams.stream,
            config: {
              iceServers: [
                {
                  urls: this.turn_url,
                  username: this.turn_username,
                  credential: this.turn_credential,
                },
              ],
            },
          });
          this.videoCallParams.receivingCall = false;
          this.videoCallParams.peer2.on("signal", (data) => {
            axios
              .post("/video/accept-call", {
                signal: data,
                to: this.videoCallParams.caller,
              })
              .then(() => {})
              .catch((error) => {
                console.log(error);
              });
          });
    
          this.videoCallParams.peer2.on("stream", (stream) => {
            this.videoCallParams.callAccepted = true;
            this.$refs.partnerVideo.srcObject = stream;
          });
    
          this.videoCallParams.peer2.on("connect", () => {
            console.log("peer connected");
            this.videoCallParams.callAccepted = true;
          });
    
          this.videoCallParams.peer2.on("error", (err) => {
            console.log(err);
          });
    
          this.videoCallParams.peer2.on("close", () => {
            console.log("call closed accepter");
          });
    
          this.videoCallParams.peer2.signal(this.videoCallParams.callerSignal);
        },
        toggleCameraArea() {
          if (this.videoCallParams.callAccepted) {
            this.isFocusMyself = !this.isFocusMyself;
          }
        },
        getUserOnlineStatus(id) {
          const onlineUserIndex = this.videoCallParams.users.findIndex(
            (data) => data.id === id
          );
          if (onlineUserIndex < 0) {
            return "Offline";
          }
          return "Online";
        },
        declineCall() {
          this.videoCallParams.receivingCall = false;
        },
    
        toggleMuteAudio() {
          if (this.mutedAudio) {
            this.$refs.userVideo.srcObject.getAudioTracks()[0].enabled = true;
            this.mutedAudio = false;
          } else {
            this.$refs.userVideo.srcObject.getAudioTracks()[0].enabled = false;
            this.mutedAudio = true;
          }
        },
    
        toggleMuteVideo() {
          if (this.mutedVideo) {
            this.$refs.userVideo.srcObject.getVideoTracks()[0].enabled = true;
            this.mutedVideo = false;
          } else {
            this.$refs.userVideo.srcObject.getVideoTracks()[0].enabled = false;
            this.mutedVideo = true;
          }
        },
    
        stopStreamedVideo(videoElem) {
          const stream = videoElem.srcObject;
          const tracks = stream.getTracks();
          tracks.forEach((track) => {
            track.stop();
          });
          videoElem.srcObject = null;
        },
        endCall() {
          // if video or audio is muted, enable it so that the stopStreamedVideo method will work
          if (!this.mutedVideo) this.toggleMuteVideo();
          if (!this.mutedAudio) this.toggleMuteAudio();
          this.stopStreamedVideo(this.$refs.userVideo);
          if (this.authuserid === this.videoCallParams.caller) {
            this.videoCallParams.peer1.destroy();
          } else {
            this.videoCallParams.peer2.destroy();
          }
          this.videoCallParams.channel.pusher.channels.channels[
            "presence-presence-video-channel"
          ].disconnect();
    
          setTimeout(() => {
            this.callPlaced = false;
          }, 3000);
        },
      },
    };
    </script>
    
    <style scoped>
    #video-row {
      width: 700px;
      max-width: 90vw;
    }
    
    #incoming-call-card {
      border: 1px solid #0acf83;
    }
    
    .video-container {
      width: 700px;
      height: 500px;
      max-width: 90vw;
      max-height: 50vh;
      margin: 0 auto;
      border: 1px solid #0acf83;
      position: relative;
      box-shadow: 1px 1px 11px #9e9e9e;
      background-color: #fff;
    }
    
    .video-container .user-video {
      width: 30%;
      position: absolute;
      left: 10px;
      bottom: 10px;
      border: 1px solid #fff;
      border-radius: 6px;
      z-index: 2;
    }
    
    .video-container .partner-video {
      width: 100%;
      height: 100%;
      position: absolute;
      left: 0;
      right: 0;
      bottom: 0;
      top: 0;
      z-index: 1;
      margin: 0;
      padding: 0;
    }
    
    .video-container .action-btns {
      position: absolute;
      bottom: 20px;
      left: 50%;
      margin-left: -50px;
      z-index: 3;
      display: flex;
      flex-direction: row;
    }
    
    /* Mobiel Styles */
    @media only screen and (max-width: 768px) {
      .video-container {
        height: 50vh;
      }
    }
    </style>
    

    ビデオチャットコンポーネントの内訳。

  • インポートするPeer からsimple-peer WebRTCとの相互作用を容易にするパッケージです.
  • コンポーネントは以下の小道具を受け付けます:allusers : 現在登録されているユーザを除くすべての登録ユーザー.これらのユーザが表示されます.認証されたユーザーが自分自身を呼び出すことを許可したくない.authuserid : The id 認証されたユーザの.turn_url : あなたのターンサーバのURLはsimple-peer アイアイイーPeer .turn_username : ターンサーバーからのユーザ名.turn_credential : ユーザ名のパスワード.
  • コンポーネントがマウントされると、presence-video-channelinitializeChannel メソッド.使用するLaravel-echo そのために.
  • 我々initializeCallListeners 我々が予約したチャンネルで.が提供されるメソッドがありますLaravel-echo どのように多くのユーザーがチャネルに加入しているユーザーとチャネルを残してユーザーを知っている.私たちはまたStartVideoChat イベントオンザpresence-video-channel 着信のために.
  • データベースからのすべてのユーザーをリストしますallUsers プロップし、彼らがオンラインかどうかを示します.オンラインでは、彼らもpresence-video-channel . これは、あなたが置くどんなページにも影響を及ぼしますvideo-chat コンポーネント.このチュートリアルでは、コンポーネントを配置するビデオチャットページを持っています.
  • placeVideoCall は呼び出しを行うために使われる.私たちはid and name パラメータとして呼び出されるユーザ.
    我々は、ユーザーとマイクとカメラへのブラウザーへのアクセスを許可するよう頼むgetMediaPermission メソッド.ストリーミングデータはブラウザに表示されます.呼び出し元はブラウザで顔を見ている.
    呼び出し元のピアを作成します.シグナルイベントがある場合peer.on('signal',..) 我々は、信号データを送る/video/call-user エンドポイント我々のバックエンド.
    受取人は着信呼び出し通知を受け取ります、そして、彼らが呼び出しを受け入れるとき、我々は呼び出し元で呼び出し元に信号を送りますanswer シグナル
    The peer.on('stream',...) リスナーはブラウザの受信者部分に表示されるストリーミングデータを受け取ります.
  • acceptCall メソッドは、着信コールを受け入れるために使用されます.ユーザーが着信通知を見ると、Acceptボタンをクリックします.受信機から受信した信号データを受信する.
    我々は、カメラやマイクにアクセスし、私たちのUI上のストリーミングデータを表示する許可を取得します.
    これはPeerinitiator プロパティセットfalse 新しいピアが受信機であることを示す.
    我々はAccept -呼び出し終点を打って、我々の合図データ(答え)を発信者に送ります.
  • ストリーミングが開始されると、我々は同様にブラウザの呼び出し元のストリームを表示し、現在のコミュニケーションは我々のバックエンドを押すことなく、pusherによって供給されたWebSocketを通して続けます.
  • 残りの機能は、オーディオをミュート、ビデオストリームを無効にし、呼び出しを終了するために使用されます.
  • 登録するVideoChat.vue コンポーネントresources/js/app.js
  • Vue.component("video-chat", require("./components/VideoChat.vue").default);
    
  • ビデオチャットを作成resources/views/video-chat.blade.php
  • @extends('layouts.app')
    
    @section('content')
        <video-chat :allusers="{{ $users }}" :authUserId="{{ auth()->id() }}" turn_url="{{ env('TURN_SERVER_URL') }}"
            turn_username="{{ env('TURN_SERVER_USERNAME') }}" turn_credential="{{ env('TURN_SERVER_CREDENTIAL') }}" />
    @endsection
    
  • 変数env変数.あなたのプッシャーAPIキーを挿入します
  • BROADCAST_DRIVER=pusher
    
    PUSHER_APP_ID=
    PUSHER_APP_KEY=
    PUSHER_APP_SECRET=
    PUSHER_APP_CLUSTER=
    
    TURN_SERVER_URL=
    TURN_SERVER_USERNAME=
    TURN_SERVER_CREDENTIAL=
    

    クレジット


    私は、私がここですべて共有することができない有益な多くの資源を見つけました、しかし、以下のYoutubeビデオは私の理解において助けられて、この実装に到着しました.

  • We Code
  • この記事でフォローすることがいかに簡単かについてあなたの考えを聞きたいです.

    更新


    私はちょうどwebrtcとライブストリーミング実装を発表した.ここでチェックアウトしてください.