Develop an overlay
Once you become familiar with the concept of overlay, you will probably want to develop your own. This page therefore serves as a quick "tutorial" so that you can master the main blocks constituting the creation of an overlay capable of receiving and emitting events but also using the OBS JavaScript API.
To find out how to host your overlay, go to the dedicated page.
Skeleton
The basic skeleton of a Multistream Tools overlay is as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom overlay example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<style>
/* Your own CSS code goes here. */
</style>
</head>
<body>
<main id="logContainer"></main>
<script type="module">
import {} from 'https://obs.multistream.tools/v1/overlay.js';
// Your own JavaScript code goes here.
</script>
</body>
</html>
The <meta name="robots" content="noindex,nofollow"/>
line prevents the indexing of your overlay by the various search engines.
This skeleton uses our CSS framework but, although recommended, this is not mandatory.
You can link as many external JS and CSS files/libraries as you want, just keep in mind that the more you have, the slower your overlay may load. This is why this skeleton directly integrates your JavaScript and CSS code, thus avoiding unnecessary HTTP requests.
Consider modifying the lang
attribute of the html
tag as well as the content of the title
tag to match your needs. You can also take advantage of our internationalization system, see below.
We strongly recommend that you implement the usual security measures on your page such as the Content Security Policy, especially when running third-party code.
Styles
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom overlay example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<style>
:root {
--fontSize: 40px;
--containerBackgroundColor: red;
}
#logContainer {
background-color: var(--containerBackgroundColor);
white-space: pre;
}
</style>
</head>
<body>
<main id="logContainer"></main>
<script type="module">
import { loadFonts } from 'https://obs.multistream.tools/v1/overlay.js';
// Optional.
loadFonts('Tangerine');
// Your own JavaScript code goes here.
</script>
</body>
</html>
Use CSS variables to stylish your overlay while making it easier to create themes applicable on the latter. The example above shows a change in the overall text size of the page as well as the assignment of the red color to the background of the block which will display events received later.
Name your variables consistently, predictably and logically by using, for example, prefixes and/or suffixes to group them by category.
You can optionally load default fonts using the loadFonts
function exposed by our JavaScript framework.
Everything is possible, give free rein to your imagination!
Parameters
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom overlay example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<style>
:root {
--fontSize: 40px;
--containerBackgroundColor: red;
}
#logContainer {
background-color: var(--containerBackgroundColor);
white-space: pre;
}
</style>
</head>
<body>
<main id="logContainer"></main>
<script type="module">
import {
loadFonts,
parameters,
validateNumber,
Queue
} from 'https://obs.multistream.tools/v1/overlay.js';
// Optional.
loadFonts('Tangerine');
let { delay } = parameters;
delay = validateNumber(delay, 5);
const queue = new Queue(delay);
</script>
</body>
</html>
Use parameters to make your overlay easily configurable for use, without having to modify its source code. Here we define a custom parameter named delay
corresponding to a number of seconds (5
by default) whose value will be used to initialize a queue (instance of the Queue
class) which will subsequently allow us to display events received for a certain time.
Name your parameters appropriately, taking care that they do not conflict with those part of the system.
Events
The core of our system is based on receiving and sending events; to do this we will respectively use the Observer
and Emitter
classes of our JavaScript framework.
Reception
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom overlay example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<style>
:root {
--fontSize: 40px;
--containerBackgroundColor: red;
}
#logContainer {
background-color: var(--containerBackgroundColor);
white-space: pre;
}
</style>
</head>
<body>
<main id="logContainer"></main>
<script type="module">
import {
loadFonts,
parameters,
validateNumber,
Queue,
setText,
stringify,
removeChildren,
Observer
} from 'https://obs.multistream.tools/v1/overlay.js';
// Optional.
loadFonts('Tangerine');
let { duration } = parameters;
duration = validateNumber(duration, 5);
const queue = new Queue(duration);
const { logContainer } = window;
const log = data => setText(stringify(data, null, ' '), logContainer);
const clear = () => removeChildren(main);
const addToQueue = data => queue.add(() => log(data), clear);
const observer = new Observer({
load: addToQueue,
session: addToQueue,
alerts: addToQueue,
chat: addToQueue,
other: addToQueue
});
</script>
</body>
</html>
We define here:
- a utility function, named
log
, responsible for replacing the content of themain
tag with an indented textual representation of a JSON object - a utility function, named
clear
, responsible for destroying the content of themain
tag - a utility function, named
addToQueue
, responsible for adding the display and destruction of a log to the previously created queue
Then we observe the reception of all events - coming from adapters - of category load, session, alerts, chat and other by calling the addToQueue
function each time. This will have the effect of displaying the data linked to these events on screen only for the desired period of time. The pause and skip events, emitted by our dock, are automatically observed in order to act on the execution of the queue.
Emission
Start by creating a new JavaScript file, named for example emitter.js, in the same directory as the HTML page of your overlay. It will contain an emitter and the name of a custom event:
import { Emitter } from 'https://obs.multistream.tools/v1/overlay.js';
export const customEvent = 'customEvent';
export const emitter = new Emitter();
Then modify your overlay to use these:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom overlay example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<style>
:root {
--fontSize: 40px;
--containerBackgroundColor: red;
}
#logContainer {
background-color: var(--containerBackgroundColor);
white-space: pre;
}
</style>
</head>
<body>
<main id="logContainer"></main>
<script type="module">
import {
loadFonts,
parameters,
validateNumber,
Queue,
setText,
stringify,
removeChildren,
Observer,
Emitter
} from 'https://obs.multistream.tools/v1/overlay.js';
import { customEvent, emitter } from './emitter.js';
// Optional.
loadFonts('Tangerine');
let { duration } = parameters;
duration = validateNumber(duration, 5);
const queue = new Queue(duration);
const { logContainer } = window;
const log = data => {
setText(stringify(data, null, ' '), logContainer);
emitter.emit(customEvent, data);
};
const clear = () => removeChildren(logContainer);
const addToQueue = data => queue.add(() => log(data), clear);
const observer = new Observer({
load: addToQueue,
session: addToQueue,
alerts: addToQueue,
chat: addToQueue,
other: addToQueue
});
</script>
</body>
</html>
The data related to each event received, coming from the different platforms, will be emitted again to other overlays observing the custom event named customEvent
. Thus, each Multistream Tools overlay can talk in both directions with several other Multistream Tools overlays!
A given overlay never receives the custom events it emits itself. This notably avoids infinite loops.
Now create a new HTML page, named for example receiver.html, in the same directory as your overlay. It simply is a second overlay capable of processing the custom event emitted by the first one:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom events receiver example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
</head>
<body>
<script type="module">
import { Observer } from 'https://obs.multistream.tools/v1/overlay.js';
import { customEvent, emitter } from './emitter.js';
const customEventKey = emitter.key(customEvent);
const observer = new Observer({
[customEventKey](data) {
// Do whatever you want with the data here.
}
});
</script>
</body>
</html>
Custom event names are automatically prefixed to avoid possible conflicts with those handled natively by our system.
OBS JavaScript API
Multistream Tools overlays have the ability to use the OBS JavaScript API through our wrapper; you will just have to remember to adjust the value of the obsAPI
and obsEvents
parameters as well as the permissions of related browser sources according to your needs. There are different ways to call this API, you can of course combine them but here is their description one by one.
Reminder: custom browser docks currently have no access to the OBS JavaScript API.
Events
OBS emits events when using the software, for example each time you start or stop a stream, change scenes, etc. In order for an overlay to receive these, you must configure the associated wrapper with the parameter named obsEvents
and simply use the Observer
class to observe the events in question.
For example, for a wrapper configured such as:
https://obs.multistream.tools/v1/wrapper?obsEvents=StreamingStarted,StreamingStopped,SceneChanged&url=<overlayURL>
The corresponding overlay can be written as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>OBS events exemple</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
</head>
<body>
<script type="module">
import { Observer } from 'https://obs.multistream.tools/v1/overlay.js';
const observer = new Observer({
obsStreamingStarted() {
// Do whatever you want here.
},
obsStreamingStopped() {
// Do whatever you want here.
},
obsSceneChanged({ data }) {
// Do whatever you want with the data here.
// `data.name` contains the name of the new scene.
}
});
</script>
</body>
</html>
The obs
prefix in front of each event name must not be present in the wrapper configuration but must not be forgotten in the instance of the Observer
class.
Properties
The OBS JavaScript API can expose properties that are not methods. The Multistream Tools system allows you to access their values by activating the obsAPI
parameter of our wrapper and then using the utility function also named obsAPI
and the Observer
class, both exposed by our JavaScript framework.
For example, for a wrapper configured such as:
https://obs.multistream.tools/v1/wrapper?obsAPI&url=<overlayURL>
The corresponding overlay can be written as follows:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>OBS properties example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
</head>
<body>
<script type="module">
import {
Observer,
obsAPI
} from 'https://obs.multistream.tools/v1/overlay.js';
const propertyName = 'pluginVersion';
const observer = new Observer({
[propertyName]({ data }) {
// Do whatever you want with the value here.
}
});
obsAPI(propertyName);
</script>
</body>
</html>
Calling the utility function named obsAPI
sends a request to OBS which will respond, *in the form of an event * to observe, with the value of the desired property; here the version number of the brick responsible for displaying browser sources.
Getters
The operation of getters is the same as for properties. You will simply call methods of the OBS JavaScript API whose name starts with get
.
Consider adjusting the value of the attribute named Page permissions of the associated browser source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>OBS getters example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
</head>
<body>
<script type="module">
import {
Observer,
obsAPI
} from 'https://obs.multistream.tools/v1/overlay.js';
const getterName = 'getCurrentScene';
const observer = new Observer({
[getterName]({ data }) {
// Do whatever you want with the value here.
}
});
obsAPI(getterName);
</script>
</body>
</html>
Control
It is possible to control OBS by calling various API methods provided for this purpose. Some accept parameters and others do not, but none returns a value to observe.
Consider adjusting the value of the attribute named Page permissions of the associated browser source.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>OBS control example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
</head>
<body>
<script type="module">
import {
obsAPI,
parameters,
validateString,
validateNumber,
toMilliSeconds
} from 'https://obs.multistream.tools/v1/overlay.js';
let { endScene, stopDelay } = parameters;
endScene = validateString(endScene, 'End');
stopDelay = validateNumber(stopDelay, 30);
obsAPI('setCurrentScene', endScene);
setTimeout(() => obsAPI('stopStreaming'), toMilliSeconds(stopDelay));
</script>
</body>
</html>
The example above shows code which, once the corresponding source is active, will begin a transition to a configurable end-of-stream scene (named 'End'
by default), before stopping it completely after a certain delay, also configurable (30
seconds by default).
Docks
You can also develop your own docks, these are nothing more than overlays integrated into OBS as custom browser docks; the only limitation being that they do not currently have direct access to the OBS JavaScript API for reasons beyond our control.
You can nevertheless get around this limitation by having your docks communicate if necessary with an overlay acting as a hatch.
Here is an example of a dock using our JS and CSS frameworks to display an administration form as well as a few buttons:
We consider that there also is a file named emitter.js, present in the same directory as the dock HTML page, containing the following code:
import { Emitter } from 'https://obs.multistream.tools/v1/overlay.js';
export const buttonClicked = 'buttonClicked';
export const settingsSubmitted = 'settingsSubmitted';
export const emitter = new Emitter();
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>Custom dock example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/dock.css"/>
</head>
<body>
<form id="settingsForm" class="page">
<section class="content">
<label class="fields field">
<input class="input" name="text" required/>
<span class="label">Text input</span>
</label>
<fieldset class="fields">
<legend class="label">Checkboxes</legend>
<div class="row">
<label class="field">
<input class="input" type="checkbox" name="checkbox1" checked/>
<span class="label">Checkbox 1</span>
</label>
<label class="field">
<input class="input" type="checkbox" name="checkbox2"/>
<span class="label">Checkbox 2</span>
</label>
</div>
</fieldset>
<fieldset class="fields">
<legend class="label">Radio buttons</legend>
<label class="field">
<input class="input" type="radio" name="radio" value="radio1" checked/>
<span class="label">Radio button 1</span>
</label>
<label class="field">
<input class="input" type="radio" name="radio" value="radio2"/>
<span class="label">Radio button 2</span>
</label>
</section>
<footer class="toolbar">
<button id="simpleButton" class="button" type="button">Button</button>
<button id="submitButton" class="button" disabled>Submit</button>
<button class="button danger" type="reset">Reset</button>
</footer>
</form>
<script type="module">
import {
ClickListener,
Form,
enable
} from 'https://obs.multistream.tools/v1/overlay.js';
import { buttonClicked, settingsSubmitted, emitter } from './emitter.js';
const { settingsForm, simpleButton, submitButton } = window;
const { text, checkbox1, checkbox2, radio } = settingsForm;
new ClickListener(() => emitter.emit(buttonClicked), simpleButton);
new Form(settingsForm, {
persisted: [checkbox1.name, checkbox2.name],
submit() {
emitter.emit(settingsSubmitted, {
text: text.value,
checkbox1: checkbox1.checked,
checkbox2: checkbox2.checked,
radio: radio.value
});
}
});
enable(submitButton);
</script>
</body>
</html>
The code above implements a form including a text field, two independent checkboxes and two radio buttons. The text field is mandatory, everything else is optional and the state of the checkboxes is automatically saved on each change to persist between each page reload. Three buttons are also present, used respectively to emit events and reset the form.
Internationalization
Using the above code example, let's add translation management in French and Spanish to our freshly created user interface:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="robots" content="noindex,nofollow"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title data-i18n="title">Custom dock example</title>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/core.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/overlay.css"/>
<link rel="stylesheet" href="https://obs.multistream.tools/v1/dock.css"/>
</head>
<body>
<form id="settingsForm" class="page">
<section class="content">
<label class="fields field">
<input class="input" name="text" required/>
<span class="label" data-i18n="textInput">Text input</span>
</label>
<fieldset class="fields">
<legend class="label" data-i18n="checkboxes">Checkboxes</legend>
<div class="row">
<label class="field">
<input class="input" type="checkbox" name="checkbox1" checked/>
<span class="label" data-i18n="checkbox1">Checkbox 1</span>
</label>
<label class="field">
<input class="input" type="checkbox" name="checkbox2"/>
<span class="label" data-i18n="checkbox2">Checkbox 2</span>
</label>
</div>
</fieldset>
<fieldset class="fields">
<legend class="label" data-i18n="radioButtons">Radio buttons</legend>
<label class="field">
<input class="input" type="radio" name="radio" value="radio1" checked/>
<span class="label" data-i18n="radio1">Radio button 1</span>
</label>
<label class="field">
<input class="input" type="radio" name="radio" value="radio2"/>
<span class="label" data-i18n="radio2">Radio button 2</span>
</label>
</section>
<footer class="toolbar">
<button id="simpleButton" class="button" type="button" data-i18n="button">Button</button>
<button id="submitButton" class="button" disabled data-i18n="submit">Submit</button>
<button class="button danger" type="reset" data-i18n="reset">Reset</button>
</footer>
</form>
<script type="module">
import {
ClickListener,
Form,
enable,
i18n
} from 'https://obs.multistream.tools/v1/overlay.js';
import { buttonClicked, settingsSubmitted, emitter } from './emitter.js';
i18n({
fr: {
title: 'Exemple de dock personnalisé',
textInput: 'Champ de texte',
checkboxes: 'Cases à cocher',
checkbox1: 'Case à cocher 1',
checkbox2: 'Case à cocher 2',
radioButtons: 'Boutons radio',
radio1: 'Bouton radio 1',
radio2: 'Bouton radio 2',
button: 'Bouton',
submit: 'Soumettre',
reset: 'Réinitialiser'
},
es: {
title: 'Ejemplo de dock personalizado',
textInput: 'Campo de texto',
checkboxes: 'Casillas de verificación',
checkbox1: 'Casilla de verificación 1',
checkbox2: 'Casilla de verificación 2',
radioButtons: 'Botones de opción',
radio1: 'Botón de opción 1',
radio2: 'Botón de opción 2',
button: 'Botón',
submit: 'Enviar',
reset: 'Restablecer'
}
});
const { settingsForm, simpleButton, submitButton } = window;
const { text, checkbox1, checkbox2, radio } = settingsForm;
new ClickListener(() => emitter.emit(buttonClicked), simpleButton);
new Form(settingsForm, {
persisted: [checkbox1.name, checkbox2.name],
submit() {
emitter.emit(settingsSubmitted, {
text: text.value,
checkbox1: checkbox1.checked,
checkbox2: checkbox2.checked,
radio: radio.value
});
}
});
enable(submitButton);
</script>
</body>
</html>
It is simply a matter of defining data-i18n
attributes on the HTML tags for which to translate the textual content, then calling the i18n
function exposed by our JavaScript framework.
All docks and overlays can be translated in the same way.
You see, it's very simple in terms of code; now it's up to you to develop your own overlays and docks!