Skip to content

Secure video playback

A public video can be viewed by anyone who has its id. To give access only to certain users, mark the video private: it can then no longer be reached publicly, and viewers need a signed URL token to watch or download it.

Public MP4 download

This page covers private videos. For public videos you do not need to mint anything: the MP4 rendition's static_mp4_link is already signed for you and lasts about a month, so it can go straight behind a download button; see MP4 downloads.

Typical uses for signed URLs:

  • Restricting playback to logged-in members
  • Granting access for a limited time (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

Signing keys sign the JWT tokens that grant access to private videos.

Manage them from the Signing keys ⧉ API endpoints.

Note

Signing keys are different from API keys.

3. Generate JWT tokens

You generate tokens in your own application with an open-source JWT library, so there is no HeapStream call each time you need one.

Every token must meet these requirements (the code examples below satisfy them all):

  • Sign tokens with RS256.
  • Set the JWT kid header to the signing key's id, so HeapStream knows which key verifies the token.
  • The API returns the signing key's private key base64-encoded; decode it before signing.
  • Keep tokens short-lived: set exp to the shortest lifetime that works for your use case (a few hours, say).
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

These URLs accept the JWT token. Each 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 opens the video page in full screen:

https://staging.heapstream.com/et/<project_id>/?token=<jwt_token>

Embed it in an iframe:

<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

The master HLS playlist:

https://staging.heapstream.com/mmpl/<project_id>/?token=<jwt_token>

Poster URL

https://staging.heapstream.com/pstv/<project_id>/<poster_id>/?token=<jwt_token>

Text track HLS playlist

https://staging.heapstream.com/mplt/<project_id>/<text_track_id>/?token=<jwt_token>

Preset HLS playlist

https://staging.heapstream.com/mpl/<project_id>/<preset_id>/?token=<jwt_token>

Static MP4 rendition

https://staging.heapstream.com/stcr4/<project_id>/<preset_id>/?token=<jwt_token>

Storyboard VTT playlist

https://staging.heapstream.com/sbt/<project_id>/<storyboard_id>/?token=<jwt_token>

Video original URL

Download the original video upload:

https://staging.heapstream.com/dlo/<project_id>/?token=<jwt_token>

Warning

The project's keep_originals setting must be true (the default) for the download to work.