OBS Overlays for Twitch Streaming

For some time now people have been using Beat Link Trigger to add fun elements to mixes streamed on Twitch by creatively screen-grabbing parts of the Player Status window (and the large waveform windows feature was added to support that). But for even longer, there has been a request to enable custom overlays based on data from Beat Link, and this is finally possible.

By opening Network  OBS Overlay Web Server you can configure and launch an embedded web server that renders an HTML template that can contain the content of your choice, arranged and styled however you like, to work with the Browser Plugin that is bundled with OBS Studio.

OBS Overlay Server Window

The Server Port spinner allows you to choose the port on which the web server will operate. You can pick any port that is not in use; the default of 17,081 is likely to work just fine. The Run checkbox starts and stops the server. Once it is running you can click Open in Browser to take a look at how the configured template renders the current player state, and to give you an easy way to copy the overlay URL from the browser address bar for pasting into OBS Studio.

Beat Link Trigger ships with a default template that is served unless you specify your own templates folder, which you can do by clicking Choose on the Templates Folder row. It will allow you to navigate to and select any folder your system, as long as there is a readable overlay.html file in that folder. This file will be used as the template rendered by the overlay server. It can reference other templates in that folder using whatever names you give them, either by extending or including them, as discussed below.

Once you have chosen a templates folder, that is stored in your preferences, and it will automatically be used the next time you run Beat Link Trigger (unless the folder no longer contains a readable overlay.html, in which case a warning will be displayed when you open this window, and the default template will be used again).

In designing your template, you are likely to want to use graphics, fonts, CSS, and possibly scripts and other resources. If these are already on the Internet, you can reference them using URLs in your template. But if you want to access them locally, you can gather them all into any folder, and configure that as a Public Folder for the overlay server by clicking the Choose button in that row, and navigating to and selecting that folder. Once you have done that, any file in that folder can be referenced from your template simply by using a URL of the format /public/ followed by the filename. So for example if you have a file logo.png in your public folder, a template can use it by referencing the URL /public/logo.png.

Just like the templates folder, your chosen public folder is stored in the preferences, and will be used on subsequent runs of Beat Link Trigger until you choose a different folder, or it can no longer be found.

Although you can set the same folder as your Templates Folder and Public Folder, this is not recommended, because it will expose your raw templates. In that configuration, accessing /public/overlay.html will serve the overlay template without performing any of the variable substitutions that add information about the tracks currently playing. Beat Link Trigger will let you set things up this way, but will warn you when you do.

Writing Templates

You can find the source for the latest version of the basic template that is built in to Beat Link Trigger on GitHub.

Templates developed or modified by other users can be found on the Wiki, and you can download them to use or modify using your favorite text editor. Overlay templates are HTML files that get processed by Selmer, so you can interpolate all kinds of information about the current tracks being played.

Don’t worry too much about the details of the templating language, because it turns out that trying to render the whole page up front doesn’t work well in the OBS browser plugin anyway. Configuring the page to refresh itself periodically, which would re-render the templates, was the original plan for this feature, but the OBS browser plugin does not use double-buffering, so that approach caused the overlay to flicker in a very unpleasant way. To get a nice-looking overlay that updates itself smoothly, write JavaScript code that periodically fetches the current player state as JSON parameters, and then updates any changed elements using DOM manipulation by ID. That is how templates, including the built-in one, are working today.

If you still want to have the original page contents render correctly, before the first JavaScript update changes them, you can learn how the Selmer templates work. (Or, if you are rendering the template in a context that does use double-buffering, or some other mechanism where you can refresh the entire page without flicker, using the template syntax directly with no JavaScript DOM updates will be a lot simpler.)

Either way, you will want to read the template language variable documentation, because that explains all the values available to you, whether you get them through Selmer or as JSON parameters.

While these instructions cover most of the features that will be useful in the context of Beat Link Trigger, you can learn more about the basics of the templating language by looking at the Django documentation, but keep in mind some details are quite different in Selmer and Beat Link Trigger. You can also consult the Selmer documentation for details about how its template language differs from Django, and to learn about filters and tags that didn’t make the cut for explaining here, but that document is aimed at Clojure programmers adding Selmer to their projects, so it might be a little hard to follow.


Variables allow you to insert information from the current state of your players into the template.

Template variables look like this: {{ variable }}. When Selmer encounters a variable like this, it looks up the value of that variable, and inserts the value into the template in place of the variable. There are many variables available to you, which are described just below, and you can also examine their actual values for the current state of your players by clicking the Inspect Template Parameters button when the overlay server is running:

OBS Overlay Template Parameters

Notice that the variables are grouped into a hierarchy. You specify the value you want by starting at the outer name, and separating names at each level with ., so at the moment captured by the above screen shot, if your template contained {{ players.2.pitch-display }} the value - 3.40% is what would appear in the overlay, showing the current pitch value displayed on Player 2.

You can expand sections of the parameter inspector by clicking on the + buttons, and collapse them by clicking on the - buttons, or simply double-click on those rows to toggle the expanded state.

Some variables may not be in the exact format that you want to display them, but you can use Filters to reformat them in flexible ways. And other variables may not be something you want to display at all, but you can use them to control which sections of your template are displayed using Tags.

You can start the overlay server even if Beat Link Trigger is not currently online with a DJ Link network. When you do that, it will serve simulated data for two players, with Gareth Emery’s Concrete Angel loaded in player 1 and Lindsey Stirling’s Transcendence in player 2, so you can work on your template design even when you don’t have actual player hardware handy. Even though those are the only two simulated tracks available, you can configure them to be in more or different players if your template design requires; ask for help on the Zulip stream if you are interested in doing that.
Table 1. Top Level Variables
Variable Value

Present when there are any computers or mobile devices running rekordbox on the network, this holds the device numbers assigned to each, and under each device number, the Player State of that rekordbox instance. Values are accessed like {{ collections.17.name }}.


Present if there is currently a Tempo Mater on the network, this holds the Player State of that player. Values are accessed like {{ master.track.title }}.


Present if there are any DJM mixers (or an XDJ-XZ) on the network, this holds the device numbers of each mixer, and under each device number, the Player State of that mixer. Values are accessed like {{ mixers.33.name }}.


Holds the device numbers of player on the network, and under each device number, the Player State of that player. Values are accessed like {{ players.1.track.title }}.

Not all values will always be present. Some devices (like mixers and rekordbox) provide very little information, and even CDJs will only provide full details when a reordbox-analyzed track is loaded. Even then, most DJs do not fill in all details about tracks. When information is not available, an entry may be entirely missing, or it may simply have an empty value.

Player State

Table 2. Player State Contents
Element Value

The IP address at which the player can be found on the network.


The beat currently playing, if known. The first beat of the track is beat 1, and they increment steadily from there.


Tracks progress through measures of the song. The first (down) beat of a measure has the value 1, then it grows to 4 before jumping back to 1 at the start of the next bar.


Tracks how many beats remain until the next hot cue or memory point in the track. If there are no cues left in the track, or if the next one is more than 64 bars from now, this has the value 511. If there is a cue coming up within 64 bars (256 beats), this will count down those beats, from the value 256 to 0, meaning we have reached the cue. As soon as the next beat arrives, it will start tracking the next hot cue or memory point.


This interprets the value of the cue-countdown reported by the player, showing how it appears on the player display. The value 511 is displayed as “--.-” (meaning “no information”), while values representing a countdown show two digits of bars, a decimal point, and the number of beats (1 through 4). So it starts at “63.4” at the furthest possible distance, then counts down to “00.1” on the final beat before the cue, and “00.0” when it is reached.


Reports the version of the firmware running the player.


Indicates whether the player has reached the end of its track and stopped playing.


Indicates that the player was in Sync mode, but the DJ has nudged the jog wheel to adjust track alignment, so it is now only following the tempo of the master player, and is no longer slaved to the precise beat locations the master is reporting.


Indicates if the player is currently playing, loading, or searching a track.


Indicates whether the player is paused at the current Cue point.


Indicates whether the player is repeating a loop.


Indicates that the player is attached to a live mixer channel (one that is currently audible in the mix). This works only with DJM mixers, and when properly configured.


Indicates that the player is waiting for the DJ to start playback.


Indicates that the player is currently playing a track. p


Indicates that the player is playing a track in Reverse mode.


Indicates that the player is currently playing a track and the jog wheel is set to CDJ mode.


Indicates that the player is currently playing a track and Reverse mode is not active.


Indicates that the player is currently playing a track and the jog wheel is set to Vinyl mode.


Indicates that the DJ is currently fast-forwarding or rewinding through a track.


Indicates that the player is staying aligned to the tempo and beats of the current Tempo Master.


Indicates that the player has been set as the current tempo master, so any players in Sync (or BPM-Only Sync) are following it.


Indicates that there is a track loaded in the player.


Either :collections, :mixers, or :players, used to group players in their proper top-level variable.


The name reported by the player, e.g. “CDJ-2000nexus”, “XDJ-XZ”.


The channel number assigned to the player.


The current playback pitch adjustment. Zero means normal speed, 100 means double speed (100% faster), -100 means slowed to a stop. This is a floating point value, so you will probably want to format it.


Reflects the way the current pitch value is displayed on the player: a + or - sign followed by a formatted percentage, with 1 or 2 digits after the decimal point. (Normal speed is shown without a sign, as “0.00%”.)


A convenient way to relate the playback pitch to track tempo. Normal speed is 1.0, and multiplying this value by track-bpm is how tempo is calculated.


The current effective tempo of the track: the natural track tempo adjusted by the current playback pitch. Can be calculated by multiplying track-bpm and pitch-multiplier.


How much time of the track has been played. (This reflects distance into the track when played at normal speed; it will move slower or faster than real time if the playback pitch has been adjusted.) This is a Time Value with multiple parts used like {{ master.time-played.minutes }} or {{ players.3.time-played.display }}.


How much time of the track is left to play. (This reflects how long it would take when played at normal speed; it will move slower or faster than real time if the playback pitch has been adjusted.) This is a Time Value with multiple parts used like {{ master.time-remaining.raw-milliseconds }} or {{ players.2.time-played.frames }}.


Another multi-part value, holding Track Details about the currently loaded rekordbox track.


The natural tempo of the current section of the track; this is how many beats per minute would be heard when played at normal speed.


The position of the track within its playlist, or menu category, or CD.


The channel number of the player (or rekordbox instance) from which the track was loaded.

Time Values

Table 3. Time Value Contents
Element Value

The full time details as displayed on a player, formatted as mm:ss:ff.f (minutes, seconds, frames, and frame-tenths).


The current half-frame being played. (There are 75 frames played per second, and the players track position by half-frame, so this alternates between the values 0 and 5.)


How many frames have been played (or remain) in the current second, ranges from 0 to 74.


How many minutes have been played (or remain).


This is the value that is used to compute all the others. It starts at zero at the beginning of the track, and counts up by a thousand for each second played. (For remaining time, it counts down from the length of the track to zero.)


How many seconds have been played (or remain) in the current minute, ranges from 0 to 59.

Track Details

Table 4. Track Detail Contents
Element Value

When the track was added to the DJ’s collection, a date in the form yyyy-mm-dd.


The title of the album to which the track belongs.


The name of the performer of the track.


The CSS color code (#rrggbb) of the color the DJ assigned the track.


The name of the color the DJ assigned the track.


The comment string the DJ wrote about the track.


How long, in seconds, the track will play at normal speed.


The musical genre the DJ assigned the track.


The rekordbox ID that identifies the track in the current database.


The musical key of the track.


The recording label that issued the track.


The artist who first recorded the track.


The star rating (0-5) the DJ assigned the track.


The person who remixed this track.


The slot from which the track was loaded (No Track, CD Slot, SD Slot, USB Slot, or rekordbox).


How many beats per minute are heard at the start of the track, when played at normal speed.


The name of the track itself.


The kind of track that was loaded (No Track, CD Digital Audio, rekordbox, or Unanalyzed).


When the track was recorded.


You can change the way variables are displayed by using filters.

Template filters look like this: {{ master.track.title|lower }}. Use a vertical bar (|) between a value and the filter you want to apply to it. This example displays the value of the master.track.title variable modified by the lower filter, which converts text to lower-case.

Some filters take arguments that adjust their behavior. Use a colon (:) to introduce a filter argument. The abbreviate filter makes sure a value fits in a maximum number of characters. Suppose the master player is playing the Skrillex track With You, Friends (Long Drive). We can make sure it fits in twenty characters in our template like so:

{{ master.track.title|abbreviate:20 }}

This would insert With You, Friends…​ into the template.

Filters can take multiple arguments, in which case you will have multiple colons in between them.

Filters can also be “chained”: you can apply another filter to the result of the previous one. Just add another | and type the next filter. So, with the same track playing, we could write:

{{ master.track.title|lower|abbreviate:20 }}

And this would insert with you, friends…​ into the template.

If your filter argument is a string, and it has any spaces in it, you need to quote it. See the default filter for an example.

Here are the filters you are most likely fo find useful. The full list can be found in the Selmer documentation.


Abbreviate the input string to given width if it exceeds a maxium width. If only a maximum width is given, abbreviated and maximum width are the same. The first parameter is the maximum width, the optional second parameter the abbreviated width.

You can control where the abbreviation happens by putting an abbr-left, abbr-middle, or abbr-right filter before the abbreviate filter. You can also use something other than …​ as the abbreviation indicator by putting and abbr-ellipsis filter with an argument containing the indicator you want to use in front of the abbreviate filter. These special abbr- filters can only be used just before an abbreviate filter.


Adds the value and all the filter’s arguments together. If they are all numbers, calculates their sums. If any is a non-numeric string, concatenates the arguments. There are also multiply and divide filters which take only a single argument, and perform the corresponding arithmetic operation on the value (they only work with numbers).

count, count-is

count counts the letters in a string, or the number of elements in a collection (such as the players). count-is checks whether that count matches its argument. You can use it with the if tag to conditionally display content if there are a certain number of players on the network, for example:

{% if players|count-is:4 %}
<p>All Players are in use!</p>
{% endif %}


Allows you to specify a value to display when a variable is empty or missing. Normally, in such situations the variable just disappears from the template (it is replaced by an empty string). If there is no track loaded on player 2, this expression will result in the string No track:

{{ players.2.track.title|default:"No track" }}

Notice that we had to quote the filter’s argument because our value had a space in it.


Rounds a number to a specified number of decimal places. This is useful when working with pitch values, since we usually only want two places after the decimal point:

{{ master.pitch|double-format:2 }}

There is also a round filter which rounds all the way to the nearest integer, and number-format which lets you use a Java format string to get full control of the way you format the number, even specifying an ISO-639 two-letter locale specifier as its second argument.

empty?, not-empty

Check whether the specified value is empty or not (both filters return true or false). These are again useful combined with the if tag to conditionally render content:

{% if players|empty? %}
<p>No Players Found!</p>
{% endif %}


Convert a value to lower-case.


Convert a value to upper-case. (There are also two variants: capitalize converts only the first character to upper-case, while title converts the first letter of each word to upper-case.)


Takes two string arguments, and replaces any instances of the first string found in the value with the second string.


Sorts the elements of the value. You will generally want to apply this filter when you are looping over the values in players because they are otherwise not going to necessarily be in numerical order.


Tags allow you to get fairly sophisticated with your templates, supporting things like looping and conditionals. There are two fundamental variants: simple, inline tags like include or extends, which exist on their own, and more complex tags which affect text in between them (and may include intermediate tags), like if, else, and endif.

Here are the tags you are most likely fo find useful. The full list can be found in the Selmer documentation.


Conditionally renders the body only if the argument is true.

{% if players.3.is-on-air %}
<img src="/public/on-air.png">
{% else %}
<img src="/public/off-air.png">
{% endif %}

You can reverse the meaning of the conditional argument by preceding it with the not operator, or combine multiple conditional arguments with the all or any operators. You can also perform numeric comparisons using the =, <, >, <=, and >= operators, or string comparison using =.

{% if players|count < 4 %}Another player can be connected!{% endif %}

(Notice that the above example shows that filters can be applied to the conditions.)

In addition to the if, else and endif tags, you can have as many elif tags as you need between the else and endif, meaning “else if”. For more examples, see the Selmer documentation.


Loops over each item in an array or map, making the item available in a variable of your choice inside the loop. Beat Link Trigger uses nested maps rather than arrays for its template parameters, and you can loop over entries in the map assigning the key and value of each entry to separate variables:

{% for number, player in players|sort %}
<p>{{ number }}: {{ player.track.title }}--{{player.track.artist }}</p>
{% endfor %}

The above example loops over all the players that are found on the network. Each time through the loop, number is set to the channel number of the player and player is set to the map of parameters describing the state of that player, so the loop outputs a paragraph for each player, showing the channel number, a colon, the title of the track being played on that player, and the artist that recorded it.

Notice that the body of the loop is ended by an endfor tag, and that we were able to use the sort filter on the players map to make sure we got them in order of increasing channel number.

After the endfor tag, the number and player variables introduced by the loop go away.

You can also place an empty tag between for and endfor, and the content within that section will be rendered if the variable you were trying to loop over had no values:

{% for number, player in players|sort %}
<p>{{ number }}: {{ player.track.title }}--{{ player.track.artist }}</p>
{% empty %}
<p>No players found!</p>
{% endfor %}

In addition to the variables you requested, the for tag always adds several others that allow you to respond to or display additional information about the looping process:

Table 5. Special Loop Variables
Variable Description

Holds the number of this iteration of the loop, starting at 1 the first time through.


Holds the number of this iteration of the loop, starting at 0 the first time through.


Starts at forloop.length and counts down to 1 for each iteration of the loop.


Starts at forloop.length−1 and counts down to 0 for each iteration of the loop.


Is true if this is the first iteration of the loop.


Is true if this is the final iteration of the loop.


Holds the number of times the loop will iterate.


If this loop is nested inside another for loop, you can access the outer loop’s special variables using (for example) forloop.parentloop.counter.


Picks the next value from a list of values each time through a loop. Here is an example of this tag combined with the for tag to create a list item for the player number and track title of each player found on the network, and alternating the CSS classes of each between blue and white:

{% for number, player in players|sort %}
<li class={% cycle "blue" "white" %}>
  {{ number }}: {{ player.track.title }}
{% endfor %}


Replaces itself with the contents of the referenced template.

{% include "player.html" %}


Specifies a block of content that can be overwritten using template inheritance.

{% block track-information %}This will be replaced.{% endblock %}


This tag is used to reference a parent template, which provides the outer structure that gets rendered. The blocks in the parent are recursively overridden by the blocks from child templates, using template inheritance.

JSON Parameters

As noted above, it turns out that using elaborate template expressions to render custom and conditional content doesn’t make a lot of sense in the context of the OBS browser plugin, because of flickering when you try to re-render the whole page to get updated values. So a better approach, as shown by the built-in template, is to simply render the page once using a simple template, and assign IDs to the elements that will change over time, so you can use JavaScript code to periodically poll the BLT overlay server for the current player state, and update the page accordingly.

In order to support this approach, whenever your page requests the URL /params.json, BLT sends a JSON response containing all the current template parameters. You can set up a timeout function to retrieve them at whatever frame rate you would like to update your overlay, and your code can process the results and render itself as desired.

The content and meaning of the JSON response is detailed in the Variables section above, but ignore the discussion of how to use Selmer syntax to navigate the variables; you will be using normal JavaScript syntax once you have parsed the JSON. You can see the exact details of the current JSON value being returned by using the Open in Browser button in the OBS Overlay Server window, and then adding /params.json at the end of your browser’s URL bar. The Inspect Template Parameters button also offers a nice hierarchical way to explore them.

Graphics Resources

In addition to the template itself, there are a variety of URLs you can use with <img> tags in your template (or from your JavaScript update code) to display things like the album artwork or track waveforms associated with a player.

Album Art

You can look up the album art for the track loaded in a particular player using a URL like /art/1 (replace the 1 with another number to get the art for the player on a different channel). If there is artwork, this will return an 80x80 pixel JPEG image. If there is no art, or no track loaded, it will return an 80x80 transparent PNG image.

If you are using a for loop to display your players, you can use template substitution to set up your URL:

<img src="/art/{{ number }}" width="80" height="80">

If you would like to display more information about why there is no artwork, the way Beat Link Trigger does, you can add a query string to the request: /art/3?icons=true. With icons set to true, when the art is missing, you will receive a PNG image that describes why. If there is no track loaded, it will read "No Track". Otherwise, it will be an icon describing where the track was loaded from: USB, SD, CD Digital Audio, CD Data, or rekordbox.

The missing-artwork icons returned are larger than 80x80 pixels so they can be displayed in higher resolution on high-DPI displays, so you should be sure to render your artwork at a fixed width and height of 80 pixels using the HTML or CSS of your template, so the layout doesn’t change depending on whether artwork is present.

Waveform Previews

You can access the whole-waveform preview (as is displayed at the bottom of a CDJ screen) for the track loaded on a particular player using a URL like /wave-preview/2. If you request it that way, it will be rendered at a default width and height, but you can specify the size you want using query parameters:

<img src="/wave-preview/{{ number }}?width=500&height=100">

This image will show the current playback position of the player as a bar moving along the image, and will have small triangles marking hot cues and memory points.

If there is no waveform available for the specified player, a transparent image will be returned.

The lack of waveform data will result in a smaller image being returned, so as with album art you should always make sure your image is rendered at a fixed size using the HTML or CSS of your template, to avoid layout changes when data availability changes.

Custom Background Colors

If you are creating a lighter interface, you might want the background behind the waveform to be drawn in a color other than black. You can achieve that by adding a line like this to your Global Setup Expression:

(set-overlay-background-color (Color/white))

That will set the background color to white, using the constant color value in the java.awt.Color class. You can use any of the other constants defined in that class, or construct your own color. Here is another way to get white:

(set-overlay-background-color (Color. 255 255 255))

That sets the background color by constructing a color whose red, green, and blue components are all 255 (out of 255), so it is white.

You can also use the constructor with four arguments to make your color partly (or completely) transparent by passing an alpha value from 0 (transparent) to 255 (opaque). This would use a fully-transparent background color (so the first three numbers don’t matter):

(set-overlay-background-color (Color. 0 0 0 0))

If you change the background from black to white, you will almost certainly also want to change the color used to draw the playback position and tick marks (which is normally white, and so they will be invisible on your new background). You can do that with set-overlay-indicator-color. A black indicator color works well with a white background:

(set-overlay-background-color (Color/white))
(set-overlay-indicator-color (Color/black))

You can even change the color used to draw down beats and active playback position markers (which are normally red) using set-overlay-emphasis-color.

Scrolling Waveform Details

You can access the scrolling detailed waveform (as is displayed at the top of a CDJ screen) for the track loaded on a particular player using a URL like /wave-detail/4. If you request it that way, it will be rendered at a default width and height, but you can request a specific width and height using query parameters like with the waveform preview. You can also control the scale (zoom level) with which the waveform is rendered. The default scale is 4, meaning each column of the image is an average of four half-frames of audio. Setting scale to 1 shows the full detail available, while larger values let you see more of the waveform at once.

<img src="/wave-detail/{{ number }}?width=800&scale=2">

The image will show the current playback position of the player as a bar centered in the image, and the waveform will scroll as playback occurs. Hot cues and memory points will be drawn (including their labels and if they have been assigned), and loops will be visible.

If there is no waveform available for the specified player, a transparent image will be returned.

The lack of waveform data will result in a smaller image being returned, so as with album art you should always make sure your image is rendered at a fixed size using the HTML or CSS of your template, to avoid layout changes when data availability changes.

As noted above, if you are creating an overlay with a light background, you might want to change the waveform background color.