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
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:
// 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))
}
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")
}
}
// [...]
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)
}
// [...]
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))
}
// [...]
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>
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>
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>
⚠️ 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>
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
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:
// 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))
}
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")
}
}
// [...]
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)
}
// [...]
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))
}
// [...]
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>
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>
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>
⚠️ 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>
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
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)
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: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.
Nice post! I noticed there seem to be a repetition somehow - "Building the backend" & "Building the dashboard" appearing twice