How to Use Svelte and Go to Build a Video Chat App

November 21, 2022
Written by
Joseph Udonsak
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Main tutorial image for the

This article is for reference only. We're not onboarding new customers to Programmable Video. Existing customers can continue to use the product until December 5, 2024.


We recommend migrating your application to the API provided by our preferred video partner, Zoom. We've prepared this migration guide to assist you in minimizing any service disruption.

The internet has made the world a much smaller place. Not only can you chat with your loved ones in real-time, but you can also see them and experience an extra level of interaction that words fail to capture.

In this tutorial, I will show you how simple it is to build a video chat app using Twilio Video, using Svelte for the frontend and Golang for the backend.

Prerequisites

To follow this tutorial, you will need the following:

Getting started

Brief Overview

The process flow of the application is as follows:

  1. The frontend displays a form that allows the user to specify a room name.
  2. The room name provided by the user is then passed to the backend.
  3. Using this room name, the backend will then generate and return an Access Token containing a video grant.
  4. This token is then used by the frontend application to connect to the created room and subscribe for room updates, such as when a new participant joins the room.

To get started, create a new folder named video_app, which will have two sub-folders named frontend and backend, by running the commands below.

mkdir video_app
cd video_app 
mkdir frontend backend

Build the application's backend

Then, change into the backend folder to begin creating the backend of the application. Now, start by creating a new module named svelte_video_app using the following command.

go mod init svelte_video_app

After that, you need to add the project's dependencies:

  1. Go CORS Handler: CORS is a net/http handler implementing Cross Origin Resource Sharing W3 specification in Golang. Since the frontend and backend will be running on separate ports, CORS will need to be enabled.
  2. GoDotEnv: This will help with managing environment variables.
  3. Twilio Go Helper Library: This simplifies interacting with the Twilio API.

To do this, run the following command.

go get github.com/twilio/twilio-go github.com/joho/godotenv github.com/rs/cors

Next, create a new file named .env to store the environment variables which the application requires. Then, in the new file paste the following code.

TWILIO_ACCOUNT_SID="<<TWILIO_ACCOUNT_SID>>"
TWILIO_API_KEY_SID="<<TWILIO_API_KEY_SID>>"
TWILIO_API_SECRET_KEY="<<TWILIO_API_SECRET_KEY>>"

After that, create a local version of the .env file using the following command.

cp .env .env.local

.env.**.local files are ignored by Git as an accepted best practice for storing credentials outside of code to keep them safe.

The Account Info panel in the Twilio Console

Launch your Twilio console to access your Twilio credentials. Copy the ACCOUNT SID from your dashboard. Henceforth, this will be referred to as your TWILIO_ACCOUNT_SID.

Create a new Twilio API key form

Next, create a new API Key which will be used for authentication to the Twilio Video API.

Give your API key a friendly name such as "Video App" and click on the Create API Key button, at the bottom of the page, to complete the process.

Once completed, the API Key SID and API Key Secret will be displayed. Make a note of both as the secret key; it is only displayed once. Going forward, these keys will be referenced as TWILIO_API_KEY_SID and TWILIO_API_SECRET_KEY respectively.

Replace the placeholder values in .env.local with the respective keys you copied from your Twilio console.

Add the ability to generate an Access Token

Create a new folder named helper in the backend folder. In the new helper folder, create a new file named token.go and add the following code to it.

package helper

import (
        "crypto/md5"
        "crypto/rand"
        "encoding/hex"
        "fmt"
        "github.com/twilio/twilio-go/client/jwt"
        "os"
)

func identity() string {
        input, _ := rand.Prime(rand.Reader, 128)
        hash := md5.Sum([]byte(input.String()))
        return hex.EncodeToString(hash[:])
}

func GenerateToken(roomName string) string {
        params := jwt.AccessTokenParams{
                AccountSid:    os.Getenv("TWILIO_ACCOUNT_SID"),
                SigningKeySid: os.Getenv("TWILIO_API_KEY_SID"),
                Secret:        os.Getenv("TWILIO_API_SECRET_KEY"),
                Identity:      identity(),
        }

        jwtToken := jwt.CreateAccessToken(params)

        videoGrant := &jwt.VideoGrant{
                Room: roomName,
        }

        jwtToken.AddGrant(videoGrant)
        token, err := jwtToken.ToJwt()

        if err != nil {
                fmt.Println(err)
        }

        return token
}

The identity() function is used to generate a unique identifier for each user. This unique identifier is passed to the AccessTokenParams struct which is used to create the Access Token JWT. Additionally, a video grant for the provided room name is added to the token. In the absence of any errors, the token is returned for use elsewhere in the application.

Model the room

Next, create a struct to model the room. This struct will simplify parsing of the JSON request. Additionally, it will hold the logic for validating the provided room name before a request to the Twilio API can be made.

To do this, create a new folder named model in the backend folder, and in that new folder create a file named room.go. Then, paste the following code into the new file.

package model

import "errors"

type Room struct {
        Name string `json:"roomName"`
}

func (room *Room) Validate() error {
        if len(room.Name) < 6 {
                return errors.New("room name cannot be less than 6 characters")
        }
        return nil
}

This struct has a single field named Name which corresponds to the room name received from the frontend application.

The Validate() function takes a Room struct and ensures that the room name is not shorter than six characters. The validation rules can be modified as required to suit your requirements.

Create the main function

All that is left is a main() function to serve as the entry point for the application and handle incoming requests. In the backend folder, create a new file named main.go and paste the following code into it.

package main

import (
        "encoding/json"
        "fmt"
        "github.com/joho/godotenv"
        "github.com/rs/cors"
        "log"
        "net/http"
        "svelte_video_app/helper"
        "svelte_video_app/model"
)

func main() {
        loadEnv()

        c := cors.Default()
        handler := http.HandlerFunc(roomHandler)
        const serverPort = "8000"

        fmt.Printf("Starting server listening on port %s\n", serverPort)

        if err := http.ListenAndServe(fmt.Sprintf(":%s", serverPort), c.Handler(handler)); err != nil {
                log.Fatal(err)
        }

}

func loadEnv() {
        err := godotenv.Load(".env.local")
        if err != nil {
                log.Fatal("Error loading .env file")
        }
}

func roomHandler(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        response := make(map[string]string)

        var room model.Room

        json.NewDecoder(request.Body).Decode(&room)

        err := room.Validate()

        if err != nil {
                writer.WriteHeader(http.StatusBadRequest)
                response["message"] = err.Error()
        } else {
                response["jwt"] = helper.GenerateToken(room.Name)
        }

        jsonResponse, err := json.Marshal(response)

        if err != nil {
                log.Fatalf("Error happened in JSON marshal. Err: %s", err)
        }

        writer.Write(jsonResponse)
}

In the main() function, four key things take place:

  1. The environment variables are loaded.
  2. A new CORS handler with default options is created.
  3. The roomHandler() function is attached to the CORS handler.
  4. The application is served on port 8000.

You can serve the backend by running the following command.

go run main.go

Build the application's frontend

Now, change into the frontend folder to begin creating the frontend of the application with Svelte.

There, create a new Svelte project by running the following command.

npx degit sveltejs/template .

When prompted with the following question, press Enter.

Need to install the following packages:
  degit
Ok to proceed? (y)

Next, install the JavaScript dependencies for the project, by running the following command.

npm install --save-dev axios izitoast twilio-video 

The dependencies are as follows:

  1. Axios: Axios will be used to make API requests and get the appropriate response
  2. Izitoast: This plugin will be used for displaying notifications
  3. Twilio-video.js: twilio-video.js allows you to add real-time voice and video to your web apps

Build helper function

First, create a helper function to handle the process of connecting to a room. In the frontend/src folder, create a new file named Helper.js and add the following code to it.

import {connect, createLocalVideoTrack} from "twilio-video";
import axios from "axios";
import iziToast from "izitoast";

const notify = message => {
    iziToast.success({
        message,
        position: 'topRight'

    });
}

const axiosInstance = axios.create({
    baseURL: "http://localhost:8000",
});

export const getAccessToken = async (roomName) => {
    const response = await axiosInstance.post("", {roomName});
    const {jwt} = response.data;
    return jwt;
};

export const connectToRoom = async (roomName, videoContainer) => {
    const token = await getAccessToken(roomName);
    const room = await connect(token, {name: roomName});
    const videoTrack = await createLocalVideoTrack();
    notify(`Successfully joined a Room: ${room}`);
    videoContainer.appendChild(videoTrack.attach());
    room.on('participantConnected', participant => {
        notify(`A remote participant connected: ${participant.identity}`);
        participant.tracks.forEach(publication => {
            if (publication.isSubscribed) {
                const track = publication.track;
                videoContainer.appendChild(track.attach());
            }
        });
        participant.on('trackSubscribed', track => {
            videoContainer.appendChild(track.attach());
        });
    });
    room.participants.forEach(participant => {
        participant.tracks.forEach(publication => {
            if (publication.track) {
                videoContainer.appendChild(publication.track.attach());
            }
        });
        participant.on('trackSubscribed', track => {
            videoContainer.appendChild(track.attach());
        });
    });
}

The connectToRoom function takes two parameters, the name of the room and the DOM element which will hold the Video Participant tracks. Using the name of the room, a new Access Token is created (via an API call to the backend application).

The Access Token is passed along with the room name to the connect() function provided by twilio-video. The connect() function returns a room object which allows you to subscribe to the participantConnected event and add the participant’s video track to the video container.

Next, a video track for the user is created and attached to the video container. The first time this occurs, a prompt will be displayed asking the user for permission to access the device camera and microphone.

Update the App component

Now, update frontend/src/App.svelte to match the following code.

<script>
    import 'izitoast/dist/css/iziToast.min.css'
    import 'izitoast/dist/js/iziToast.min'

    import {connectToRoom} from "./Helper";

    let hasJoinedRoom = false;

    const handleSubmit = async e => {
        const formData = new FormData(e.target);
        const roomName = formData.get('roomName');
        hasJoinedRoom = true
        const videoContainer = document.getElementById('remote-media');
        await connectToRoom(roomName, videoContainer)
    }
</script>

<main>
    <h1> Svelte Go Twilio Video Chat App </h1>
    <div id="remote-media"></div>
    {#if !hasJoinedRoom}
        <form on:submit|preventDefault={handleSubmit}>
            <div>
                <label for="roomName">Room name</label>
                <input
                        type="text"
                        id="roomName"
                        name="roomName"
                        value=""
                />
            </div>
            <button type="submit">Submit</button>
        </form>
    {/if}
</main>

<style>
    main {
        text-align: center;
        padding: 1em;
        max-width: 240px;
        margin: 0 auto;
    }

    @media (min-width: 640px) {
        main {
            max-width: none;
        }
    }
</style>

Configure Rollup for Twilio-video.js

While the code for the frontend is now complete, there’s still some work to do before the frontend application can be bundled successfully. Try running the application using the following command.

npm run dev

You will encounter the following error:

!] Error: Unexpected token (Note that you need @rollup/plugin-json to import JSON files)
node_modules/twilio-video/package.json (2:8)

To fix this problem, import the rollup/plugin-json package using the following command.

npm install --save-dev @rollup/plugin-json

Next, open the frontend/rollup.config.js file. In the plugins array of the default export, add the following plugin

plugins: [
    json(),
    ......
],

Don’t forget to add the appropriate import statement to the top of the file.

import json from "@rollup/plugin-json";

Svelte has LiveReload enabled hence the bundle process will re-run once you have made the changes. This time you will notice a warning as shown below:

(!) Plugin node-resolve: preferring built-in module 'events' over local alternative at ... , pass 'preferBuiltins: false' to disable this behavior or 'preferBuiltins: true' to disable this warning
(!) Missing global variable name
Use output.globals to specify browser global variable names corresponding to external modules
events (guessing 'require$$0')

If you try navigating to the served application at http://localhost:8080/ you will encounter a blank screen and the following error in your console.

Uncaught ReferenceError: require$$0 is not defined

To fix this, head back to your rollup config. There, in the plugins array, add the following to the options object passed to the resolve() function.

{
    preferBuiltins: false,
        ...
}

This time, your frontend application bundles without any errors and you can test your application. To do that, navigate to http://localhost:8080/ in two browser tabs to see your application in action.

Demo of the working Svelte / Go application

Conclusion

There you have it, without writing any code for permissions, RTC (Real Time Communication) or data streaming, you have an application capable of handling all those and even more.

In addition to video streaming, the Twilio Video SDK provides functionality for muting audio, recording video chats and displaying camera previews before joining a room. You can view the documentation here,

I’m excited to see you build the next big thing in video communication.  

You can review the final codebase for this article on Github. Until next time ✌🏾

Joseph Udonsak is a software engineer with a passion for solving challenges – be it building applications, or conquering new frontiers on Candy Crush. When he’s not staring at his screens, he enjoys a cold beer and laughs with his family and friends.

Main image credits