Introduction
My first attempt at WebRTC was to implement a video call feature within a Laravel Application. The implementation involved placing a call, showing an incoming call notification, and the ability of the receiver to accept the call. I wrote about it over here:
Adding Video Chat To Your Laravel App
Kofi Mupati ・ Nov 12 '20 ・ 11 min read
One of my readers asked whether it was possible to build a live streaming application with WebRTC in a Laravel Application. I took up this challenge and even though WebRTC has limitations, I came up with a simple live streaming implementation.
We'll go through my implementation in this article.
Final Project Repository: https://github.com/Mupati/laravel-video-chat Note that this repository contains code for some other technical articles.
Requirements
This tutorial assumes you know how to set up a new
Laravel
project withVueJs
authentication. Create some users after setting up your project. You should be familiar with Laravel's broadcasting mechanism and have a fair idea of how WebSockets work. You may use this starter project I created: Laravel 8 Vue Auth StarterSet up a free pusher account on pusher.com
Set up your ICE SERVER (TURN SERVER) details. This tutorial is a good guide. HOW TO INSTALL COTURN.
Project Setup
# Install needed packages
composer require pusher/pusher-PHP-server "~4.0"
npm install --save laravel-echo pusher-js simple-peer
Configuring Backend
- Add routes for streaming pages in
routes/web.php
. The routes will be used to visit the live stream page, start a live stream from the device camera, and generate a broadcast link for other authenticated users to view your live stream.
Route::get('/streaming', 'App\Http\Controllers\WebrtcStreamingController@index');
Route::get('/streaming/{streamId}', 'App\Http\Controllers\WebrtcStreamingController@consumer');
Route::post('/stream-offer', 'App\Http\Controllers\WebrtcStreamingController@makeStreamOffer');
Route::post('/stream-answer', 'App\Http\Controllers\WebrtcStreamingController@makeStreamAnswer');
- Uncomment
BroadcastServiceProvider
inconfig/app.php
. This allows us to use Laravel's broadcasting system.
+ App\Providers\BroadcastServiceProvider::class
- //App\Providers\BroadcastServiceProvider::class
- Create Dynamic Presence and Private Channel in routes/channels.php.
Authenticated users subscribe to both channels.
The presence channel is dynamically created with a streamId
generated by the broadcaster. This way, we can detect all users who have joined the live broadcast.
Signaling information is exchanged between the broadcaster and the viewer through the private channel.
// Dynamic Presence Channel for Streaming
Broadcast::channel('streaming-channel.{streamId}', function ($user) {
return ['id' => $user->id, 'name' => $user->name];
});
// Signaling Offer and Answer Channels
Broadcast::channel('stream-signal-channel.{userId}', function ($user, $userId) {
return (int) $user->id === (int) $userId;
});
- Create
StreamOffer
andStreamAnswer
events. Signaling information is broadcast on thestream-signal-channel-{userId}
private channel we created early on.
The broadcaster sends an offer to a new user who joins the live stream when we emit the StreamOffer
event and the viewer replies with an answer using the StreamAnswer
event.
php artisan make:event StreamOffer
php artisan make:event StreamAnswer
- Add the following code to
app/Events/StreamOffer.php
.
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class StreamOffer 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()
{
// stream offer can broadcast on a private channel
return new PrivateChannel('stream-signal-channel.' . $this->data['receiver']['id']);
}
}
- Add the following code to
app/Events/StreamAnswer.php
.
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class StreamAnswer 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 PrivateChannel('stream-signal-channel.' . $this->data['broadcaster']);
}
}
- Create
WebrtcStreamingController
to handle the broadcasting, viewing, and signaling for the live stream.
php artisan make:controller WebrtcStreamingController
- Add the following to the
WebrtcStreamingController
<?php
namespace App\Http\Controllers;
use App\Events\StreamAnswer;
use App\Events\StreamOffer;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class WebrtcStreamingController extends Controller
{
public function index()
{
return view('video-broadcast', ['type' => 'broadcaster', 'id' => Auth::id()]);
}
public function consumer(Request $request, $streamId)
{
return view('video-broadcast', ['type' => 'consumer', 'streamId' => $streamId, 'id' => Auth::id()]);
}
public function makeStreamOffer(Request $request)
{
$data['broadcaster'] = $request->broadcaster;
$data['receiver'] = $request->receiver;
$data['offer'] = $request->offer;
event(new StreamOffer($data));
}
public function makeStreamAnswer(Request $request)
{
$data['broadcaster'] = $request->broadcaster;
$data['answer'] = $request->answer;
event(new StreamAnswer($data));
}
}
Methods in the WebrtcStreamingController
Let's explore what the methods in the controller are doing.
index
: This returns the view for the broadcaster. We pass a 'type': broadcaster and the user's ID into the view to help identify who the user is.consumer
: It returns the view for a new user who wants to join the live stream. We pass a 'type': consumer, the 'streamId' we extract from the broadcasting link, and the user's ID.makeStreamOffer
: It broadcasts an offer signal sent by the broadcaster to a specific user who just joined. The following data is sent:broadcaster
: The user ID of the one who initiated the live stream i.e the broadcasterreceiver
: The ID of the user to whom the signaling offer is being sent.offer
: This is the WebRTC offer from the broadcaster.
makeStreamAnswer
: It sends an answer signal to the broadcaster to fully establish the peer connection.broadcaster
: The user ID of the one who initiated the live stream i.e the broadcaster.answer
: This is the WebRTC answer from the viewer after, sent after receiving an offer from the broadcaster.
Configuring Frontend
- Instantiate
Laravel Echo
andPusher
inresources/js/bootstrap.js
by uncommenting the following block of code.
+ 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
-});
- Create
resources/js/helpers.js
. Add agetPermissions
function to help with permission access for the microphone and camera. This method handles the video and audio permission that is required by browsers to make the video calls. It waits for the user to accept the permissions before we can proceed with the video call. We allow both audio and video. Read more on 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 media devices. 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}`);
});
});
};
Create a component for the Broadcaster named Broadcaster.vue in
resources/js/components/Broadcaster.vue
.Create a component for the Viewer named Viewer.vue in
resources/js/components/Viewer.vue
.
Explanation of the Broadcaster and Viewer components.
The following video explains the call logic on the client-side from the perspective of both Broadcaster and Viewers.
- Register the
Broadcaster.vue
andViewer.vue
components inresources/js/app.js
// Streaming Components
Vue.component("broadcaster", require("./components/Broadcaster.vue").default);
Vue.component("viewer", require("./components/Viewer.vue").default);
Create the video broadcast view in
resources/views/video-broadcast.blade.php
Update env variables. Insert your Pusher API keys
APP_ENV=
BROADCAST_DRIVER=pusher
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=
TURN_SERVER_URL=
TURN_SERVER_USERNAME=
TURN_SERVER_CREDENTIAL=
Live Stream Demo
Final Thoughts
The logic for this live streaming application can be likened to a group video call where only one person's stream is seen.
The stream of the broadcaster is rendered on the viewers' browser but the broadcaster doesn't receive anything from the viewers after exchanging the signaling information which is required in WebRTC.
This looks like a star topology and there is a limitation on how many peers can be connected to a single user.
I want to explore the option of turning some of the viewers into broadcasters after the initial broadcaster's peer has connected to about 4 users.
The goal is to rebroadcast the stream they received from the original broadcaster.
Is it possible? I can't tell. This will be an interesting challenge to explore.
Stay tuned!!!.
Link : https://dev.to/mupati/live-stream-with-webrtc-in-your-laravel-application-2kl3