あなたのLaravelアプリにビデオチャットを追加する
導入
私はVuejsとLaravelプロジェクトのカスタムビデオチャットアプリケーションを構築する必要がありました.私は、それを作動させるために、たくさんのフープを経験しました.私は、私がここの上でプロセスを通して学んだすべてを共有します.
最終プロジェクトリポジトリ https://github.com/Mupati/laravel-video-chat
要件
Laravel
プロジェクトVueJs
認証.プロジェクトの設定後にユーザーを作成します.あなたはLaravelの放送メカニズムに精通していなければならなくて、WebSocketsがどのように働くかについての公正な考えを持っていなければなりません.プロジェクト設定
# Install needed packages
composer require pusher/pusher-php-server "~4.0"
npm install --save laravel-echo pusher-js simple-peer
バックエンドの設定
# 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-channel
とinitializeChannel
メソッド.使用する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上のストリーミングデータを表示する許可を取得します.
これは
Peer
とinitiator
プロパティセットfalse
新しいピアが受信機であることを示す.我々はAccept -呼び出し終点を打って、我々の合図データ(答え)を発信者に送ります.
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
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ビデオは私の理解において助けられて、この実装に到着しました.
更新
私はちょうどwebrtcとライブストリーミング実装を発表した.ここでチェックアウトしてください.
あなたのLaravelアプリケーションでWebRTCとライブストリーム
コフィムパチ・ Mar 23 ' 21・ 6分読む
#webrtc
#livestreaming
#laravel
#vue
Reference
この問題について(あなたのLaravelアプリにビデオチャットを追加する), 我々は、より多くの情報をここで見つけました https://dev.to/mupati/adding-video-chat-to-your-laravel-app-5ak7テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol