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 Channels for the development.
Here’s a demo of the final application:
The source code for this tutorial is available on GitHub.
To follow along with this article, you will need the following:
Once you have all the above requirements, we can proceed.
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
.
1$ cd $GOPATH/src 2 $ mkdir go-pusher-presence-app 3 $ cd go-pusher-presence-app
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
Open the presence.go
file and paste the following code:
1// File: ./presence.go 2 package main 3 4 import ( 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "net/http" 10 pusher "github.com/pusher/pusher-http-go" 11 ) 12 13 var client = pusher.Client{ 14 AppId: "PUSHER_APP_ID", 15 Key: "PUSHER_APP_KEY", 16 Secret: "PUSHER_APP_SECRET", 17 Cluster: "PUSHER_APP_CLUSTER", 18 Secure: true, 19 } 20 21 type user struct { 22 Username string `json:"username" xml:"username" form:"username" query:"username"` 23 Email string `json:"email" xml:"email" form:"email" query:"email"` 24 } 25 26 var loggedInUser user 27 28 func main() { 29 // Define our routes 30 http.Handle("/", http.FileServer(http.Dir("./static"))) 31 http.HandleFunc("/isLoggedIn", isUserLoggedIn) 32 http.HandleFunc("/new/user", NewUser) 33 http.HandleFunc("/pusher/auth", pusherAuth) 34 35 // Start executing the application on port 8090 36 log.Fatal(http.ListenAndServe(":8090", nil)) 37 }
NOTE: 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:
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:
1// File: ./presence.go 2 3 // [...] 4 5 func isUserLoggedIn(rw http.ResponseWriter, req *http.Request){ 6 if loggedInUser.Username != "" && loggedInUser.Email != "" { 7 json.NewEncoder(rw).Encode(loggedInUser) 8 } else { 9 json.NewEncoder(rw).Encode("false") 10 } 11 } 12 13 // [...]
After the function above, let’s add the handler function for the /new/user
endpoint:
1// File: ./presence.go 2 3 // [...] 4 5 func NewUser(rw http.ResponseWriter, req *http.Request) { 6 body, err := ioutil.ReadAll(req.Body) 7 if err != nil { 8 panic(err) 9 } 10 err = json.Unmarshal(body, &loggedInUser) 11 if err != nil { 12 panic(err) 13 } 14 json.NewEncoder(rw).Encode(loggedInUser) 15 } 16 17 // [...]
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:
1// File: ./presence.go 2 3 // [...] 4 5 // ------------------------------------------------------- 6 // Here, we authorize users so that they can subscribe to 7 // the presence channel 8 // ------------------------------------------------------- 9 10 func pusherAuth(res http.ResponseWriter, req *http.Request) { 11 params, _ := ioutil.ReadAll(req.Body) 12 13 data := pusher.MemberData{ 14 UserId: loggedInUser.Username, 15 UserInfo: map[string]string{ 16 "email": loggedInUser.Email, 17 }, 18 } 19 20 response, err := client.AuthenticatePresenceChannel(params, data) 21 if err != nil { 22 panic(err) 23 } 24 25 fmt.Fprintf(res, string(response)) 26 } 27 28 // [...]
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)
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.
Open the index.html
file and update it with the following code:
1<!-- File: ./static/index.html --> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <meta charset="utf-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 7 <title>Live streamer</title> 8 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"> 9 <style> 10 :root { 11 --input-padding-x: .75rem; 12 --input-padding-y: .75rem; 13 } 14 html, 15 body, body > div { 16 height: 100%; 17 } 18 body > div { 19 display: -ms-flexbox; 20 display: flex; 21 -ms-flex-align: center; 22 align-items: center; 23 padding-top: 40px; 24 padding-bottom: 40px; 25 background-color: #f5f5f5; 26 } 27 .form-signin { 28 width: 100%; 29 max-width: 420px; 30 padding: 15px; 31 margin: auto; 32 } 33 .form-label-group { 34 position: relative; 35 margin-bottom: 1rem; 36 } 37 .form-label-group > input, 38 .form-label-group > label { 39 padding: var(--input-padding-y) var(--input-padding-x); 40 } 41 .form-label-group > label { 42 position: absolute; 43 top: 0; 44 left: 0; 45 display: block; 46 width: 100%; 47 margin-bottom: 0; /* Override default `<label>` margin */ 48 line-height: 1.5; 49 color: #495057; 50 cursor: text; /* Match the input under the label */ 51 border: 1px solid transparent; 52 border-radius: .25rem; 53 transition: all .1s ease-in-out; 54 } 55 .form-label-group input::-webkit-input-placeholder { 56 color: transparent; 57 } 58 .form-label-group input:-ms-input-placeholder { 59 color: transparent; 60 } 61 .form-label-group input::-ms-input-placeholder { 62 color: transparent; 63 } 64 .form-label-group input::-moz-placeholder { 65 color: transparent; 66 } 67 .form-label-group input::placeholder { 68 color: transparent; 69 } 70 .form-label-group input:not(:placeholder-shown) { 71 padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3)); 72 padding-bottom: calc(var(--input-padding-y) / 3); 73 } 74 .form-label-group input:not(:placeholder-shown) ~ label { 75 padding-top: calc(var(--input-padding-y) / 3); 76 padding-bottom: calc(var(--input-padding-y) / 3); 77 font-size: 12px; 78 color: #777; 79 } 80 </style> 81 </head> 82 83 <body> 84 <div id="app"> 85 <form class="form-signin"> 86 <div class="text-center mb-4"> 87 <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"> 88 <h1 class="h3 mb-3 font-weight-normal">Live streamer</h1> 89 <p>STREAM YOUR FAVOURITE VIDEOS FOR FREE</p> 90 </div> 91 <div class="form-label-group"> 92 <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus=""> 93 <label for="inputUsername">Username</label> 94 </div> 95 96 <div class="form-label-group"> 97 <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required> 98 <label for="inputEmail">Email address</label> 99 </div> 100 101 <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button> 102 <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p> 103 </form> 104 </div> 105 106 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 107 </body> 108 </html>
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:
1<script> 2 var app = new Vue({ 3 el: '#app', 4 methods: { 5 login: function () { 6 let username = this.$refs.username.value 7 let email = this.$refs.email.value 8 9 fetch('new/user', { 10 method: 'POST', 11 headers: { 12 'Accept': 'application/json', 13 'Content-Type': 'application/json' 14 }, 15 body: JSON.stringify({username, email}) 16 }) 17 .then(res => res.json()) 18 .then(data => window.location.replace('/dashboard.html')) 19 } 20 } 21 }) 22 </script>
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.
Open the dashboard.html
file and update it with the following code:
1<!-- File: ./static/dashboard.html --> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <meta charset="utf-8"> 6 <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> 7 <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"> 8 <title>Live streamer | Dashboard</title> 9 </head> 10 <body> 11 <div id="app"> 12 <div class="container-fluid row shadow p-1 mb-3"> 13 <div class="col-3"> 14 <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"/> 15 </div> 16 <div class="col-6 ml-auto mt-3"> 17 <div class="input-group"> 18 <input type="text" class="form-control" aria-label="Text input with dropdown button"> 19 <div class="input-group-append"> 20 <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Search</button> 21 </div> 22 </div> 23 </div> 24 <div class="col-3 float-right"> 25 <img src="https://www.seoclerk.com/pics/319222-1IvI0s1421931178.png" height="72px" width="72px" class="rounded-circle border"/> 26 <p class="mr-auto mt-3 d-inline"> {{ username }} </p> 27 </div> 28 </div> 29 <div class="container-fluid"> 30 <div class="row"> 31 <div class="col-8"> 32 <div class="embed-responsive embed-responsive-16by9"> 33 <iframe width="854" height="480" class="embed-responsive-item" src="https://www.youtube.com/embed/VYOjWnS4cMY" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> 34 </div> 35 <div class="text-center mt-3 p-3 text-muted font-weight-bold border"> 36 {{ member }} person(s) is/are currently viewing this video 37 <hr> 38 <li class="m-auto text-success" v-for="member in connectedMembers"> 39 {{ member }} 40 </li> 41 </div> 42 </div> 43 <div class="col-4 border text-justify" style="background: #e0e0e0; height: 30em; overflow-y: scroll; position: relative;"> 44 <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> 45 <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> 46 <div class="h-75 text-center"> 47 <h2 class="text-center my-3"> Lyrics </h2> 48 <p class="w-75 m-auto" style="font-size: 1.5rem"> 49 We just wanna party<br> 50 Party just for you<br> 51 We just want the money<br> 52 Money just for you<br> 53 I know you wanna party<br> 54 Party just for me<br> 55 Girl, you got me dancin' (yeah, girl, you got me dancin')<br> 56 Dance and shake the frame<br> 57 We just wanna party (yeah)<br> 58 Party just for you (yeah)<br> 59 We just want the money (yeah)<br> 60 Money just for you (you)<br> 61 I know you wanna party (yeah)<br> 62 Party just for me (yeah)<br> 63 Girl, you got me dancin' (yeah, girl, you got me dancin')<br> 64 Dance and shake the frame (you)<br> 65 This is America<br> 66 Don't catch you slippin' up<br> 67 Don't catch you slippin' up<br> 68 Look what I'm whippin' up<br> 69 This is America (woo)<br> 70 Don't catch you slippin' up<br> 71 Don't catch you slippin' up<br> 72 Look what I'm whippin' up<br> 73 </p> 74 </div> 75 </div> 76 </div> 77 </div> 78 </div> 79 <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> 80 <script src="https://js.pusher.com/4.2/pusher.min.js"></script> 81 </body> 82 </html>
IMPORTANT: 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:
1<script> 2 var app = new Vue({ 3 el: '#app', 4 data: { 5 username: '', 6 member: 0, 7 addedMember: '', 8 removedMember: '', 9 connectedMembers: [] 10 }, 11 12 created() { 13 fetch('/isLoggedIn', { 14 method: 'GET', 15 headers: { 16 'Accept': 'application/json', 17 'Content-Type': 'application/json' 18 } 19 }) 20 .then(res => res.json()) 21 .then(data => { 22 if (data != 'false') { 23 this.username = data.username 24 } else { 25 window.location.replace('/') 26 } 27 }) 28 29 this.subscribe() 30 }, 31 32 methods: { 33 subscribe: function () { 34 const pusher = new Pusher('PUSHER_APP_KEY', { 35 authEndpoint: '/pusher/auth', 36 cluster: 'PUSHER_APP_CLUSTER', 37 encrypted: true 38 }); 39 40 let channel = pusher.subscribe('presence-channel') 41 42 channel.bind('pusher:subscription_succeeded', data => { 43 this.member = data.count 44 data.each(member => this.connectedMembers.push(member.id)) 45 }) 46 47 // Display a notification when a member comes online 48 channel.bind('pusher:member_added', data => { 49 this.member++ 50 this.connectedMembers.push(data.id) 51 this.addedMember = data.id 52 53 this.$refs.added.classList.add('visible') 54 this.$refs.added.classList.remove('invisible') 55 56 window.setTimeout(() => { 57 this.$refs.added.classList.remove('visible'); 58 this.$refs.added.classList.add('invisible'); 59 }, 3000) 60 }); 61 62 // Display a notification when a member goes offline 63 channel.bind('pusher:member_removed', data => { 64 this.member-- 65 let index = this.connectedMembers.indexOf(data.id) 66 67 if (index > -1) { 68 this.connectedMembers.splice(index, 1) 69 } 70 71 this.removedMember = data.id 72 this.$refs.removed.classList.add('visible') 73 this.$refs.removed.classList.remove('invisible') 74 75 window.setTimeout(() => { 76 this.$refs.removed.classList.remove('visible') 77 this.$refs.removed.classList.add('invisible') 78 }, 3000) 79 }) 80 } 81 } 82 }) 83 </script>
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.
We can test the application by compiling down the Go source code and running it with this command:
$ go run presence.go
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:
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.