Build a streaming platform and watch videos with your friends
A basic understanding of Go and JavaScript (Vue) would help you with this tutorial. However, it is not a requirement as this is a step-by-step guide.
When building applications that allow multiple users to interact with one another, it is essential to display their online presence. That way each user gets an idea of how many other users are currently online.
Presence channels are a great solution to keep track of who is subscribed to a channel and notify channel members when someone joins or leaves. However, Presence channels are limited to 100 members. What if you have more users than that?
Instead of Presence channels, you can use Subscription Counting events to get updates about the number of people watching a video stream.
Now, what if end users want to know more than just the number of users watching a stream? With the Watchlist Online Status feature, you can build the presence functionality at the app level. This means end users will get notified when the people they follow are online.
In this tutorial, we explain how to build a live streaming application that displays the online status of users you are interested in and who are currently streaming a video.
Here’s a demo of the final application.
Prerequisites
Before you can start implementing this tutorial, you need the following:
- An IDE of your choice, e.g., Visual Studio Code.
- Go (version >= 0.10.x) installed on your computer. Check out the Installation guide.
- JavaScript (Vue) installed on your computer. Go to the Installation guide
Features used
Implementation steps
Here’s how to build a streaming platform using Pusher Channels.
Step 1: Set up Channels
This section exlains how to set up Pusher Channels.
Create an app
- If you don’t have a Pusher account, sign up for free. If you are already familiar with Channels and have an account, skip this step and sign in.
NOTE: The Pusher sandbox plan is free and fully featured. It comes with 200,000 messages per day and 100 concurrent connections (total number of simultaneous connections across your app). This plan is more than sufficient for this tutorial app.
-
Navigate to Channels > Create app.
-
Configure your app by proving the following information:
- App name - Name your app. This is just for your reference and you can change this name later.
- Cluster - The physical location of the servers that handle requests from your Channels app. You cannot change the cluster afterward. Read more about Cluster configuration.
- Choose a tech stack you intend to integrate Pusher with for a better setup experience (optional).
4. Click Create app to proceed.
Step 2: Enable Watchlist events and Subscription Count features
To build this particular use case, we’ve mentioned we are using Subscription Counting, and Watchlist events. To use them, you have to enable them both in the Pusher dashboard.
To enable these features, navigate to the channel app you are currently building, go to App Settings, and toggle the Enable subscription counting and Enable Watchlist Events options.
Step 3: Get App Keys
To use Channels, you need app keys for your application. Go to App Keys and copy and save these keys for later.
Step 4: Set up the codebase
We will build the backend server in Go.
- Create a new project directory, let’s call this directory
streaming-video-with-watchlist
.
$ mkdir streaming-video-with-watchlist
$ cd streaming-video-with-watchlist
- Save Pusher App credentials and create a new
.env
file.
touch .env
- Paste in the following contents to the newly created file, and update values with your app credentials obtained from the dashboard. Refer back to Step 3 - Get App Keys.
PUSHER_APP_ID="YOUR_PUSHER_APP_ID"
PUSHER_APP_KEY="YOUR_PUSHER_APP_KEY"
PUSHER_APP_SECRET="YOUR_PUSHER_APP_SECRET"
PUSHER_APP_CLUSTER="YOUR_PUSHER_APP_CLUSTER"
- Create a new Go file and call it server.go, this file will be where our entire backend server logic will be.
$ touch server.go
- Pull in the latest Go Pusher and godotenv packages with these commands:
$ go mod init
$ go get github.com/pusher/pusher-http-go/v5
$ go get github.com/joho/godotenv
Step 5: Building the backend
Open the server.go file and paste in the following contents to the newly created file:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"github.com/joho/godotenv"
"github.com/pusher/pusher-http-go/v5"
)
var teams = map[string][]string{
"group-Dave": {"Dave", "Lisa", "Phil", "Britney", "Phil"},
"group-Lora": {"Lora", "Frank", "Peter", "Janine"},
}
type user struct {
Username string `json:"username" xml:"username" from:"username" query:"username"
Email string `json:"email" xml:"email" form:"email" query:"email"
}
var loggedInUser user
var client = pusher.Client{}
func main() {
err := godotenv.Load(".env")
if err != nil {
log.Fatal("Error loading .env file")
}
client = pusher.Client{
AppID: os.Getenv("PUSHER_APP_ID")
Key: os.Getenv("PUSHER_APP_KEY")
Secret: os.Getenv("PUSHER_APP_SECRET"),
Cluster: os.Getenv("PUSHER_APP_CLUSTER"),
}
http.Handle("/", http.FileServer(http.Dir("./static")))
http.HandleFunc("/isLoggedIn", isUserLoggedIn)
http.HandleFunc("/new/user", NewUser)
http.HandleFunc("/pusher/user-auth", pusherUserAuth)
log.Fatal(http.ListenAndServe(":8090", nil))
}
The code above:
- Imports all the packages that are required for the application to work, including Pusher.
- Instantiates the Pusher client that we will use to authenticate users from the client side. It uses credentials we have saved in the .env file recently.
- Defines a user struct and gives it two properties—username and email—so that the app knows how to handle incoming payloads and correctly bind it to a user info object.
- Creates the global struct to store user names and their friends’ list. This is a very naive implementation of observing group of people logic.
Note: For optimal use of this feature, a watchlist should be short. Otherwise, the user may be bombarded with events about many users going online and offline.
- Creates 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.
The main function registers four endpoints:
/
— Loads all the static files from the static directory./isLoggedIn
— Checks if a user is logged in and returns a fitting message./new/user
— Connects a new user and initializes the global user instance./pusher/user-auth
— Pusher user authentication endpoint.
- In the same file, just above the main function, add the code for the handlers function of the
/isLoggedIn
and/new/user
endpoints:
func isUserLoggedIn(rw http.ResponseWriter, req *http.Request) {
if loggedInUser.Username != "" && loggedInUser.Email != "" {
json.NewEncoder(rw).Encode(loggedInUser)
} else {
json.NewEncoder(rw).Encode("false")
}
}
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)
}
To decide who joins the streaming, we want to collect some data about the user. The app receives a new user’s details in a POST request and binds it to an instance of the user struct. We further use this user instance to check if a user is logged in or not.
Since your servers are the authority on who your users are, and what they can access, Pusher client libraries can make calls to endpoints of your choice to supply signed authentication tokens for the bearing user.
The user object passed to the authenticateUser
method includes:
- ID – A non-empty string is required
- Email - Optional field
- Watchlist - An array of user IDs. These user IDs represent a circle of interest (e.g., friends or followers) whose online/offline presence will be exposed to the user.
- As the last step of backend-side implementation, let’s add the code for the
/pusher/user-auth
endpoint:
func pusherUserAuth(res http.ResponseWriter, req *http.Request) {
params, _ := ioutil.ReadAll(req.Body)
userData := map[string]interface{}{
"id": loggedInUser.Username,
"email": loggedInUser.Email,
"watchlist": getUserWatchlist(loggedInUser.Username),
}
response, err := client.AuthenticateUser(params, userData)
if err != nil {
panic(err)
}
fmt.Fprintf(res, string(response))
}
func getUserWatchlist(userName string) []string {
for key := range teams {
for _, v := range teams[key] {
if v == userName {
return teams[key]
}
}
}
return []string{}
}
Pusher authenticates a user once per connection session. Authenticating a user gives your application access to user-based features in Pusher Channels.
For example, for sending events to a user based on user id or enables you to build presence functionality at the app level via Online Status events.
Step 6: Building the frontend
- In the root directory of the project, create a
static
folder and enter it by using the commands:
$ mkdir static
$ cd static
- Create two files in the directory,
index.html
anddashboard.html
.
$ touch index.html
$ touch dashboard.html
Set up the connection page
-
In the
index.html
file, we will write the HTML code that allows users to connect to the application using their name and email. -
Open the
index.html
file, copy the code from here, and paste it in (take your time to go through it before continuing).
Note: Video is an embed from YouTube and may not play depending on your region.
The script you just pasted above submits user data to the backend Go server and navigates the browser’s location to the dashboard’s URL.
Now it’s high time to build the dashboard!
Set up the dashboard
- Open the
dashboard.html
file and like before, copy the source code from here and paste it in (take your time to go through it before continuing).
Note: Replace the YOUR__PUSHER_APP_* values with your app credentials obtained from the dashboard. Refer back to Step 3: Get App Keys.
Also, Video is an embed from YouTube and may not play depending on your region.
Set up Watchlist Online Status and Subscription Counting events
Let’s go through the contents of the <script>
tag in the dashboard.html
snippet you pasted above.
We created some Vue data variables to display reactive updates.
var app = new Vue({
el: '#app',
data: {
username: '',
count: 0,
addedMember: '',
removedMember: '',
connectedMembers: []
},
We also registered a created()
lifecycle hook that checks if a user is connected to the backend server and eligible to view the dashboard before calling the onLogin()
method.
created: function(){
let that = this;
fetch('/isLoggedIn', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(function(res){ return res.json(); })
.then(function(data){
if(data != 'false'){
that.username = data.username;
} else {
window.location.replace( "/" );
}
});
this.onLogin()
},
The onLogin()
method first configures a Pusher instance using the keys provided on the dashboard. Then we subscribe to Watchlist Online Status events. This practically means that when anybody from the user’s watchlist appears online or goes offline your user gets a notification.
methods: {
onLogin: function(){
let that = this
const pusher = new Pusher('YOUR_PUSHER_APP_KEY', {
userAuthentication: {
endpoint: "/pusher/user-auth",
},
cluster: 'YOUR_PUSHER_APP_CLUSTER'
});
pusher.user.watchlist.bind('online', (event) => {
event.user_ids.forEach(member => {
if (that.username !== member) {
that.addedMember = member
that.$refs.added.classList.add('visible');
that.$refs.added.classList.remove('invisible');
setTimeout(function() {
that.$refs.added.classList.remove('visible');
that.$refs.added.classList.add('invisible');
}, 3000)
}
that.connectedMembers.push(member)
});
});
pusher.user.watchlist.bind('offline', (event) => {
event.user_ids.forEach(member => {
if (that.username !== member) {
that.removedMember = member
that.$refs.removed.classList.add('visible');
that.$refs.removed.classList.remove('invisible');
setTimeout(function() {
that.$refs.removed.classList.remove('visible');
that.$refs.removed.classList.add('invisible');
}, 3000)
}
var index = that.connectedMembers.indexOf(member);
if (index > -1) { that.connectedMembers.splice(index, 1) }
});
});
...
- Call
pusher.signin()
to authenticate the user and notify all their followers that the user is online now.
...
pusher.signin()
...
- Subscribe to the public channel and bind to the subscription counting events. So we can track the current number of viewers.
...
let channel = pusher.subscribe('my-channel');
channel.bind('pusher:subscription_count', (data) => {
that.count = data.subscription_count;
});
Step 7: Test the app
Here we are. We are done with building the app!
We can test the application by compiling down the Go source code and running it with this command from the root of the project:
$ cd ../
$ go mod tidy
$ go run server.go
The app should be running now and you can access it through http://127.0.0.1:8090.
Here’s a display of how the application should look:
See it all in action
Check out the GitHub repo for this project to see the demo code altogether.
Conclusion
In this tutorial, we have learned how to leverage Pusher to build a realtime application where users can see which of their friends are online, and see how many users are watching the live stream.
This demo app gives you an idea of how you can build realtime apps with Pusher Channels and provide a great user experience.
The source code for this tutorial is available on GitHub.
Pro tips
Our tutorials serve to showcase what can be done with our developer APIs. To further inspire you, here are a few ways how you can level up this use case!
- Implement actual logic for user watchlist creation.
- Add the ability to chat with other viewers.
- Add the ability to send messages to particular users.
Sign up for a Pusher account and get started with this tutorial.
7 December 2022
by Agata Walukiewicz