The standard web streaming platforms such as Zoom and Google Meets are good platforms but there are many cases we want to have control over the user experience our event hosts and attendees have. Let us see how we can create our own live stream using WebRTC.
The completed project is available on Codesandbox.
You can find the full codebase on my Github
To build this project, we will use NuxtJS, a convenient and powerful VueJS framework. Ensure you have Yarn or NPM v5.2+/v6.1+ installed. Open your terminal in your preferred directory and run the following command:
yarn create nuxt-app nuxtjs-web-streaming
# OR
npx create-nuxt-app nuxtjs-web-streaming
# OR
npm init nuxt-app nuxtjs-web-streaming
Code language: PHP (php)
On running the above command, here are our preferences for the setup questions to follow:
Project name: nuxtjs-web-streaming Programming language: JavaScript Package manager: Yarn UI framework: Tailwind CSS Nuxt.js modules: N/A Linting tools: N/A Testing frameworks: None Rendering mode: Universal (SSR/SSG) Deployment target: Server (Node.js hosting) Development tools: N/A What is your Github username?
<your-github-username>
Version control system: Git
When the setup is complete, enter and run your project.
cd nuxtjs-web-streaming
yarn dev
# OR
npm run dev
Code language: PHP (php)
You can now access your project on http://localhost:3000
Cloudinary is a powerful media management platform with a comprehensive set of APIs and SDKs. We are going to use its WebRTC live streaming functionality. If you do not have an account, feel free to create one here.
We are now going to install the cloudinary-js-streaming package together with the Cloudinary video player to allow watching of the live stream. Let us include the CDN hosted package files.
// nuxt.config.js
export default {
...
link: [
...
{ rel: 'stylesheet', href: 'https://unpkg.com/cloudinary-video-player@1.5.9/dist/cld-video-player.min.css' },
],
script: [
{ src: 'https://unpkg.com/cloudinary-core@latest/cloudinary-core-shrinkwrap.min.js' },
{ src: 'https://unpkg.com/cloudinary-video-player@1.5.9/dist/cld-video-player.min.js' },
{ src: 'https://unpkg.com/@cloudinary/js-streaming/dist/js-streaming.js' },
],
},
...
}
Code language: JavaScript (javascript)
Create new live streaming upload preset. Upload presets are a set of pre-defined rules to be applied to media upload. Use this link to create the upload preset. Here are our recommended settings:
Name: streaming_upload_preset Unique filename: true Live broadcast: true Delivery type: upload Access mode: public Auto tagging: 0.7
We now need to configure our environmental variables. These are values we want to configure outside of our codebase for various reasons. Create your env
file.
touch .env
Code language: CSS (css)
We are now going to add our Cloudinary cloud name and our streaming upload preset.
<!-- .env -->
NUXT_ENV_CLOUDINARY_CLOUD_NAME=<your-cloudinary-cloud-name>
NUXT_ENV_CLOUDINARY_STREAMING_UPLOAD_PRESET=streaming_upload_preset
Code language: HTML, XML (xml)
To initialize the streaming in our app, we use the initLiveStream
function. We will pass various configurations to the function including our event listeners. If none of the env
variables are set, we do not initialize the stream.
Before initializing, we additionally find and set the video element that will be used to display the stream.
// pages/index.vue
<template>
<div>
...
<video id="video"></video>
...
</div>
</template>
<script>
export default {
data(){
return {
cloudinaryJsStreaming:null,
videoElement: null,
initializing:false,
liveStream: false,
...
};
},
...
methods:{
async initializeStream(){
this.initializing = true;
if(
!process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME ||
!process.env.NUXT_ENV_CLOUDINARY_STREAMING_UPLOAD_PRESET
){
this.initializing = false;
return;
}
this.videoElement = document.getElementById("video");
this.cloudinaryJsStreaming = cloudinaryJsStreaming;
this.liveStream = await this.cloudinaryJsStreaming.initLiveStream({
cloudName: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME,
uploadPreset: process.env.NUXT_ENV_CLOUDINARY_STREAMING_UPLOAD_PRESET,
debug: "all",
hlsTarget: true,
fileTarget: true,
events: {
start: function (args) {
console.log("start",args);
},
stop: function (args) {
console.log("stop",args);
},
error: function(error){
console.log("error",error);
},
local_stream: stream => this.attachStream(stream)
}
});
this.initializing = false;
},
...
}
}
</script>
Code language: HTML, XML (xml)
As visible below, when local streaming starts, we call the attachStream
method. This method attaches the stream to the video element and plays the video as well as registers to the state that the stream is playing and the video is showing.
<script>
export default {
...
methods:{
...
attachStream(stream) {
this.liveStream.attach(this.videoElement, stream);
this.videoElement.play();
this.streaming = true;
this.videoShowing = true;
},
...
}
...
}
</script>
Code language: HTML, XML (xml)
Before streaming starts, we want to allow our users to start the video, confirm that the input is correct, and stop the video at will. To do this, we add startVideo
and stopVideo
methods which utilize the attachCamera
and detachCamera
functions in the Cloudinary streaming package.
<script>
export default {
data(){
return {
...
videoShowing:false,
...
}
},
...
methods:{
...
startVideo(){
this.cloudinaryJsStreaming
.attachCamera(this.videoElement)
.then(() => {
this.videoElement.play();
this.videoShowing = true;
});
},
stopVideo(){
this.cloudinaryJsStreaming
.detachCamera(this.videoElement,false)
.then(() => this.videoShowing = false);
},
}
}
</script>
Code language: HTML, XML (xml)
To start the stream, we simply call the start
function passing the live stream public_id. To stop streaming, we call the stop
function and update the state.
// pages/index.vue
<script>
export default {
data(){
return {
...
streaming:false,
...
}
},
...
methods:{
...
startStream(){
this.liveStream.start(this.liveStream.response.public_id);
},
stopStream(){
this.liveStream.stop();
this.streaming = false;
},
...
}
}
</script>
Code language: HTML, XML (xml)
We added the video element that will display the camera input while streaming, however, this is not adequate. We need to display the buttons to allow the user to toggle the video as well as the streaming state. Let us set this up.
<template>
<div>
<video id="video"></video>
<div v-if="liveStream">
<button
v-if="!videoShowing"
@click="startVideo"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2 6a2 2 0 012-2h6a2 2 0 012 2v8a2 2 0 01-2 2H4a2 2 0 01-2-2V6zM14.553 7.106A1 1 0 0014 8v4a1 1 0 00.553.894l2 1A1 1 0 0018 13V7a1 1 0 00-1.447-.894l-2 1z" />
</svg>
</button>
<button
v-else
@click="stopVideo"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
<div>
<button
v-if="!liveStream && !initializing"
@click="initializeStream"
>
Initialize Stream
</button>
<button
v-if="liveStream && !streaming"
@click="startStream"
>
Start Streaming
</button>
<button
v-else-if="liveStream"
@click="stopStream"
>
Stop Streaming
</button>
</div>
</div>
...
<table>
<tbody>
<tr>
<td>Video</td>
<td>{{ videoShowing ? "On" : "Off" }}</td>
</tr>
<tr>
<td>Status</td>
<td>{{
liveStream
? (streaming ? "Streaming" : "Not streaming")
: (initializing ? "Initializing" : "Not initialized")
}}</td>
</tr>
<tr>
<td>Public ID</td>
<td>{{ liveStream ? liveStream.response.public_id : ""}}</td>
</tr>
<tr>
<td>Watch</td>
<td>
<a v-if="liveStream" target="_blank" :href="`/watch/${liveStream.response.public_id}`">
Watch stream online
<svg class="inline-block h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</td>
</tr>
<tr>
<td>Stream URL</td>
<td>{{ liveStream ? streamURL : ""}}</td>
</tr>
<tr>
<td>File URL</td>
<td>{{ liveStream ? fileURL : ""}}</td>
</tr>
</tbody>
</table>
...
</div>
</template>
<script>
export default {
...
computed:{
fileURL(){
return this.liveStream
? 'https://res.cloudinary.com/' + process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME + '/video/upload/' + this.liveStream.response.public_id + '.mp4'
: null;
},
streamURL(){
return this.liveStream
? 'https://res.cloudinary.com/'+ process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME + '/video/upload/' + this.liveStream.response.public_id + '.m3u8'
: null;
},
},
...
}
</script>
Code language: HTML, XML (xml)
The above configuration will result in the following user interface.
In the above UI we display a button allowing our users to watch the stream. Let us create a page to respond to these requests. The public_id
is the URL slug the \watch route receives.
touch pages/watch/_public_id.vue
Once we receive the public_id
in the URL params, we add it to the page state. We then initialize the video player configured with the hls
sourceType and the m3u8
format. We will also configure the underlying Video.Js instance to support the live stream. We attach the video player to an HTML video
element.
<template>
<div>
<video
id="video-player"
controls
muted
class="cld-video-player cld-video-player-skin-dark w-2/3 h-96 mx-auto"
>
</video>
<p class="text-muted text-center text-sm">
Placeholder by Max Andrey from Pexels
</p>
</div>
</template>
<script>
export default {
async asyncData({ params }) {
return {
publicId: params.public_id
};
},
data(){
return {
cld:null,
player:null,
};
},
mounted(){
this.cld = cloudinary.Cloudinary.new({ cloud_name: process.env.NUXT_ENV_CLOUDINARY_CLOUD_NAME });
this.player = this.cld.videoPlayer('video-player');
this.player.source(
{
publicId: this.publicId,
sourceTypes: ['hls'],
format: 'm3u8',
},
{
fluid: true,
videojs: {
html5: {
hls: {overrideNative: true},
nativeAudioTracks: false,
nativeVideoTracks: false
},
loadingSpinner: false,
},
analytics: {
events: ['play', 'pause', 'ended', {type: 'percentsplayed', percents: [10, 40, 70, 90]}, 'error']
},
posterOptions: {
publicId: 'nuxtjs-webrtc-streaming/poster'
}
}
);
},
}
</script>
Code language: HTML, XML (xml)
With the above code, our users should see the following user interface when they decide to watch a stream.
Once the stream is available, it will be displayed to the user.
We are now able to allow our users to create a live stream and watch this stream. Feel free to review the following documentation to learn more: