Server-Side Implementation for Native Frame Streaming
Introduction
When implementing live streaming with the Native Frame SDK, a server-side component plays a crucial role in managing streams, handling authentication, and providing necessary endpoints for the client-side application. This document will explain the key concepts and responsibilities of the server in the context of the Native Frame SDK.
Why You Need Both a Server and a Client
In a Native Frame streaming setup, you need both a server and a client component for several reasons:
- Security: The server component allows you to keep sensitive information, like service account credentials, secure and not exposed to the client.
- Authentication: The server generates short-lived, scoped tokens for broadcasters and viewers, enhancing security.
- Stream Management: The server can create and manage streams, relieving the client of this responsibility.
- API Abstraction: The server can abstract the Native Frame API, providing a simpler interface for the client application.
Service Account JWTs vs User JWTs
- Service Account JWTs: These are long-lived tokens used to authenticate your server with the Native Frame backend. They have broad permissions and should never be exposed to the client.
- User JWTs: These are short-lived tokens generated for specific users (broadcasters or viewers) with limited permissions. These are safe to use on the client-side.
Key Responsibilities of the Server
- Creating and managing streams.
- Generating authentication tokens for broadcasters and viewers.
- Providing API endpoints for the client-side application.
- Securing sensitive information.
Let's explore each of these responsibilities in detail.
Creating and Managing Streams
Before a broadcast can begin, a stream needs to be created. This is typically done on the server-side:
async function createStream() {
const options = {
"streamId": uuid(),
"streamName": "my-fun-stream",
"authKey": "sssh-its-a-secret", // for use with RTMP streaming
"authType": "private",
};
const response = await fetch(`${API_URL}/projects/${PROJECT_ID}/streams`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': 783254439,
'Authorization': `Bearer ${SERVICE_ACCOUNT_JWT}`
},
body: JSON.stringify(options),
});
// Handle response and return streamId
}
Note the x-user-id header provided in stream creation. This is required to provide context of who created this program. Remember that since you are interacting with a third party platform, choose IDs that do not leak Personally Identifiable Information.
This function creates a new stream using the Native Frame API. The SERVICE_ACCOUNT_JWT is used to authenticate the request, ensuring that sensitive credentials are not exposed to the client.
See API docs for Program creation here.
Generating Authentication Tokens
Both broadcasters and viewers need authentication tokens to connect to a stream. These should be generated on the server:
async function generateBroadcasterToken(streamId) {
const options = {
kid: KEY_ID,
videoToken: {
scopes: ["private-broadcaster"],
userId: 783254439,
ttl: 86400, // 24 hours
data: {
displayName: streamId,
mirrors: [
{
id: streamId,
streamName: streamId,
kind: "pipe",
clientEncoder: "SaaS",
streamKey: streamId,
clientReferrer: PROJECT_ID,
},
]
},
},
}
return await fetchVideoToken(options);
}
async function generateViewerToken() {
const options = {
kid: KEY_ID,
videoToken: {
scopes: ["private-viewer"],
userId: 783254439,
ttl: 3600, // 1 hour
data: {
displayName: "bodacious_giraffe",
},
},
}
return await fetchVideoToken(options);
}
async function fetchVideoToken(options) {
try {
const response = await fetch(`${AUTH_URL}/auth/v1/video-jwt`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${SERVICE_ACCOUNT_JWT}`
},
body: JSON.stringify(options),
});
if (response.status !== 201) {
throw new Error("Unable to fetch video jwt");
}
const body = await response.json();
return body.token;
} catch (error) {
console.error("Unable to get video jwt", error);
throw error;
}
}
These functions use the service account JWT and Key ID to generate short-lived, scoped tokens for broadcasters and viewers. The fetchVideoToken function makes the actual call to the Native Frame authentication server to generate the token. This step is crucial as it's where the token is actually created and signed by the Native Frame backend.
As with the x-user-id header above, choose IDs and Display Names that do not leak Personally Identifiable Information.
See API docs for JWT creation for streamers and viewers here.
Constructing the Manifest
For playback, a manifest URL needs to be constructed. This is typically done by pulling stream information and formatting it correctly:
async function getManifestUrl(streamId) {
const response = await fetch(`${API_URL}/projects/${PROJECT_ID}/streams/${streamId}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${SERVICE_ACCOUNT_JWT}`
},
});
const data = await response.json();
if (data && data.manifestUrl) {
return `https://${data.manifestUrl}/live/${streamId}.json`;
}
throw new Error('No manifestUrl found');
}
This function retrieves the stream information and constructs the full manifest URL required for playback.
See API docs for for retrieving stream information here and the manifest request itself here.
Providing API Endpoints
Your server should provide API endpoints for the client-side application. Here are some essential endpoints you might need:
- Create a new stream
- Get active stream(s)
- Get manifest URL for a stream
- Generate broadcaster token
- Generate viewer token
Example Express.js setup:
const express = require('express');
const app = express();
app.post('/api/streams/create', async (req, res) => {
try {
const streamId = await createStream();
res.json({ streamId });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.get('/api/streams/:streamId/manifest', async (req, res) => {
try {
const { streamId } = req.params;
const manifestUrl = await getManifestUrl(streamId);
res.json({ manifestUrl });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/auth/broadcaster', async (req, res) => {
try {
const { streamId } = req.body;
const token = await generateBroadcasterToken(streamId);
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/auth/viewer', async (req, res) => {
try {
const token = await generateViewerToken();
res.json({ token });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
These endpoints allow the client-side application to interact with the Native Frame backend securely, without exposing sensitive credentials.
Conclusion
The server component in a Native Frame streaming setup acts as a crucial intermediary between the client-side application and the Native Frame backend. It handles sensitive operations like stream creation and token generation, ensuring that service account credentials are kept secure. By providing a set of API endpoints, it enables the client-side application to implement streaming and viewing functionality without directly exposing the underlying Native Frame API calls.
This architecture allows for better security, easier management of streams and authentication, and a cleaner separation of concerns between the client and server components of the application. When implementing your own streaming solution with Native Frame, be sure to follow these principles to create a robust and secure system.