Skip to content

RESOURCES / BLOG

Creating a live stream on WebRTC using NuxtJS

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.

Stream control UI

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.

Watch stream UI

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:

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free