Historically, building video applications has been very difficult. There's a lot of complicated tech behind recording, encoding, and playing videos. Luckily, Cloudflare Stream abstracts all the difficult parts away, so you can build custom video and streaming applications easily. Let's look at how we can combine Cloudflare Stream, Access, Pages, and Workers to create a high-performance video application with very little code.
Today, we’re going to build a video application inspired by Cloudflare TV. We’ll have user authentication and the ability for administrators to upload recorded videos or livestream new content. Think about being able to build your own YouTube or Twitch using Cloudflare services!
Fetching a list of videos
On the main page of our application, we want to display a list of all videos. The videos are uploaded and stored with Cloudflare Stream, but more on that later! This code could be changed to display only the "trending" videos or a selection of videos chosen for each user. For now, we'll use the search API and pass in an empty string to return all.
import { getSignedStreamId } from "../../src/cfStream"
export async function onRequestGet(context) {
const {
request,
env,
params,
} = context
const { id } = params
if (id) {
const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream/${id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${env.CF_API_TOKEN_STREAM}`
}
})
const video = (await res.json()).result
if (video.meta.visibility !== "public") {
return new Response(null, {status: 401})
}
const signedId = await getSignedStreamId(id, env.CF_STREAM_SIGNING_KEY)
return new Response(JSON.stringify({
signedId: `${signedId}`
}), {
headers: {
"content-type": "application/json"
}
})
} else {
const url = new URL(request.url)
const res = await (await fetch(`https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/stream?search=${url.searchParams.get("search") || ""}`, {
headers: {
"Authorization": `Bearer ${env.CF_API_TOKEN_STREAM}`
}
})).json()
const filteredVideos = res.result.filter(x => x.meta.visibility === "public")
const videos = await Promise.all(filteredVideos.map(async x => {
const signedId = await getSignedStreamId(x.uid, env.CF_STREAM_SIGNING_KEY)
return {
uid: x.uid,
status: x.status,
thumbnail: `https://videodelivery.net/${signedId}/thumbnails/thumbnail.jpg`,
meta: {
name: x.meta.name
},
created: x.created,
modified: x.modified,
duration: x.duration,
}
}))
return new Response(JSON.stringify(videos), {headers: {"content-type": "application/json"}})
}
}
We'll go through each video, filter out any private videos, and pull out the metadata we need, such as the thumbnail URL, ID, and created date.
Playing the videos
To allow users to play videos from your application, they need to be public, or you'll have to sign each request. Marking your videos as public makes this process easier. However, there are many reasons you might want to control access to your videos. If you want users to log in before they play them or the ability to limit access in any way, mark them as private and use signed URLs to control access. You can find more information about securing your videos here.
If you are testing your application locally or expect to have fewer than 10,000 requests per day, you can call the /token endpoint to generate a signed token. If you expect more than 10,000 requests per day, sign your own tokens as we do here using JSON Web Tokens.
Allowing users to upload videos
The next step is to build out an admin page where users can upload their videos. You can find documentation on allowing user uploads here.
This process is made easy with the Cloudflare Stream API. You use your API token and account ID to generate a unique, one-time upload URL. Just make sure your token has the Stream:Edit permission. We hook into all POST requests from our application and return the generated upload URL.
export const cfTeamsAccessAuthMiddleware = async ({request, data, env, next}) => {
try {
const userEmail = request.headers.get("cf-access-authenticated-user-email")
if (!userEmail) {
throw new Error("User not found, make sure application is behind Cloudflare Access")
}
// Pass user info to next handlers
data.user = {
email: userEmail
}
return next()
} catch (e) {
return new Response(e.toString(), {status: 401})
}
}
export const onRequest = [
cfTeamsAccessAuthMiddleware
]
The admin page contains a form allowing users to drag and drop or upload videos from their computers. When a logged-in user hits submit on the upload form, the application generates a unique URL and then posts the FormData to it. This code would work well for building a video sharing site or with any application that allows user-generated content.
async function getOneTimeUploadUrl() {
const res = await fetch('/api/admin/videos', {method: 'POST', headers: {'accept': 'application/json'}})
const upload = await res.json()
return upload.uploadURL
}
async function uploadVideo() {
const videoInput = document.getElementById("video");
const oneTimeUploadUrl = await getOneTimeUploadUrl();
const video = videoInput.files[0];
const formData = new FormData();
formData.append("file", video);
const uploadResult = await fetch(oneTimeUploadUrl, {
method: "POST",
body: formData,
})
}
Adding real time video with Stream Live
You can add a livestreaming section to your application as well, using Stream Live in conjunction with the techniques we've already covered. You could allow logged-in users to start a broadcast and then allow other logged-in users, or even the public, to watch it in real-time! The streams will automatically save to your account, so they can be viewed immediately after the broadcast finishes in the main section of your application.
Securing our app with middleware
We put all authenticated pages behind this middleware function. It checks the request headers to make sure the user is sending a valid authenticated user email.
export const cfTeamsAccessAuthMiddleware = async ({request, data, env, next}) => {
try {
const userEmail = request.headers.get("cf-access-authenticated-user-email")
if (!userEmail) {
throw new Error("User not found, make sure application is behind Cloudflare Access")
}
// Pass user info to next handlers
data.user = {
email: userEmail
}
return next()
} catch (e) {
return new Response(e.toString(), {status: 401})
}
}
export const onRequest = [
cfTeamsAccessAuthMiddleware
]
Putting it all together with Pages
We have Cloudflare Access controlling our log-in flow. We use the Stream APIs to manage uploading, displaying, and watching videos. We use Workers for managing fetch requests and handling API calls. Now it’s time to tie it all together using Cloudflare Pages!
Pages provides an easy way to deploy and host static websites. But now, Pages seamlessly integrates with the Workers platform (link to announcement post). With this new integration, we can deploy this entire application with a single, readable repository.
Controlling access
Some applications are better public; others contain sensitive data and should be restricted to specific users. The main page is public for this application, and we've used Cloudflare Access to limit the admin page to employees. You could just as easily use Access to protect the entire application if you’re building an internal learning service or even if you want to beta launch a new site!
When a user clicks the admin link on our demo site, they will be prompted for an email address. If they enter a valid Cloudflare email, the application will send them an access code. Otherwise, they won't be able to access that page.
Check out the source code and get started building your own video application today!