Secure video playback¶
By default, public videos can be viewed by anyone with just a video id. If you want to give access only to certain users, you have to make the video private and use signed URLs. After you mark a video private, it can no longer be accessed publicly. Instead, the user will need a signed URL token to watch or download the video.
Public MP4 download
This page covers private videos. For public videos, the MP4 rendition is available at a permanent, shareable URL that needs no signed token — see MP4 downloads.
Common use cases for using signed URLs:
- Restricting access to only logged in members
- Allowing access only for a limited time period (e.g., 24 hours)
1. Making videos require signed URLs¶
Only private videos require signed URLs. You can set a video to private either through the GUI or the edit video ⧉ API endpoint.
2. Creating signing keys¶
Next, we'll need to create signing keys. They are used to generate JWT tokens to access private videos.
Signing keys can be managed from the Signing keys ⧉ API endpoints.
Note
Signing keys are different from API keys.
3. Generate JWT tokens¶
Tokens are generated on the client side using open source libraries. This way, you do not need to call a HeapStream endpoint each time you need to generate a token.
Every token must meet these requirements (the code examples below satisfy them all):
- Algorithm — tokens are signed with RS256.
kidheader — set the JWTkidheader to the signing key'sid, so HeapStream knows which key verifies the token.- Private key — the API returns the signing key's private key base64-encoded; decode it before signing.
- Expiry — keep tokens short-lived: set
expto the shortest lifetime that works for your use case (e.g., a few hours).
| Claim code | Description | Value |
|---|---|---|
sub | Subject of the JWT | video.id |
aud | Audience (intended application of the token) | v for access to all endpoints |
exp | Expiration time | UNIX Epoch seconds when the token expires |
nbf | Not Before value | UNIX Epoch seconds before which the token will not work |
Code examples¶
# /// script
# requires-python = ">=3.14"
# dependencies = [
# "pyjwt",
# ]
# ///
import base64
import time
import jwt
video_id = "" # Enter your video id here
signing_key_id = "" # Enter your signing key id here
private_key_base64_encoded_from_api = "" # Enter your base64 encoded private key here
private_key = base64.b64decode(private_key_base64_encoded_from_api)
token = {
"sub": video_id,
"aud": "v",
"exp": int(time.time()) + 3600,
}
headers = {"kid": signing_key_id}
json_web_token = jwt.encode(
token,
private_key,
algorithm="RS256",
headers=headers,
)
print(json_web_token)
package main
import (
"encoding/base64"
"fmt"
"log"
"time"
"github.com/golang-jwt/jwt/v4"
)
func main() {
// Your signing key id
keyId := ""
// Your base64 encoded private key
base64privateKey := ""
// Your video_id
videoId := ""
decodedKey, err := base64.StdEncoding.DecodeString(base64privateKey)
if err != nil {
log.Fatalf("Can not base64 decode private key: %v", err)
}
signKey, err := jwt.ParseRSAPrivateKeyFromPEM(decodedKey)
if err != nil {
log.Fatalf("Can not parse RSA private key: %v", err)
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"sub": videoId,
"aud": "v",
"exp": time.Now().Add(time.Hour * 15).Unix(),
"kid": keyId,
})
tokenString, err := token.SignedString(signKey)
if err != nil {
log.Fatalf("Can not generate token: %v", err)
}
fmt.Println(tokenString)
}
Include the JWT token in the URL¶
A list of URLs that accept the JWT token. Note that each URL names the project in its path — the video is identified by the token itself (its sub claim), so no video id appears in the URL.
Private video page¶
This URL will open the video page in full screen:
You can embed this URL in an iFrame like so:
<iframe src="https://staging.heapstream.com/et/<project_id>/?token=<jwt_token>" width="1280" height="720"
allowFullScreen
style="border: none;"
allow="fullscreen; accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"></iframe>
Add &autoplay=1 to the URL to have the player try to play as soon as it loads, the same as on the public embed URL. Note that browsers block autoplay with sound; pair it with a muted player if you need playback to start reliably.
Add &player_id=<player_id> to render the embed with a specific one of the project's players instead of the project's default. This works on the public embed URL too. The player must belong to the project; an unknown id falls back to the default player.
HLS master playlist URL¶
This URL will point to the master HLS playlist:
Poster URL¶
Text track HLS playlist¶
Preset HLS playlist¶
Static MP4 rendition¶
Storyboard VTT playlist¶
Video original URL¶
Download the original video upload:
Warning
Project keep_originals setting needs to be True (default) for the download to work.