DEV Community

Neo
Neo

Posted on

CREATE A GO APPLICATION WITH ONLINE PRESENCE

You will need Go 0.10+ installed on your machine.

When building applications that allow multiple users to interact with one another, it is essential to display their online presence so that each user gets an idea of how many other users are online.

In this article, we will build a live streaming application that displays the online presence of the users currently streaming a video. We will use Go, JavaScript (Vue) and Pusher for the development.

Here’s a demo of the final application:

The source code for this tutorial is available on GitHub.

PREREQUISITES

To follow along with this article, you will need the following:

  • A code editor like Visual Studio Code.
  • Basic knowledge of the Go programming language.
  • Go (version >= 0.10.x) installed on your computer. Installation guide.
  • Basic knowledge of JavaScript (Vue).
  • A Pusher application. Create one here. Once you have all the above requirements, we can proceed.

BUILDING THE BACKEND SERVER

We will build the backend server in Go. Create a new project directory in the src directory that is located in the $GOPATH, let’s call this directory go-pusher-presence-app.

    $ cd $GOPATH/src
    $ mkdir go-pusher-presence-app
    $ cd go-pusher-presence-app
Enter fullscreen mode Exit fullscreen mode

Next, create a new Go file and call it presence.go, this file will be where our entire backend server logic will be. Now, let’s pull in the official Go Pusher package with this command:

   $ go get github.com/pusher/pusher-http-go
Enter fullscreen mode Exit fullscreen mode

Open the presence.go file and paste the following code:

    // File: ./presence.go
    package main

    import (
        "encoding/json"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"
        pusher "github.com/pusher/pusher-http-go"
    )

    var client = pusher.Client{
        AppId:   "PUSHER_APP_ID",
        Key:     "PUSHER_APP_KEY",
        Secret:  "PUSHER_APP_SECRET",
        Cluster: "PUSHER_APP_CLUSTER",
        Secure:  true,
    }

    type user struct {
        Username  string `json:"username" xml:"username" form:"username" query:"username"`
        Email string `json:"email" xml:"email" form:"email" query:"email"`
    }

    var loggedInUser user

    func main() {
        // Define our routes
        http.Handle("/", http.FileServer(http.Dir("./static")))
        http.HandleFunc("/isLoggedIn", isUserLoggedIn)
        http.HandleFunc("/new/user", NewUser)
        http.HandleFunc("/pusher/auth", pusherAuth)

        // Start executing the application on port 8090
        log.Fatal(http.ListenAndServe(":8090", nil))
    }

Enter fullscreen mode Exit fullscreen mode
Replace the PUSHER_APP_* keys with the keys on your Pusher dashboard.

Enter fullscreen mode Exit fullscreen mode

Here’s a breakdown of what we’ve done in the code above:

  • We imported all the packages that are required for the application to work, including Pusher.
  • We instantiated the Pusher client that we will use to authenticate users from the client-side.
  • We defined a user struct and gave it two the properties — username and email — so that Go knows how to handle incoming payloads and correctly bind it to a user instance.
  • We created a global instance of the user struct so that we can use it to store a user’s name and email. This instance is going to somewhat serve the purpose of a session on a server, we will check that it is set before allowing a user to access the dashboard of this application. In the main function, we registered four endpoints:
  1. / - loads all the static files from the static directory.
  2. /isLoggedIn - checks if a user is logged in or not and returns a fitting message.
  3. /new/user - allows a new user to connect and initializes the global user instance.
  4. /pusher/auth — authorizes users from the client-side. In the same file, above the main function, add the code for the handler function of the /isLoggedIn endpoint:
  // File: ./presence.go

    // [...]

    func isUserLoggedIn(rw http.ResponseWriter, req *http.Request){
        if loggedInUser.Username != "" && loggedInUser.Email != "" {
            json.NewEncoder(rw).Encode(loggedInUser)
        } else {
            json.NewEncoder(rw).Encode("false")
        }
    }

    // [...]
Enter fullscreen mode Exit fullscreen mode

After the function above, let’s add the handler function for the /new/user endpoint:

    // File: ./presence.go

    // [...]

    func NewUser(rw http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            panic(err)
        }
        err = json.Unmarshal(body, &loggedInUser)
        if err != nil {
            panic(err)
        }
        json.NewEncoder(rw).Encode(loggedInUser)
    }

    // [...]
Enter fullscreen mode Exit fullscreen mode

Above, we receive a new user's details in a POST request and bind it to an instance of the user struct. We further use this user instance to check if a user is logged in or not

Lastly, after the function above, let’s add the code for the /pusher/auth endpoint:

  // File: ./presence.go

    // [...]

    // -------------------------------------------------------
    // Here, we authorize users so that they can subscribe to 
    // the presence channel
    // -------------------------------------------------------

    func pusherAuth(res http.ResponseWriter, req *http.Request) {
        params, _ := ioutil.ReadAll(req.Body)

        data := pusher.MemberData{
            UserId: loggedInUser.Username,
            UserInfo: map[string]string{
                "email": loggedInUser.Email,
            },
        }

        response, err := client.AuthenticatePresenceChannel(params, data)
        if err != nil {
            panic(err)
        }

        fmt.Fprintf(res, string(response))
    }

    // [...]
Enter fullscreen mode Exit fullscreen mode

To ensure that every connected user has a unique presence, we used the properties of the global loggedInUser variable in setting the pusher.MemberData instance.

The syntax for authenticating a Pusher presence channel is:

    client.AuthenticatePresenceChannel(params, presenceData)
Enter fullscreen mode Exit fullscreen mode

BUILDING THE FRONTEND

Next, in the root of the project, create a static folder. Create two files the directory named index.html and dashboard.html. In the index.html file, we will write the HTML code that allows users to connect to the live streaming application using their name and email.

SETTING UP THE CONNECTION PAGE

Open the index.html file and update it with the following code:

 <!-- File: ./static/index.html -->
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <title>Live streamer</title>
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
            <style>
                  :root {
                    --input-padding-x: .75rem;
                    --input-padding-y: .75rem;
                  }
                  html,
                  body, body > div {
                    height: 100%;
                  }
                  body > div {
                    display: -ms-flexbox;
                    display: flex;
                    -ms-flex-align: center;
                    align-items: center;
                    padding-top: 40px;
                    padding-bottom: 40px;
                    background-color: #f5f5f5;
                  }
                  .form-signin {
                    width: 100%;
                    max-width: 420px;
                    padding: 15px;
                    margin: auto;
                  }
                  .form-label-group {
                    position: relative;
                    margin-bottom: 1rem;
                  }
                  .form-label-group > input,
                  .form-label-group > label {
                    padding: var(--input-padding-y) var(--input-padding-x);
                  }
                  .form-label-group > label {
                    position: absolute;
                    top: 0;
                    left: 0;
                    display: block;
                    width: 100%;
                    margin-bottom: 0; /* Override default `<label>` margin */
                    line-height: 1.5;
                    color: #495057;
                    cursor: text; /* Match the input under the label */
                    border: 1px solid transparent;
                    border-radius: .25rem;
                    transition: all .1s ease-in-out;
                  }
                  .form-label-group input::-webkit-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input:-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-moz-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::placeholder {
                    color: transparent;
                  }
                  .form-label-group input:not(:placeholder-shown) {
                    padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
                    padding-bottom: calc(var(--input-padding-y) / 3);
                  }
                  .form-label-group input:not(:placeholder-shown) ~ label {
                    padding-top: calc(var(--input-padding-y) / 3);
                    padding-bottom: calc(var(--input-padding-y) / 3);
                    font-size: 12px;
                    color: #777;
                  }
            </style>
          </head>

          <body>
            <div id="app">
              <form class="form-signin">
                <div class="text-center mb-4">
                  <img class="mb-4" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" alt="" width="72" height="72">
                  <h1 class="h3 mb-3 font-weight-normal">Live streamer</h1>
                  <p>STREAM YOUR FAVOURITE VIDEOS FOR FREE</p>
                </div>
                <div class="form-label-group">
                    <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus="">
                      <label for="inputUsername">Username</label>
                  </div>

                <div class="form-label-group">
                  <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required>
                    <label for="inputEmail">Email address</label>
                </div>

                <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button>
                <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p>
              </form>
              </div>

              <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

On line 106, we added Vue using a CDN. Let’s add the Vue script for the page.

Before the closing body tag add the following code:

<script>
      var app = new Vue({
        el: '#app',
        methods: {
          login: function () {
            let username = this.$refs.username.value
            let email = this.$refs.email.value

            fetch('new/user', {
              method: 'POST',
              headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({username, email})
            })
            .then(res => res.json())
            .then(data => window.location.replace('/dashboard.html'))
          }
        }
      })
    </script>
Enter fullscreen mode Exit fullscreen mode

This script above submits user data to the backend Go server and navigates the browser’s location to the dashboard’s URL.

Next, let’s build the dashboard.

SETTING UP THE DASHBOARD

Open the dashboard.html file and update it with the following code:

    <!-- File: ./static/dashboard.html -->
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
        <title>Live streamer | Dashboard</title>
      </head>
      <body>
        <div id="app">
          <div class="container-fluid row shadow p-1 mb-3">
            <div class="col-3">
              <img class="ml-3" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" height="72px" width="72px"/>
            </div>
            <div class="col-6 ml-auto mt-3">
              <div class="input-group">
                <input type="text" class="form-control" aria-label="Text input with dropdown button">
                <div class="input-group-append">
                  <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Search</button>
                </div>
              </div>
            </div>
            <div class="col-3 float-right">
              <img src="https://www.seoclerk.com/pics/319222-1IvI0s1421931178.png"  height="72px" width="72px" class="rounded-circle border"/>
              <p class="mr-auto mt-3 d-inline"> {{ username }} </p>
            </div>
          </div>
          <div class="container-fluid">
            <div class="row">
              <div class="col-8">
                <div class="embed-responsive embed-responsive-16by9">
                  <iframe width="854" height="480" class="embed-responsive-item" src="https://www.youtube.com/embed/VYOjWnS4cMY" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
                </div>
                <div class="text-center mt-3 p-3 text-muted font-weight-bold border">
                  {{ member }} person(s) is/are currently viewing this video 
                  <hr>
                  <li class="m-auto text-success" v-for="member in connectedMembers">
                    {{ member }}
                  </li>
                </div>
              </div>
              <div class="col-4 border text-justify" style="background: #e0e0e0; height: 30em; overflow-y: scroll; position: relative;">
                <div class="border invisible h-50 w-75 text-center" ref="added" style="font-size: 2rem; position: absolute; right: 0; background: #48cbe0">{{ addedMember }} just started watching.</div>
                <div class="border invisible h-50 w-75 text-center" ref="removed" style="font-size: 2rem; position: absolute; right: 0; background: #ff8325">{{ removedMember }} just stopped watching.</div>
                <div class="h-75 text-center">
                  <h2 class="text-center my-3"> Lyrics </h2>
                  <p class="w-75 m-auto" style="font-size: 1.5rem">
                    We just wanna party<br>
                    Party just for you<br>
                    We just want the money<br>
                    Money just for you<br>
                    I know you wanna party<br>
                    Party just for me<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame<br>
                    We just wanna party (yeah)<br>
                    Party just for you (yeah)<br>
                    We just want the money (yeah)<br>
                    Money just for you (you)<br>
                    I know you wanna party (yeah)<br>
                    Party just for me (yeah)<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame (you)<br>
                    This is America<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                    This is America (woo)<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

⚠️ Video is an embed from YouTube and may not play depending on your region.

On line 80 we imported the JavaScript Pusher library so let’s add some code to utilize it. Before the closing body tag, add the following code:

    <script>
    var app = new Vue({
        el: '#app',
        data: {
            username: '',
            member: 0,
            addedMember: '',
            removedMember: '',
            connectedMembers: []
        },

        created() {
            fetch('/isLoggedIn', {
                method: 'GET',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                }
            })
            .then(res => res.json())
            .then(data => {
                if (data != 'false') {
                    this.username = data.username
                } else {
                    window.location.replace('/')
                }
            })

            this.subscribe()
        },

        methods: {
            subscribe: function () {
                const pusher = new Pusher('PUSHER_APP_KEY', {
                    authEndpoint: '/pusher/auth',
                    cluster: 'PUSHER_APP_CLUSTER',
                    encrypted: true
                });

                let channel = pusher.subscribe('presence-channel')

                channel.bind('pusher:subscription_succeeded', data => {
                    this.member = data.count
                    data.each(member => this.connectedMembers.push(member.id))
                })

                // Display a notification when a member comes online
                channel.bind('pusher:member_added', data => {
                    this.member++
                    this.connectedMembers.push(data.id)
                    this.addedMember = data.id

                    this.$refs.added.classList.add('visible')
                    this.$refs.added.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.added.classList.remove('visible');
                        this.$refs.added.classList.add('invisible');
                    }, 3000)
                });

                // Display a notification when a member goes offline
                channel.bind('pusher:member_removed', data => {
                    this.member--
                    let index = this.connectedMembers.indexOf(data.id)

                    if (index > -1) {
                        this.connectedMembers.splice(index, 1)
                    }

                    this.removedMember = data.id
                    this.$refs.removed.classList.add('visible')
                    this.$refs.removed.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.removed.classList.remove('visible')
                        this.$refs.removed.classList.add('invisible')
                    }, 3000)
                })
            }
        }
    })
    </script>
Enter fullscreen mode Exit fullscreen mode

You will need Go 0.10+ installed on your machine.
When building applications that allow multiple users to interact with one another, it is essential to display their online presence so that each user gets an idea of how many other users are online.

In this article, we will build a live streaming application that displays the online presence of the users currently streaming a video. We will use Go, JavaScript (Vue) and Pusher for the development.

Here’s a demo of the final application:

go-online-presence-demo

The source code for this tutorial is available on GitHub.

PREREQUISITES

To follow along with this article, you will need the following:

A code editor like Visual Studio Code.
Basic knowledge of the Go programming language.
Go (version >= 0.10.x) installed on your computer. Installation guide.
Basic knowledge of JavaScript (Vue).
A Pusher application. Create one here.
Once you have all the above requirements, we can proceed.

BUILDING THE BACKEND SERVER

We will build the backend server in Go. Create a new project directory in the src directory that is located in the $GOPATH, let’s call this directory go-pusher-presence-app.

$ cd $GOPATH/src
$ mkdir go-pusher-presence-app
$ cd go-pusher-presence-app
Enter fullscreen mode Exit fullscreen mode

Next, create a new Go file and call it presence.go, this file will be where our entire backend server logic will be. Now, let’s pull in the official Go Pusher package with this command:

$ go get github.com/pusher/pusher-http-go
Enter fullscreen mode Exit fullscreen mode

Open the presence.go file and paste the following code:
// File: ./presence.go
package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    pusher "github.com/pusher/pusher-http-go"
)

var client = pusher.Client{
    AppId:   "PUSHER_APP_ID",
    Key:     "PUSHER_APP_KEY",
    Secret:  "PUSHER_APP_SECRET",
    Cluster: "PUSHER_APP_CLUSTER",
    Secure:  true,
}

type user struct {
    Username  string `json:"username" xml:"username" form:"username" query:"username"`
    Email string `json:"email" xml:"email" form:"email" query:"email"`
}

var loggedInUser user

func main() {
    // Define our routes
    http.Handle("/", http.FileServer(http.Dir("./static")))
    http.HandleFunc("/isLoggedIn", isUserLoggedIn)
    http.HandleFunc("/new/user", NewUser)
    http.HandleFunc("/pusher/auth", pusherAuth)

    // Start executing the application on port 8090
    log.Fatal(http.ListenAndServe(":8090", nil))
}
Enter fullscreen mode Exit fullscreen mode

Replace the PUSHER_APP_* keys with the keys on your Pusher dashboard.

Here’s a breakdown of what we’ve done in the code above:

We imported all the packages that are required for the application to work, including Pusher.
We instantiated the Pusher client that we will use to authenticate users from the client-side.
We defined a user struct and gave it two the properties — username and email — so that Go knows how to handle incoming payloads and correctly bind it to a user instance.
We created a global instance of the user struct so that we can use it to store a user’s name and email. This instance is going to somewhat serve the purpose of a session on a server, we will check that it is set before allowing a user to access the dashboard of this application.
In the main function, we registered four endpoints:

/ - loads all the static files from the static directory.
/isLoggedIn - checks if a user is logged in or not and returns a fitting message.
/new/user - allows a new user to connect and initializes the global user instance.
/pusher/auth — authorizes users from the client-side.
In the same file, above the main function, add the code for the handler function of the /isLoggedIn endpoint:

// File: ./presence.go

// [...]

func isUserLoggedIn(rw http.ResponseWriter, req *http.Request){
    if loggedInUser.Username != "" && loggedInUser.Email != "" {
        json.NewEncoder(rw).Encode(loggedInUser)
    } else {
        json.NewEncoder(rw).Encode("false")
    }
}

// [...]
Enter fullscreen mode Exit fullscreen mode

After the function above, let’s add the handler function for the /new/user endpoint:

// File: ./presence.go

// [...]

func NewUser(rw http.ResponseWriter, req *http.Request) {
    body, err := ioutil.ReadAll(req.Body)
    if err != nil {
        panic(err)
    }
    err = json.Unmarshal(body, &loggedInUser)
    if err != nil {
        panic(err)
    }
    json.NewEncoder(rw).Encode(loggedInUser)
}

// [...]
Enter fullscreen mode Exit fullscreen mode

Above, we receive a new user's details in a POST request and bind it to an instance of the user struct. We further use this user instance to check if a user is logged in or not

Lastly, after the function above, let’s add the code for the /pusher/auth endpoint:

// File: ./presence.go

// [...]

// -------------------------------------------------------
// Here, we authorize users so that they can subscribe to 
// the presence channel
// -------------------------------------------------------

func pusherAuth(res http.ResponseWriter, req *http.Request) {
    params, _ := ioutil.ReadAll(req.Body)

    data := pusher.MemberData{
        UserId: loggedInUser.Username,
        UserInfo: map[string]string{
            "email": loggedInUser.Email,
        },
    }

    response, err := client.AuthenticatePresenceChannel(params, data)
    if err != nil {
        panic(err)
    }

    fmt.Fprintf(res, string(response))
}

// [...]
Enter fullscreen mode Exit fullscreen mode

To ensure that every connected user has a unique presence, we used the properties of the global loggedInUser variable in setting the pusher.MemberData instance.

The syntax for authenticating a Pusher presence channel is:
client.AuthenticatePresenceChannel(params, presenceData)

BUILDING THE FRONTEND

Next, in the root of the project, create a static folder. Create two files the directory named index.html and dashboard.html. In the index.html file, we will write the HTML code that allows users to connect to the live streaming application using their name and email.

SETTING UP THE CONNECTION PAGE

Open the index.html file and update it with the following code:

<!-- File: ./static/index.html -->
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <title>Live streamer</title>
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
            <style>
                  :root {
                    --input-padding-x: .75rem;
                    --input-padding-y: .75rem;
                  }
                  html,
                  body, body > div {
                    height: 100%;
                  }
                  body > div {
                    display: -ms-flexbox;
                    display: flex;
                    -ms-flex-align: center;
                    align-items: center;
                    padding-top: 40px;
                    padding-bottom: 40px;
                    background-color: #f5f5f5;
                  }
                  .form-signin {
                    width: 100%;
                    max-width: 420px;
                    padding: 15px;
                    margin: auto;
                  }
                  .form-label-group {
                    position: relative;
                    margin-bottom: 1rem;
                  }
                  .form-label-group > input,
                  .form-label-group > label {
                    padding: var(--input-padding-y) var(--input-padding-x);
                  }
                  .form-label-group > label {
                    position: absolute;
                    top: 0;
                    left: 0;
                    display: block;
                    width: 100%;
                    margin-bottom: 0; /* Override default `<label>` margin */
                    line-height: 1.5;
                    color: #495057;
                    cursor: text; /* Match the input under the label */
                    border: 1px solid transparent;
                    border-radius: .25rem;
                    transition: all .1s ease-in-out;
                  }
                  .form-label-group input::-webkit-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input:-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-moz-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::placeholder {
                    color: transparent;
                  }
                  .form-label-group input:not(:placeholder-shown) {
                    padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
                    padding-bottom: calc(var(--input-padding-y) / 3);
                  }
                  .form-label-group input:not(:placeholder-shown) ~ label {
                    padding-top: calc(var(--input-padding-y) / 3);
                    padding-bottom: calc(var(--input-padding-y) / 3);
                    font-size: 12px;
                    color: #777;
                  }
            </style>
          </head>

          <body>
            <div id="app">
              <form class="form-signin">
                <div class="text-center mb-4">
                  <img class="mb-4" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" alt="" width="72" height="72">
                  <h1 class="h3 mb-3 font-weight-normal">Live streamer</h1>
                  <p>STREAM YOUR FAVOURITE VIDEOS FOR FREE</p>
                </div>
                <div class="form-label-group">
                    <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus="">
                      <label for="inputUsername">Username</label>
                  </div>

                <div class="form-label-group">
                  <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required>
                    <label for="inputEmail">Email address</label>
                </div>

                <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button>
                <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p>
              </form>
              </div>

              <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

On line 106, we added Vue using a CDN. Let’s add the Vue script for the page.

Before the closing body tag add the following code:

<script>
  var app = new Vue({
    el: '#app',
    methods: {
      login: function () {
        let username = this.$refs.username.value
        let email = this.$refs.email.value

        fetch('new/user', {
          method: 'POST',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({username, email})
        })
        .then(res => res.json())
        .then(data => window.location.replace('/dashboard.html'))
      }
    }
  })
</script>
Enter fullscreen mode Exit fullscreen mode

This script above submits user data to the backend Go server and navigates the browser’s location to the dashboard’s URL.

Next, let’s build the dashboard.

SETTING UP THE DASHBOARD

Open the dashboard.html file and update it with the following code:

    <!-- File: ./static/dashboard.html -->
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
        <title>Live streamer | Dashboard</title>
      </head>
      <body>
        <div id="app">
          <div class="container-fluid row shadow p-1 mb-3">
            <div class="col-3">
              <img class="ml-3" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" height="72px" width="72px"/>
            </div>
            <div class="col-6 ml-auto mt-3">
              <div class="input-group">
                <input type="text" class="form-control" aria-label="Text input with dropdown button">
                <div class="input-group-append">
                  <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Search</button>
                </div>
              </div>
            </div>
            <div class="col-3 float-right">
              <img src="https://www.seoclerk.com/pics/319222-1IvI0s1421931178.png"  height="72px" width="72px" class="rounded-circle border"/>
              <p class="mr-auto mt-3 d-inline"> {{ username }} </p>
            </div>
          </div>
          <div class="container-fluid">
            <div class="row">
              <div class="col-8">
                <div class="embed-responsive embed-responsive-16by9">
                  <iframe width="854" height="480" class="embed-responsive-item" src="https://www.youtube.com/embed/VYOjWnS4cMY" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
                </div>
                <div class="text-center mt-3 p-3 text-muted font-weight-bold border">
                  {{ member }} person(s) is/are currently viewing this video 
                  <hr>
                  <li class="m-auto text-success" v-for="member in connectedMembers">
                    {{ member }}
                  </li>
                </div>
              </div>
              <div class="col-4 border text-justify" style="background: #e0e0e0; height: 30em; overflow-y: scroll; position: relative;">
                <div class="border invisible h-50 w-75 text-center" ref="added" style="font-size: 2rem; position: absolute; right: 0; background: #48cbe0">{{ addedMember }} just started watching.</div>
                <div class="border invisible h-50 w-75 text-center" ref="removed" style="font-size: 2rem; position: absolute; right: 0; background: #ff8325">{{ removedMember }} just stopped watching.</div>
                <div class="h-75 text-center">
                  <h2 class="text-center my-3"> Lyrics </h2>
                  <p class="w-75 m-auto" style="font-size: 1.5rem">
                    We just wanna party<br>
                    Party just for you<br>
                    We just want the money<br>
                    Money just for you<br>
                    I know you wanna party<br>
                    Party just for me<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame<br>
                    We just wanna party (yeah)<br>
                    Party just for you (yeah)<br>
                    We just want the money (yeah)<br>
                    Money just for you (you)<br>
                    I know you wanna party (yeah)<br>
                    Party just for me (yeah)<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame (you)<br>
                    This is America<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                    This is America (woo)<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
      </body>
    </html>
Enter fullscreen mode Exit fullscreen mode

⚠️ Video is an embed from YouTube and may not play depending on your region.

On line 80 we imported the JavaScript Pusher library so let’s add some code to utilize it. Before the closing body tag, add the following code:

    <script>
    var app = new Vue({
        el: '#app',
        data: {
            username: '',
            member: 0,
            addedMember: '',
            removedMember: '',
            connectedMembers: []
        },

        created() {
            fetch('/isLoggedIn', {
                method: 'GET',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                }
            })
            .then(res => res.json())
            .then(data => {
                if (data != 'false') {
                    this.username = data.username
                } else {
                    window.location.replace('/')
                }
            })

            this.subscribe()
        },

        methods: {
            subscribe: function () {
                const pusher = new Pusher('PUSHER_APP_KEY', {
                    authEndpoint: '/pusher/auth',
                    cluster: 'PUSHER_APP_CLUSTER',
                    encrypted: true
                });

                let channel = pusher.subscribe('presence-channel')

                channel.bind('pusher:subscription_succeeded', data => {
                    this.member = data.count
                    data.each(member => this.connectedMembers.push(member.id))
                })

                // Display a notification when a member comes online
                channel.bind('pusher:member_added', data => {
                    this.member++
                    this.connectedMembers.push(data.id)
                    this.addedMember = data.id

                    this.$refs.added.classList.add('visible')
                    this.$refs.added.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.added.classList.remove('visible');
                        this.$refs.added.classList.add('invisible');
                    }, 3000)
                });

                // Display a notification when a member goes offline
                channel.bind('pusher:member_removed', data => {
                    this.member--
                    let index = this.connectedMembers.indexOf(data.id)

                    if (index > -1) {
                        this.connectedMembers.splice(index, 1)
                    }

                    this.removedMember = data.id
                    this.$refs.removed.classList.add('visible')
                    this.$refs.removed.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.removed.classList.remove('visible')
                        this.$refs.removed.classList.add('invisible')
                    }, 3000)
                })
            }
        }
    })
    </script>
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we created some Vue data variables to display reactive updates on different parts of the DOM. We also registered a created() lifecycle hook that checks if a user is connected on the backend server and eligible to view the dashboard before calling the subscribe() method.

The subscribe() method first configures a Pusher instance using the keys provided on the dashboard then subscribes to a presence channel. Next, it binds to several events that are available on the returned object of a presence channel subscription.

In the callback function of these bindings, we are able to update the state of the data variables, this is how we display the visual updates on user presence in this application.

TESTING THE APPLICATION

We can test the application by compiling down the Go source code and running it with this command:

$ go run presence.go
Enter fullscreen mode Exit fullscreen mode

The application will be available for testing on this address http://127.0.0.1:8090, here’s a display of how the application should look:

CONCLUSION

In this tutorial, we have learned how to leverage the Pusher SDK in creating a live streaming application powered by a Go backend server.

The source code for this tutorial is available on [GitHub(https://github.com/neoighodaro/go-pusher-presence-app).

This was first published on pusher site.

Top comments (2)

Collapse
 
tonyalaribe profile image
Anthony Alaribe

Awesome article. I wish you would come for the Golang meetup tomorrow.

Anyway, there is just one thing I would want to comment about on your article, especially for newcomers who might end up copying and pasting.

loggedInUser is a global variable that would be accessed from the handlers. So, the way Go router works is that it creates a goroutine to handle each request, and with all the goroutines accessing that struct, it would mean 2 things:

  1. Only one user can be logged in at a time. If another user logs in, it would overwrite that variable in memory.
  2. In an application which would have multiple requests being handled at a time, you are going to have a data race (multiple goroutines accessing the same resource in memory). A solution could be to use mutex locks, but that is also not a good solution depending on who you ask. It's a better idea to avoid global variables in the first place, and architect your application such that you never need to use locks.

If you must use a global+mutex lock, at least store the data as a map of say; usernames, to the user struct so multiple users can be logged in at once. I noticed a few other things, but they are minor.

Great article, still.

Collapse
 
codehakase profile image
Francis Sunday

Nice post! I noticed there seem to be a repetition somehow - "Building the backend" & "Building the dashboard" appearing twice