Some content is only meant for specific audiences. Use content protection to:
- Prevent unauthorized websites from embedding your content;
- Limit your exclusive, premium or confidential content to your target audience only.
In this article, we’ll guide you through creating and applying content protection policies to your mediaclips and channels.
1.0 | What is a Content Protection Policy?
A policy can consist of one or more of the following types of rules:
- Country: specify which countries are allowed or blocked from viewing your content.
- Domain: choose which websites (URLs) are allowed or blocked to play your content.
- IP: restrict video access to specific IP addresses.
- Token: use tokens to provide advanced access control. Tokens are useful when you need more granular control. In contrast to a country, domain or IP policy, the backend of your website/application needs to be able to “generate” a valid token. Read more about generating tokens in the last section of this article (see 5.0 | Advanced – Using Token Secrets).
2.0 | Create a Content Protection Policy
To create a Content Protection policy:
- In the OVP, click “Publication Settings” in the left menu panel;
- Select “Content Protection” and add a new Content Protection Policy;
…
![]()
…
- Name your policy and enter a description. This helps keep track of what the policy is for. The provided name and description will appear throughout the OVP.
2.1 | Create a Rule Set
A rule set determines under what conditions viewers are allowed to watch your content:
- Select a type:
- Country
- Domain
- IP
- Token (advanced use only; to create a rule set for a token, you will need to create a ‘secret’ first: see 5.0 Advanced – Using Token Secrets)
- Configure a rule by using “is” or “is not” statements :
- Is / is any of: use to allow one or more countries, domains, or IPs addresses
- Is not / is none of: use to exclude one or more countries, domains, or IP addresses.
…
Example 1: only allow views from Spain:
…
![]()
…
Example 2: only allow views from www.my-exclusive-channel.com:
…
![]()
…
Example 3: allow views from all countries except from Poland and US:
…
![]()
…
- Combine multiple rules using “and” and “or” conditions:
- And: use to ensure all conditions are met before allowing content to be viewed.
- Or: use to ensure only one condition needs to be true.
…
Example 1: only allow views if the visit is from France and if the visit is on www.my-daily-news.fr.
…
![]()
…
Example 2: allow views if the visit is from France or if the visitor’s IP address is 12.2345.678.910.
…
![]()
2.2 | Blocked Content Display: Media Clip
Choose what viewers see when your content protection policy blocks a media clip:
- Show content, but prevent it from playing
…
![]()
…
![]()
…
- Don’t show content (the background color set in the playout settings will be shown):
![]()
…
![]()
2.3 | Blocked Content Display: Channel
Choose what viewers see when your content protection policy blocks a channel:
- Show content, but prevent it from playing
…
![]()
![]()
…
- Don’t show content: a blank screen will be shown
![]()
…
3.0 | Apply a Content Protection Policy
3.1 | Media clip
To apply a Content Protection Policy to a media clip:
- In the OVP, click “Media Library” in the left menu panel;
- Select “Media clips” and open the “Content Protection” tab in your media clip;
……
![]()
…
- Select a Content Protection Policy in the dropdown menu
The name, description and rules defined in the policy are displayed as a helpful reminder of the content protection settings. Use the shortcut to the policy settings if adjustments are needed.
…
![]()
3.2 | Channel
To apply a Content Protection Policy to a channel:
- In the OVP, click “Media Library” in the left menu panel;
- Select “Channels” and open the “General” tab;
- Scroll down to “Content Protection”
- Select a policy in the dropdown menu:
.
![]()
.
The name, description and rules defined in the policy are displayed as a helpful reminder of the content protection settings. Use the shortcut to the policy settings if adjustments are needed.
4.0 | Test Your Content Protection Policy
Embed your clip to test your policy settings. If the conditions are not met, a rejection is displayed in the player (as set in 3.0 | Apply a Content Protection Policy).
4.1 | Country
![]()
4.2 | Domain
![]()
4.3 | IP Address
![]()
4.4 | Token
![]()
5.0 | Advanced – Using Token Secrets
For advanced use cases, use tokens to authenticate viewers accessing your content. This allows for more granular access control than country, domain, or IP rules alone.
The shared secret itself is not included in the request and is therefore only known to the client and the Blue Billywig platform. This way only authorized websites are able to calculate a token.
- TOTP (RPC Token)
A time-based one-time password that rotates automatically. Used for simple integrations where you only need time-based access control. - JWT
A signed JSON payload with custom claims and expiration. Used for advanced integrations requiring per-user or per-content access control.
- Your backend retrieves the shared secret (stored securely server-side)
- Your backend generates a TOTP or JWT token
- The token is passed to the player via query parameter or HTTP header
- The OVP validates the token against the shared secret
- If valid, video playback is allowed; if invalid, playback is blocked
- A Blue Billywig OVP publication
- Access to the OVP management console (specifically: Publication settings > Secrets and Publication settings > Content protection)
- A Content Protection Policy with a Token rule configured (see Section 5.2).
- A backend server capable of generating tokens (the shared secret must never be exposed client-side)
5.1 | Create a token
To create a token:
- In the OVP sidebar, navigate to Publication settings > Secrets.
- On the Secrets overview page, click the Create new content protection token button.
…
![]()
…
- Fill in the form:
- Label (required): A descriptive name for your secret (e.g., “Production JWT Token”).
- Description (optional): Additional context about the secret’s purpose.
- Expiration settings: Set the token validity duration using the Days, Hours, and Minutes fields. The expiration must be at least 1 minute. After the expiration duration expires, the token becomes invalid and a new token needs to be generated.
- Click Save & Activate to create and immediately activate the secret, or click Save to create it in an inactive state
- After saving, the secret is generated. You can find it in the Secret field on the detail page (click Show secret to reveal the value).…
- The Details panel on the right shows:
- ID: The numeric secret identifier (you will need this for TOTP tokens).
- Secret type: Confirms “Content protection”.
- Label, Created, and Modified metadata.
- Copy the secret value and store it securely in your backend (e.g., in environment variables or a secrets manager).
5.2 | Create a Content Protection Policy with a Token Rule
- In the OVP sidebar, navigate to Publication settings > Content protection.
- On the Content protection policies overview page, click Create new content protection policy.
- Under Policy name, fill in a Title and optional Description.
- Under Select policy type, configure the rules:
- Click the Add rule dropdown and select Token.
- Choose an operator (e.g., is, is not, is empty, is not empty).
- If using is or is not, select the content protection token you created in Section 5.1.
- You can combine multiple rules:
- Click Add rule to add an additional condition within the same rule set (AND logic).
- Click Add rule set to add an alternative set of conditions (OR logic).
- Click Save to create the policy.
Blocked Content Display
When a token is invalid or missing, the policy determines how blocked content appears to viewers:
- Media clips: Either show the player with playback disabled, or hide the content entirely (a background
color is displayed in its place). - Channels: Either show the channel with playback disabled, or hide it completely (a blank screen appears).
These display options are configured when creating the policy (see Section 2.2 – Blocked Content
Display.
Apply the Policy
After creating the policy, assign it to your content:
- Media clips: Navigate to Media library > Media clips, open a clip, go to the
Content Protection tab, and select your policy. - Channels: Navigate to Media library > Channels, open a channel, go to the General tab, scroll
to Content Protection, and select your policy.
5.3 | TOTP Token
Authenticating a request using a TOTP token requires two elements:
- The ID of the Secret
- The generated token itself
TOTP (Time-based One-Time Password) tokens use the [RFC 6238] (https://datatracker.ietf.org/doc/html/rfc6238) standard. The token value rotates automatically based on the configured time step (expiration window).
Token Format
The generated token must use a time-step duration matching the secret’s expiration time in seconds. Append the generated token to the ID with a hyphen:
{SECRET_ID}-{GENERATED_TOTP_CODE}For example: 42-839205 Include this string in the rpctoken header or query parameter.
Generating a TOTP Token
Node.js (using `speakeasy`)
const speakeasy = require("speakeasy");
const SECRET_ID = "42";
const SHARED_SECRET = "your-shared-secret";
const EXPIRATION = 86400; // 24 hours in seconds (must match OVP setting)
function generateRpcToken() {
const totp = speakeasy.totp({ secret: SHARED_SECRET, digits: 6, step: EXPIRATION });
return;
`${SECRET_ID}-${totp}`;
}
const token = generateRpcToken(); // e.g. "42-839205"
Python (using `pyotp`)
import pyotp
import base64
SECRET_ID = "42"
SHARED_SECRET = "your-shared-secret"
EXPIRATION = 86400 # 24 hours in seconds
def generate_rpc_token(): # pyotp expects a base32-encoded secret
secret_b32 = base64.b32encode(SHARED_SECRET.encode()).decode()
totp = pyotp.TOTP(secret_b32, digits=6, interval=EXPIRATION)
code = totp.now()
return f"{SECRET_ID}-{code}"
token = generate_rpc_token() # e.g. "42-839205"/PHP (using `OTPHP`)
<?php use OTPHP\TOTP; $secretId = '42'; $sharedSecret = 'your-shared-secret'; $expiration = 86400; // 24 hours function generateRpcToken(string $secretId, string $secret, int $expiration): string { $totp = TOTP::createFromSecret($secret); $totp->setPeriod($expiration);
$totp->setDigits(6);
$code = $totp->now();
return "{$secretId}-{$code}";
}
$token = generateRpcToken($secretId, $sharedSecret, $expiration); // e.g. "42-839205" Passing the TOTP Token
The token can be sent as a query parameter or HTTP header:
- Query parameter (appended to the embed URL): https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205
- HTTP header rpctoken: 42-839205
5.4 | JWT Token
When authenticating a request using a JWT token, sign the payload with the shared secret. JWT tokens offer more flexibility than TOTP: you can include custom claims such as expiration time, subject identifiers, and entity-level access restrictions.
JWT Structure
The JWT must be signed using HMAC SHA-256 (HS256) with the shared secret. The payload should include at minimum:
{
"sub": "user-identifier",
"iat": 1700000000,
"exp": 1700000900
}| Claim | Description | Required |
| sub | Subject identifier (e.g., user ID or session ID) | Recommended |
| iat | Issued-at timestamp (Unix epoch seconds) | Recommended |
| exp | Expiration timestamp (Unix epoch seconds) | Yes |
| entity_access | Entity-level access restrictions (see Section 5.6) | Optional |
Generating a JWT Token
Node.js (using `jsonwebtoken`)
const jwt = require("jsonwebtoken");
const SHARED_SECRET = "your-shared-secret";
function generateJwtToken(userId) {
const now = Math.floor(Date.now() / 1000);
const payload = { sub: userId, iat: now, exp: now + 900 }; // 15 minutes
return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
}
const token = generateJwtToken("user-123");
Python (using `pyotp`)
import jwt
import time
SHARED_SECRET = "your-shared-secret"
def generate_jwt_token(user_id: str) -> str:
now = int(time.time())
payload = {
"sub": user_id,
"iat": now,
"exp": now + 900, # 15 minutes
}
return jwt.encode(payload, SHARED_SECRET, algorithm="HS256")
token = generate_jwt_token("user-123")
PHP (using `OTPHP`)
<?php use Firebase\JWT\JWT; $sharedSecret = 'your-shared-secret'; function generateJwtToken(string $userId): string { global $sharedSecret; $now = time(); $payload = [ 'sub' => $userId,
'iat' => $now,
'exp' => $now + 900, // 15 minutes
];
return JWT::encode($payload, $sharedSecret, 'HS256');
}
$token = generateJwtToken('user-123');
Passing the JWT Token
- Query parameter (appended to the embed URL):
https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi… - HTTP header jwt: eyJhbGciOi…
- Authorization Bearer header Authorization: Bearer eyJhbGciOi…
5.5 | Embedding with Tokens
Append the token as a query parameter to the .js embed URL:
<!-- TOTP -->
<script
type="text/javascript"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?rpctoken=42-839205"
async="true"
></script>
<!-- JWT -->
<script
type="text/javascript"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.js?jwt=eyJhbGciOi..."
async="true"
></script>iFrame Embed
Append the token to the .html embed URL in the src attribute:
<iframe
onload="
this.src +=
'#!referrer=' + encodeURIComponent(location.href) + '&realReferrer=' + encodeURIComponent(document.referrer)"
src="https://{publication}.bbvms.com/p/{playout}/c/{clipId}.html?inheritDimensions=true&jwt=eyJhbGciOi..."
width="720"
height="405"
frameborder="0"
allowfullscreen
allow="autoplay; fullscreen"
></iframe>Server-Side Token Injection
For maximum security, have your backend generate the token and inject it into the embed URL so that the token never appears in static HTML source code:
// Express.js example — renders a page with the token embedded in the player URL
const jwt = require("jsonwebtoken");
app.get("/video/:clipId", (req, res) => {
const token = generateJwtToken(req.user.id);
const embedUrl = `https://${PUBLICATION}.bbvms.com/p/${PLAYOUT}/c/${req.params.clipId}.js?jwt=${token}`;
res.send(`
<html>
<body>
<script type="text/javascript" src="${embedUrl}" async="true"></script>
</body>
</html>
`);
});…
5.6 | Entity-Level Access Control (JWT)
JWT tokens support fine-grained access control through the entity_access claim. This allows you to restrict a token to specific media clips, clip lists, or projects.
Claim Structure
The entity_access claim is an object where keys are entity types and values are arrays of allowed entity IDs:
{
"sub": "user-123",
"iat": 1700000000,
"exp": 1700000900,
"entity_access": { "mediaclip": ["12345", "67890"], "mediacliplist": ["101"], "project": ["42"] }
}| Entity Type | Description |
| mediaclip | Individual video clips |
| mediacliplist | Clip lists / playlists |
| project | Projects |
Behavior
- If the entity_access claim is omitted or empty, the token grants access to all content protected by the policy (no entity-level restriction).
- If entity_access is present, the OVP checks whether the requested content matches one of the allowed entities. Access is granted if any entity type/ID pair matches.
- An empty instances array (“mediaclip“: []) matches all instances of that entity type.
Example: Restricting to a Single Clip
const jwt = require("jsonwebtoken");
function generateClipToken(userId, clipId) {
const now = Math.floor(Date.now() / 1000);
const payload = { sub: userId, iat: now, exp: now + 900, entity_access: { mediaclip: [String(clipId)] } };
return jwt.sign(payload, SHARED_SECRET, { algorithm: "HS256" });
} // This token only grants access to clip 12345
const token = generateClipToken("user-123", 12345);