DEV Community

Cover image for Live stream with WebRTC in your Laravel application
Kofi Mupati
Kofi Mupati

Posted on

Live stream with WebRTC in your Laravel application

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:

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 with VueJs 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 Starter

  • Set 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
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode
  • Uncomment BroadcastServiceProvider in config/app.php. This allows us to use Laravel's broadcasting system.
+ App\Providers\BroadcastServiceProvider::class
- //App\Providers\BroadcastServiceProvider::class 
Enter fullscreen mode Exit fullscreen mode
  • 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;
});

Enter fullscreen mode Exit fullscreen mode
  • Create StreamOffer and StreamAnswer events. Signaling information is broadcast on the stream-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
Enter fullscreen mode Exit fullscreen mode
  • 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']);
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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']);
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Create WebrtcStreamingController to handle the broadcasting, viewing, and signaling for the live stream.
php artisan make:controller WebrtcStreamingController
Enter fullscreen mode Exit fullscreen mode
  • 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));
    }
}

Enter fullscreen mode Exit fullscreen mode

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 broadcaster
    • receiver: 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 and Pusher in resources/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
-});
Enter fullscreen mode Exit fullscreen mode
  • Create resources/js/helpers.js. Add a getPermissions 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}`);
            });
    });
};
Enter fullscreen mode Exit fullscreen mode
  • 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 and Viewer.vue components in resources/js/app.js
//  Streaming Components
Vue.component("broadcaster", require("./components/Broadcaster.vue").default);
Vue.component("viewer", require("./components/Viewer.vue").default);
Enter fullscreen mode Exit fullscreen mode
  • 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=
Enter fullscreen mode Exit fullscreen mode

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!!!.

Top comments (37)

Collapse
 
sayamk3004 profile image
sayamk3004

Well I have successfully connected, and it worked too. I have one question? Do we have any peer connection limitations? Cant this this be used as a live stream which can have millions of viewers watching it together?

Collapse
 
mupati profile image
Kofi Mupati

You'll need to modify the code. The limiting factor is the number of users that can be accommodated on a presence channel.

By the way how many users were able to connect to it after you got it to work?

Collapse
 
shehranahmad profile image
ShehranAhmad

Can you explain what did you changed in code because mine join stream button not work??

Collapse
 
mupati profile image
Kofi Mupati

do you have any error messages in the browser console?

Thread Thread
 
shehranahmad profile image
ShehranAhmad

No it's not showing any error or message in console.
I think this method is not hitting in Viewer.vue component

window.Echo.private(stream-signal-channel.${this.auth_user_id}).listen(
"StreamOffer",
({ data }) => {
console.log("Signal Offer from private channel");
this.broadcasterId = data.broadcaster;
this.createViewerPeer(data.offer, data.broadcaster);
}
);

Thread Thread
 
mupati profile image
Kofi Mupati

okay. are you connecting on the same local network?

Thread Thread
 
shehranahmad profile image
ShehranAhmad

Yes I am connecting on same local network but join stream button not working

Collapse
 
sayamk3004 profile image
sayamk3004

I have tried everything discussed above, but im not being able to get the peer connection by turn server. Is there any other way I can connect my peer ? My project currently turns on the camera for the broadcaster with a streaming link, but when the viewer hits join stream. Nothing happens!

Collapse
 
mupati profile image
Kofi Mupati

Do you have any errors in your browser console?

Collapse
 
sayamk3004 profile image
sayamk3004

No. Its just nothing happens after the viewer clicks... When I console log I see
like this
Ps. The broadcaster video starts, and I also have the sharing link... But when the viewers gets into the link and tries to join this happens^

Thread Thread
 
mupati profile image
Kofi Mupati • Edited

Were you able to set up the TURN/STUN server? If not, comment out the following code from the peer instance and test again. In this case, both the broadcaster and viewer should be on the same network.

The code to comment out:

        config: {
          iceServers: [
            {
              urls: "stun:stun.stunprotocol.org",
            },
            {
              urls: this.turn_url,
              username: this.turn_username,
              credential: this.turn_credential,
            },
          ],
        },
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
sayamk3004 profile image
sayamk3004

i did commented out and tried, is there any other way to get the peer connection apart from turn/stun server? It would be really nice if you can use my teamviewer for some time please?

Collapse
 
maczad profile image
maczad

I need a live-streaming with live chat included for my content support website, like a live chat where livestreamers can receive gifts from users, like it is done on BIGO and TikTok. Is this achievable?

Collapse
 
mupati profile image
Kofi Mupati

Yeah. That is possible.

Collapse
 
dariochiappello profile image
Darío Chiappello • Edited

hi, is it possible to run the project without a turn server? for example with xampp or wampserver

Collapse
 
mupati profile image
Kofi Mupati

The TURN/STUN server is not needed to run your webserver. It is used during the signaling process between 2 peers. Run the application like any laravel application on your xampp or wampserver.

Comment out the following code. It will work if both devices are on the same network:

        config: {
          iceServers: [
            {
              urls: "stun:stun.stunprotocol.org",
            },
            {
              urls: this.turn_url,
              username: this.turn_username,
              credential: this.turn_credential,
            },
          ],
        },
Enter fullscreen mode Exit fullscreen mode
Collapse
 
alinajmuddin98 profile image
Muhammad Ali Najmuddin

Ive commented this out from bradcast, viewer, and app js. Still not working. I use php artisan serve. Do i need to use xampp?

Collapse
 
alinajmuddin98 profile image
Muhammad Ali Najmuddin

Hi Dario, if I may ask, did you commented all of the occurance of config details for turn server?

Ive commented all of it in broadcast, viewer and app js file. Still the join stream wont work.

Collapse
 
dariochiappello profile image
Darío Chiappello

Hello Muhammad. I commented on the config details and the camera and sound of the sender can be activated but the image is not reaching the receiver

Collapse
 
rakeshmaity271 profile image
honeycrisp

Join stream button not working bro.

Collapse
 
mupati profile image
Kofi Mupati

What's the error in your browser console?

Collapse
 
richajaiswal11 profile image
RICHA JAISWAL

Not getting any issue on console.

Collapse
 
hasansarm profile image
hasan-sarm

how i can save video after stream ?

Collapse
 
mupati profile image
Kofi Mupati

I didn't handle that but you can explore the MediaRecorder API developer.mozilla.org/en-US/docs/W...

Such solutions exist so you can do some googling too.

Collapse
 
crazeetee profile image
Timothy Kimemia

can someone guide how they reduced or removed the echo from the stream

Collapse
 
richajaiswal11 profile image
RICHA JAISWAL

I have tried everything discussed above, and also comment iceServers code on both vue file but join stream button is not working and not getting any issue in my console.

Collapse
 
mupati profile image
Kofi Mupati

As in the Livestream works but it's very slow at the viewer's end? If that is the case you should note that there are optimizations to be considered if you want to implement this at scale.

Collapse
 
sanjaikumar profile image
sanjai-kumar

dev-to-uploads.s3.amazonaws.com/up...

After Logged In I Get this Empty page.