Build a real-time video chat app with Vue and Daily Prebuilt in under ten minutes
Jess Mitchell
Posted on August 30, 2021
At Daily, we’ve spent a lot of time making sure our video and audio-only APIs can be used with any frontend framework, or no framework at all. 🍦 It’s important to us to make flexible APIs that can be incorporated into any app looking to add audio and video chat. We’ve created several demos for our customers using plain JavaScript, React, Next.js, React Native, and more to help cover as many use cases as possible.
Recently, we decided to expand our demo coverage even more with one of our favourite frameworks: Vue!
In today’s tutorial, we’ll cover how to incorporate Daily Prebuilt into your Vue app, as well as how to programmatically manage Daily Prebuilt controls via your app’s UI with our latest demo app.
If you are interested in building a custom video chat app with Vue, fear not; we have an upcoming tutorial series on how to do just that. Stay tuned! 👀
Tutorial requirements
Before we get started, be sure to sign up for a Daily account. Once you’re logged in, you can either create a room through the Dashboard or through the REST API.
For this tutorial, you can clone the Daily Prebuilt Vue demo repo and run it locally, or start from scratch and follow along as we build our Vue components.
To run the Daily Prebuilt Vue demo app locally, clone it and run the following in your terminal:
npm install
npm run serve
To view the app, open http://localhost:8080
in the browser of your choice.
Create a new Vue app
If you prefer to create your own Vue app to add Daily Prebuilt to, start by installing the Vue CLI globally to your machine.
npm install -g @vue/cli
Once installed, we can create a new Vue app to add Daily Prebuilt to using the Vue CLI.
In your terminal, run:
vue create daily-prebuilt-demo
Once the project is created, go to the project’s root directory and add daily-js
as a dependency.
npm install @daily-co/daily-js
Then, following the same instructions as above for the demo app, start the server:
npm run serve
Demo project overview
The Daily Prebuilt Vue demo only has four components:
-
App.vue
, the parent component for every other component included in the app. -
Header.vue
, a completely optional component we included for the app’s title and project links. -
Home.vue
, the main component which is where Daily Prebuilt is embedded and the control panel is added when in a Daily call. -
Controls.vue
, the control panel for programmatically controlling Daily Prebuilt. This is also optional but useful for understanding how to interact withdaily-js
to customize your app’s usage of Daily Prebuilt.
We won’t go into the details of what’s happening in the Header
since it’s static content, but what’s important to know is that the App
component imports the Header
and Home
component, and both are displayed at all times.
<template>
<Header />
<Home />
</template>
<script>
import Home from "./components/Home.vue";
import Header from "./components/Header.vue";
export default {
name: "App",
components: {
Home,
Header,
},
};
</script>
Ready to go Home: Importing Daily Prebuilt to your Vue app
The Home
component is the most important one in this demo because it loads all the main content, including the Daily call and control panel.
The default view of the Home component will include two buttons and an input:
- The first button is only used if you’ve deployed the app via Netlify, so we’ll skip that for now. (Check out the project’s README for more information.)
- The input and second button are used to submit the Daily room URL you’ll be joining (i.e. from the Daily room created above). The format of this URL is
https://YOUR_DAILY_DOMAIN.daily.co/ROOM_NAME
.
The container for this default home view is conditionally rendered depending on the status
value in the component’s data option.
<div class="home" v-if="status === 'home'">
…
</div>
The status can be home
, lobby
, or call
. home
refers to the default view, before a call has been started, and lobby
refers to when a call has been started but not joined yet. (We call this the “hair check” view sometimes too, so you can view yourself and set up your devices before joining a call.) Lastly, call
refers to when you are live in a Daily call. We’ll look at how the status
value gets updated in a bit.
There is also a call container div
that is included in the Home
component, which is conditionally displayed depending on the app’s current status. This means it is in the DOM in the default view but only visible to the user once a call has been started.
Let’s look at the Vue template for how this is set up:
<template>
<main class="wrapper">
<div class="home" v-if="status === 'home'">
<h2>Daily Prebuilt demo</h2>
<p>Start demo with a new unique room or paste in your own room URL</p>
<div class="start-call-container">
<button @click="createAndJoinRoom" :disabled="runningLocally">
Create room and start
</button>
<p v-if="roomError" class="error">Room could not be created</p>
<p class="subtext">or</p>
<!-- Daily room URL is entered here -->
<input
type="text"
placeholder="Enter room URL..."
v-model="roomUrl"
pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
@input="validateInput"
/>
<!-- button to submit URL and join call -->
<button @click="submitJoinRoom" :disabled="!validRoomURL">
Join room
</button>
</div>
</div>
<div class="call-container" :class="{ hidden: status === 'home' }">
<!-- The Daily Prebuilt iframe is embedded in the div below using the ref -->
<div id="call" ref="callRef"></div>
<!-- Only show the control panel if a call is live -->
<controls
v-if="status === 'call'"
:roomUrl="roomUrl"
:callFrame="callFrame"
/>
</div>
</main>
</template>
Now that we know how the Home
component is structured, let’s look at the JavaScript code that gives it functionality:
<script>
import DailyIframe from "@daily-co/daily-js";
import Controls from "./Controls.vue";
import api from "../api.js";
export default {
components: { Controls },
name: "Home",
data() {
return {
roomUrl: "",
status: "home",
callFrame: null,
validRoomURL: false,
roomError: false,
runningLocally: false,
};
},
created() {
if (window?.location?.origin.includes("localhost")) {
this.runningLocally = true;
}
},
methods: {
createAndJoinRoom() {
api
.createRoom()
.then((room) => {
this.roomUrl = room.url;
this.joinRoom(room.url);
})
.catch((e) => {
console.log(e);
this.roomError = true;
});
},
// Daily callframe created and joined below
joinRoom(url) {
if (this.callFrame) {
this.callFrame.destroy();
}
// Daily event callbacks
const logEvent = (ev) => console.log(ev);
const goToLobby = () => (this.status = "lobby");
const goToCall = () => (this.status = "call");
const leaveCall = () => {
if (this.callFrame) {
this.status = "home";
this.callFrame.destroy();
}
};
// DailyIframe container element
const callWrapper = this.$refs.callRef;
// Create Daily call
const callFrame = DailyIframe.createFrame(callWrapper, {
iframeStyle: {
height: "auto",
width: "100%",
aspectRatio: 16 / 9,
minWidth: "400px",
maxWidth: "920px",
border: "1px solid var(--grey)",
borderRadius: "4px",
},
showLeaveButton: true,
});
this.callFrame = callFrame;
// Add event listeners and join call
callFrame
.on("loaded", logEvent)
.on("started-camera", logEvent)
.on("camera-error", logEvent)
.on("joining-meeting", goToLobby)
.on("joined-meeting", goToCall)
.on("left-meeting", leaveCall);
callFrame.join({ url });
},
submitJoinRoom() {
this.joinRoom(this.roomUrl);
},
validateInput(e) {
this.validRoomURL = !!this.roomUrl && e.target.checkValidity();
},
},
};
</script>
Let’s start by focusing on the joinRoom
method, which is where all the Daily video call ✨magic✨happens.
joinRoom(url) {
if (this.callFrame) {
this.callFrame.destroy();
}
...
First, if there is already a callFrame
(i.e. the video call iframe), we destroy it to avoid multiple calls being loaded unintentionally. Defensive coding FTW. 💅
// Daily event callbacks
const logEvent = (ev) => console.log(ev);
const goToLobby = () => (this.status = "lobby");
const goToCall = () => (this.status = "call");
const leaveCall = () => {
if (this.callFrame) {
this.status = "home";
this.callFrame.destroy();
}
};
Next, we set up the callbacks that will be used by daily-js
whenever an event happens in the call that will affect our app’s UI. This can be moved outside the joinRoom
function too, but we won’t worry about optimizing for now.
These callbacks are where we update our data options’ status
value to know what stage of the call we’re in.
const callWrapper = this.$refs.callRef;
Next, we select the div
container that we’ll instruct daily-js
to embed the video call iframe into (the DailyIframe
instance).
<div id="call" ref="callRef"></div>
If we look back at the DOM structure, there was a div
included with a ref added to it to simplify selecting that div
in our joinRoom
method. This is what we're targeting with const callWrapper = this.$refs.callRef;
// Create Daily call
const callFrame = DailyIframe.createFrame(callWrapper, {
iframeStyle: {
height: "auto",
width: "100%",
aspectRatio: 16 / 9,
minWidth: "400px",
maxWidth: "920px",
border: "1px solid var(--grey)",
borderRadius: "4px",
},
showLeaveButton: true,
});
this.callFrame = callFrame;
Getting back to joinRoom
, we then actually create the DailyIframe
that will host our video call and assign it to the variable callFrame
. This variable then gets assigned to our data option so it can be referenced later. (If you were using a state management library, you would add it to your app’s state at this point.)
Note: The options passed to createFrame
, like iframeStyle
, are optional.
// Add event listeners and join call
callFrame
.on("loaded", logEvent)
.on("started-camera", logEvent)
.on("camera-error", logEvent)
.on("joining-meeting", goToLobby)
.on("joined-meeting", goToCall)
.on("left-meeting", leaveCall);
callFrame.join({ url });
Once the callFrame
exists, we can attach all the Daily event listeners to it with our callbacks created earlier, and join the call. To join, make sure you pass the Daily room URL, which is the value the user entered into the input.
After the join
method is called, you should see two possible views depending on your room’s prejoin UI
settings.
If you have the prejoin UI
option enabled, you will see the lobby view. The joining-meeting
event will get triggered, which will call the goToLobby
callback that we set above.
In the lobby view, you will no longer see the default view because the status
value has changed to lobby
. If we review our DOM elements, we can see the call container now shows because status !== ‘home’
(it equals lobby
now). The controls do not show yet, though, because we’re not officially in the call yet.
<div class="call-container" :class="{ hidden: status === 'home' }">
<!-- The Daily Prebuilt iframe is embedded in the div below using the ref -->
<div id="call" ref="callRef"></div>
<!-- Only show the control panel if a call is live -->
<controls
v-if="status === 'call'"
:roomUrl="roomUrl"
:callFrame="callFrame"
/>
</div>
The second possible view, if you have the prejoin UI
disabled for the room you’re in, is seeing the call view. This means you are in the Daily call! 💪
The joined-meeting
event would have been triggered, calling the goToCall
callback we set, which will update the status
to be call
. This status change will cause the controls to now show.
Controlling your in-call experience programmatically
One of the best things about Daily Prebuilt is that the hard parts of building video calls are done for you but there are still lots of options that can be configured or customized.
Once the DailyIframe
instance (our video call iframe) has been created, you have access to dozens of instance methods to help you manage your call functionality.
For example, let’s say you want to add a button to your app to leave a call. You can create a button that calls the .leave()
instance method on click.
To look at how some of these methods work, we can review how the Controls
component is set up.
To start, let’s see which props are passed to the Controls
component where it’s used in Home
.
<controls
v-if="status === 'call'"
:roomUrl="roomUrl"
:callFrame="callFrame"
/>
The v-if
means the controls are only rendered if the status
value is equal to call
. This means it only shows when a person is live in a call in this demo.
The roomUrl
prop is the URL the user submitted in the default home view.
The callFrame
prop is the DailyIframe instance created for the call, which gives us access to all the instance methods.
Note: Not all instance methods are available for Daily Prebuilt. Refer to our documentation to know which ones can be used.
Now let’s take a look at our Controls
component and see how the HTML is structured:
<template>
<div class="controls">
<h2>Call overview</h2>
<hr />
<h3>Invite participants</h3>
<label for="urlInput">Share URL below to invite others</label>
<div>
<!-- Room URL to copy and share -->
<input type="text" id="urlInput" :value="roomUrl" />
<button @click="copyUrl" class="teal">{{ copyButtonText }}</button>
</div>
<hr />
<h3>Example custom controls</h3>
<p>
You can also create your own meeting controls using daily-js methods
</p>
<div>
<button @click="toggleCamera">Toggle camera</button>
<button @click="toggleMic">Toggle mic</button>
<button @click="toggleScreenShare">Toggle screen share</button>
<button @click="expandFullscreen">Expand fullscreen</button>
<button @click="toggleLocalVideo">
{{ localVideoText }} local video
</button>
<button @click="toggleRemoteParticipants">
{{ remoteVideoText }} remote participants (Speaker view only)
</button>
<button @click="leaveCall">
Leave call
</button>
</div>
</div>
</template>
We display the roomUrl
prop in the input for the user to copy and share with others so they can join the call, too.
We also have eight buttons included in the control panel to programmatically interact with the DailyIframe
instance. There interactions include:
- Turning the local camera on and off
- Turning the local microphone on an off
- Sharing the local call participant’s screen
- Expanding Daily Prebuilt to be fullscreen
- Hiding and showing the local participant’s tile in the call
- Hiding and showing the participants bar, which is where all the remote participants’ tiles are locally while in speaker mode
- Leaving the call to go back to the default home view
Toggle your local camera programmatically
To understand how these work, let’s review a couple, starting with toggling the local camera.
<button @click="toggleCamera">Toggle camera</button>
To turn the local camera on and off, the control panel button has the following click event attached to it:
toggleCamera() {
this.callFrame.setLocalVideo(!this.callFrame.localVideo());
},
this.callFrame
refers to the callFrame
prop passed in the Home
component, which gives us access to the DailyIframe
instance. We can then call .setLocalVideo()
, an instance method that accepts a boolean value.
The current status of the local camera can be accessed with the .localVideo()
instance method, which will return whether the local camera is currently on or off. Since we want this method to toggle the current state, we can pass .setLocalVideo()
whatever the inverse of the current state of the camera is with !this.callFrame.localVideo()
.
So, if the camera is currently on, calling this.callFrame.setLocalVideo(!this.callFrame.localVideo());
is the same as calling this.callFrame.setLocalVideo(false);
to turn it off.
Go fullscreen with the click of a button ✨
The other buttons in the control panel mostly work the same way. Let’s take a look at one more example to see how to update your Daily Prebuilt calls programmatically.
The control panel includes a button to make the Daily Prebuilt iframe fullscreen:
<button @click="expandFullscreen">Expand fullscreen</button>
The click handler on this button uses the callFrame
prop to access the DailyIframe
instance, which can then call the requestFullscreen()
instance method.
And with one click, you're in fullscreen mode. It’s really as simple as that! 🙌
Wrapping up
Now that you know how to embed Daily Prebuilt in a Vue app, you can add Daily video chat to any Vue projects you’re building! Tag us on Twitter (@trydaily) to show us your projects. 😊
In terms of next steps, to learn how to customize your video apps even more, try updating your Daily Prebuilt colour theme.
Posted on August 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.