Skip to content

Instantly share code, notes, and snippets.

@FabienLavocat
Created October 23, 2023 17:57
Show Gist options
  • Save FabienLavocat/50aedafbfff4081212e017c0b0e05d55 to your computer and use it in GitHub Desktop.
Save FabienLavocat/50aedafbfff4081212e017c0b0e05d55 to your computer and use it in GitHub Desktop.
Dolby.io - Real-time Streaming - Create Subscribe Tokens
<html>
<head>
<title>Dolby.io Real-time Streaming</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="https://kit.fontawesome.com/914cd69875.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jsrender/jsrender.min.js"></script>
<style>
textarea {
resize: none;
}
</style>
</head>
<body>
<div class="bg-dark text-white px-4 py-4">
<h1>Dolby.io Real-time Streaming</h1>
</div>
<div id="div-form" class="container mt-3">
<div class="alert alert-primary" role="alert">
This utility will create a series of subscribe tokens for the list of users and streams that you provide. Reach out to <a href="mailto:fabien.lavocat@dolby.com">Fabien Lavocat</a> if you have any questions.
</div>
<div class="row mb-3">
<div class="col-3">
<label for="input-account-id" class="form-label">
Dolby.io Account ID
<i class="fa-solid fa-circle-info text-primary" title="In your Dolby.io dashboard, locate your Account ID in the API tab of a publisher token (usually 6 random characters)."></i>
<i class="fa-solid fa-asterisk text-danger" title="Mandatory"></i>
</label>
<input id="input-account-id" type="text" class="form-control" placeholder="a0B1c2">
</div>
<div class="col-9">
<label for="input-secret" class="form-label">
Dolby.io API Secret
<i class="fa-solid fa-circle-info text-primary" title="In your Dolby.io dashboard, locate your API Secret from the Settings section."></i>
<i class="fa-solid fa-asterisk text-danger" title="Mandatory"></i>
</label>
<input id="input-secret" type="text" class="form-control" placeholder="API Secret">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="input-stream-names" class="form-label">
Stream names (one per line)
<i class="fa-solid fa-asterisk text-danger" title="Mandatory"></i>
</label>
<textarea class="form-control" id="input-stream-names" rows="8"></textarea>
</div>
<div class="col-6">
<label for="input-user-names" class="form-label">
User names (one per line)
<i class="fa-solid fa-asterisk text-danger" title="Mandatory"></i>
</label>
<textarea class="form-control" id="input-user-names" rows="8"></textarea>
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<div class="mb-3">
<label for="input-bindIpsOnUsage" class="form-label">Bind IPs On Usage <i class="fa-solid fa-circle-info text-primary" title="If specified will bind the token to the first X IP addresses used with token in requests to Director API, thus restricting the token to those IP addresses without being known beforehand. Mutually exclusive with allowedIpAddresses option."></i></label>
<div class="input-group">
<input type="number" id="input-bindIpsOnUsage" class="form-control" value="0">
<button class="btn btn-outline-primary" type="button" onclick="(0)">0</button>
<button class="btn btn-outline-primary" type="button" onclick="setBindIpsOnUsage(1)">1</button>
<button class="btn btn-outline-primary" type="button" onclick="setBindIpsOnUsage(2)">2</button>
</div>
</div>
<div class="mb-3">
<label for="input-expires" class="form-label">Expires (in seconds)</label>
<div class="input-group">
<input id="input-expires" type="number" class="form-control" onchange="onExpiresChange()" value="31536000">
<button class="btn btn-outline-info" type="button" onclick="addToExpires(- 30 * 24 * 60 * 60)">- 30d</button>
<button class="btn btn-outline-info" type="button" onclick="addToExpires(- 7 * 24 * 60 * 60)">- 7d</button>
<button class="btn btn-outline-info" type="button" onclick="addToExpires(- 24 * 60 * 60)">- 1d</button>
<button class="btn btn-outline-primary" type="button" onclick="addToExpires(24 * 60 * 60)">+ 1d</button>
<button class="btn btn-outline-primary" type="button" onclick="addToExpires(7 * 24 * 60 * 60)">+ 7d</button>
<button class="btn btn-outline-primary" type="button" onclick="addToExpires(30 * 24 * 60 * 60)">+ 30d</button>
</div>
<label id="text-expires" class="form-label mt-2"> - </label>
</div>
</div>
<div class="col-6">
<label for="input-allowedCountries" class="form-label">Allowed Countries (one per line) <i class="fa-solid fa-circle-info text-primary" title="Optional. Specify the ISO 3166-1 two letter country codes to explicitly allow end-users to view from. If the end-user's location does not match any of the specified countries they will be blocked from viewing stream, else they will be allowed to view stream. This geo-fencing rule works in concert with the IP and domain restrictions as well. Specifying geo restriction rules in a token will override account-wide rules."></i></label>
<textarea class="form-control" id="input-allowedCountries" rows="6"></textarea>
</div>
</div>
<div id="actions" class="row">
<div class="col">
<button type="button" class="btn btn-primary btn-lg" onclick="generate()">Generate</button>
<button id="btn-clear" type="button" class="btn btn-secondary btn-lg" onclick="clearForm()">Clear</button>
</div>
</div>
</div>
<div id="div-results" class="container mt-3 list-group list-group-flush d-none">
<div class="alert alert-success" role="alert">
List of subscribe tokens created:
</div>
</div>
<script>
const inputAccountId = document.getElementById('input-account-id');
const inputSecret = document.getElementById('input-secret');
const inputStreamNames = document.getElementById('input-stream-names');
const inputUserNames = document.getElementById('input-user-names');
const inputBindIpsOnUsage = document.getElementById('input-bindIpsOnUsage');
const inputAllowedCountries = document.getElementById('input-allowedCountries');
const inputExpires = document.getElementById('input-expires');
const divResults = document.getElementById('div-results');
function onExpiresChange() {
const txtExpires = document.getElementById('text-expires');
try {
const value = parseInt(inputExpires.value);
const newTime = new Date(new Date().getTime() + (value * 1000));
txtExpires.innerHTML = newTime.toString();
} catch (error) {
console.error(error);
txtExpires.innerHTML = '';
}
}
function addToExpires(seconds) {
console.log('addToExpires', seconds);
try {
const value = parseInt(inputExpires.value) + seconds;
inputExpires.value = value > 0 ? value : 0;
} catch (error) {
inputExpires.value = seconds;
}
onExpiresChange();
}
function setBindIpsOnUsage(value) {
inputBindIpsOnUsage.value = value;
}
function loadConfigurationFromLocalStorage() {
inputAccountId.value = localStorage.getItem('accountId') ?? '';
inputStreamNames.value = localStorage.getItem('streamNames') ?? '';
inputUserNames.value = localStorage.getItem('userNames') ?? '';
inputBindIpsOnUsage.value = localStorage.getItem('bindIpsOnUsage') ?? 0;
inputAllowedCountries.value = localStorage.getItem('allowedCountries') ?? '';
inputExpires.value = localStorage.getItem('expires') ?? 31536000;
}
function saveConfigurationToLocalStorage() {
localStorage.setItem('accountId', inputAccountId.value);
localStorage.setItem('streamNames', inputStreamNames.value);
localStorage.setItem('userNames', inputUserNames.value);
localStorage.setItem('bindIpsOnUsage', inputBindIpsOnUsage.value);
localStorage.setItem('allowedCountries', inputAllowedCountries.value);
localStorage.setItem('expires', inputExpires.value);
}
function displayError(errorMessage) {
const template = jsrender.templates('<div class="alert alert-danger" role="alert">{{:error}}</div>');
const frag = document.createDocumentFragment();
const temp = document.createElement('div');
temp.innerHTML = template.render({error: errorMessage});
frag.appendChild(temp.firstChild);
const elem = document.getElementById("div-results");
elem.appendChild(frag);
}
async function createSubscribeToken(userName, streamNames) {
console.group('Subscribe token');
console.log(`Username: ${userName}`);
const label = `Subscriber - ${userName}`;
console.log(`Label: ${label}`);
console.log('Stream names', streamNames);
console.groupEnd();
const tokenRequest = {
label: label,
streams: streamNames.map(sn => {
return {
isRegex: false,
streamName: sn
};
}),
};
const bindIpsOnUsage = parseInt(inputBindIpsOnUsage.value);
if (!isNaN(bindIpsOnUsage)) {
tokenRequest.bindIpsOnUsage = bindIpsOnUsage;
}
const allowedCountries = inputAllowedCountries.value.split('\n').filter(o=>o);
if (allowedCountries.length) {
tokenRequest.allowedCountries = allowedCountries;
}
const expires = parseInt(inputExpires.value);
if (!isNaN(expires) && expires > 0) {
tokenRequest.expires = expires;
}
const accountId = inputAccountId.value;
const apiSecret = inputSecret.value;
// Prefix with try.readme.io to avoir CORS errors
const url = 'https://try.readme.io/https://api.millicast.com/api/subscribe_token';
const options = {
method: 'POST',
headers: {
accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${apiSecret}`,
},
body: JSON.stringify(tokenRequest),
};
const fetchResponse = await fetch(url, options);
if (!fetchResponse.ok) {
displayError(`${label} -> HTTP ${fetchResponse.status} - ${fetchResponse.statusText}`);
return;
}
const response = await fetchResponse.json();
const token = response.data;
const template = jsrender.templates({
markup: `
<div class="list-group-item d-flex gap-3 py-3" aria-current="true">
<i class="fa-solid fa-user-shield fa-xl mt-3 text-success"></i>
<div class="w-100 justify-content-between">
<h6 class="ms-2"><b>Label:</b> {{:label}}</h6>
<h6 class="ms-2"><b>Subscribe Token:</b> {{:token}}</h6>
<table class="table mb-0">
<thead>
<tr>
<th scope="col">Stream name</th>
<th scope="col">Viewer URL</th>
</tr>
</thead>
<tbody>
{{for streams}}
<tr>
{{* streamName = encodeURI(data.streamName); }}
<td>{{:streamName}}</td>
<td>https://viewer.millicast.com?streamId=${accountId}/{{*:streamName}}&token={{:~root.token}}</td>
</tr>
{{/for}}
</tbody>
</table>
</div>
</div>
`,
allowCode: true
});
const frag = document.createDocumentFragment();
const temp = document.createElement('div');
temp.innerHTML = template.render(token);
frag.appendChild(temp.firstChild);
const elem = document.getElementById("div-results");
elem.appendChild(temp.firstChild);
return token;
}
async function generate() {
document.getElementById('actions').remove();
divResults.classList.remove('d-none');
// Disable the input elements
const inputs = [...document.getElementsByTagName('input'), ...document.getElementsByTagName('textarea')];
for (let i = 0; i < inputs.length; i++) {
inputs[i].disabled = true;
}
saveConfigurationToLocalStorage();
const streamNames = inputStreamNames.value.split('\n').filter(o=>o);
const userNames = inputUserNames.value.split('\n').filter(o=>o);
try {
for (let i = 0; i < userNames.length; i++) {
const userName = userNames[i];
await createSubscribeToken(userName, streamNames);
}
} catch (error) {
console.error(error);
displayError(error.message);
}
}
function clearForm() {
inputStreamNames.value = '';
inputUserNames.value = '';
inputBindIpsOnUsage.value = 0;
inputAllowedCountries.value = '';
inputExpires.value = 31536000;
}
loadConfigurationFromLocalStorage();
onExpiresChange();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment