Creating Custom Themes and Clients¶
YouTube Music Streamer allows you to create your own themes and clients (from now on, we only say "client") for the server. This is a very technical guide, so if you're not familiar with programming, or at least HTML, CSS and JavaScript, you might want to skip this guide.
License for First-Party & Example Clients/Widgets
All first-party clients and widgets, including the "dev-widget" and any auto-generated examples from the app’s "Generate client" feature, are part of the project's AGPL-3.0 codebase.
- If you download, modify, or redistribute any of these example clients, your changes must be released under AGPL-3.0.
- If you use the “Generate widget” feature and then tweak that code, you’re bound by the same AGPL-3.0 requirements.
Conversely, if you build a brand-new widget from scratch—and do not copy or link to any AGPL-3.0–licensed files, then you may license that widget under any terms you choose.
For the full license text, see the LICENSE file in the repository.
What is a Client?¶
A client is, very simple explained, anything that can communicate via WebSockets. It should (here comes the "theme" part) also be able to display something. For the first-party clients, this is a HTML file that can be embedded in the most streaming software programs (Browser Source). You can also create your own clients in any other way, with any language you like, as long as it can communicate via WebSockets. The possibilities are endless!
Technical Information¶
The server communicates with the clients via WebSockets. It uses the DotNet System.Net.HttpListener
class to create a simple HTTP server that listens for incoming WebSocket connections and is therefore very lightweight and minimalistic but this also means, the server is mostly in "send-only" mode (it doesn't listen for any incoming messages. Only for connections and disconnections).
To connect with it, you need to use the WebSocket protocol. The server listens on localhost:new WebSocket('ws://localhost:9876/
);`
The server sends multiple formats of messages in one single channel, depending on the type of message. It sends Song/Track Information and, if not disabled, Audio Data.
Message Formats:¶
Song/Track Information: This is a JSON object that contains information about the currently playing song, such as title, artist, album, duration, and progress. It is sent every time the song changes or the progress updates.
data
is YTMDesktop CSharp Companion StateOutput from YTMDesktop CSharp Companion.
minified example json
{
"e": "TrackInfo",
"data": {
"Player": {
"TrackState": 1,
"VideoProgress": 198.46830118367325,
"Volume": 10,
"AdPlaying": false,
"Queue": {
"Autoplay": true,
"Items": [],
"AutomixItems": [],
"IsGenerating": false,
"IsInfinite": false,
"RepeatMode": 0,
"SelectedItemIndex": 48
}
},
"Video": {
"Author": "London Elektricity",
"ChannelId": "UCTyQFEG-Sr03fjZ4IY59Tiw",
"Title": "Just One Second (Apex Remix)",
"Album": "Sick Music",
"AlbumId": "MPREb_sl4GScOhioX",
"LikeStatus": 1,
"Thumbnails": [
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w60-h60-l90-rj",
"Width": 60,
"Height": 60
},
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w120-h120-l90-rj",
"Width": 120,
"Height": 120
},
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w180-h180-l90-rj",
"Width": 180,
"Height": 180
},
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w226-h226-l90-rj",
"Width": 226,
"Height": 226
},
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w302-h302-l90-rj",
"Width": 302,
"Height": 302
},
{
"Url": "https://lh3.googleusercontent.com/NuxLr-wv-zCR5u57iJP1JEsZJijWC0AR1m5S9lzjAudWKtdUpl2B98uK2aWpC1FwqCmaFgy5naD1sV1j=w544-h544-l90-rj",
"Width": 544,
"Height": 544
}
],
"DurationSeconds": 378,
"Id": "_YZD9PKeeDo"
},
"PlaylistId": "RDTMAK5uy_n_5IN6hzAOwdCnM8D8rzrs3vDl12UcZpA"
}
}
Audio Data: This is binary data (byte[]
) that contains the audio stream of the currently playing song. It uses NAudio
. It is sent every 33ms (about 30Hz) and can be used to visualize the audio in real-time. This is only sent if the "Allow Audio Stream" option is enabled in the server settings.
{
"e": "AudioInfo",
"data": {
"SampleRate": 44100, // (1)!
"BitsPerSample": 16, // (2)!
"Channels": 2, // (3)!
"Encoding": "Pcm" // (4)!
}
}
- integer - Sample rate of the audio stream (e.g., 44100Hz).
- integer - Bits per sample (e.g., 16 bits).
- integer - Number of channels (1 for mono, 2 for stereo, ...).
- string - Encoding type of the audio stream (e.g., "Pcm", "Mp3", etc.). This is the
WaveFormatEncoding
fromNAudio
, as a string, so you can use it to determine how to decode the audio data.
Creating a Client¶
For this, we concentrate only on the connection part etc. The "theme" part is not included here, because that has less to do with the application itself and more with the design and layout of a custom client. Below is a simple example of how to connect to the server and work with the data we receive. For the example, we're using JavaScript.
For an even more detailed example with visualizing, theming, etc., feel free to generate any of the clients in the server settings or use the copy of the "dev-widget" in Example Client. It is a good example of how to create a client that can display song information and visualize audio data.
// connect to server
let ws = new WebSocket('ws://localhost:9876/');
// On close, very simple reconnect
ws.onclose = (event) => {
ws = new WebSocket(event.target.url);
ws.onclose = event.target.onclose;
ws.onmessage = event.target.onmessage;
}
// listen for messages
ws.onmessage = async (event) => {
// first check, if it's json data or binary data
if (typeof event.data === 'string') {
const data = JSON.parse(event.data);
// if it's json data, check if it's track info or audio info
switch (data.e) {
case 'TrackInfo':
// If it's track info, do something to visualize it, here comes theming into play
case 'AudioInfo':
// If it's audio info, use those as metadata to work better with the binary audio data
}
} else {
// Binary message (Blob) with raw audio data. It's recommended to first wait for the metadata (AudioInfo)
if (!audioInfo) {
console.error("Audio metadata not received yet!");
return;
}
// Do something with the binary data (make an audio visualizer, etc). Here's a small extended example on how you could work with it.
// Convert it to a float 32 array
const arrayBuffer = await event.data.arrayBuffer();
const floatData = new Float32Array(arrayBuffer);
// check if it's mono or stereo based on the channels
let monoData;
if (audioInfo.Channels === 2) {
// For stereo data, average the left and right channels
const numSamples = floatData.length / 2;
monoData = new Float32Array(numSamples);
for (let i = 0; i < numSamples; i++) {
// Assuming interleaved stereo: left at index 2*i, right at index 2*i+1
monoData[i] = (floatData[2 * i] + floatData[2 * i + 1]) / 2;
}
} else {
// If mono, no need to process further
monoData = floatData;
}
// save it for for visualization
latestAudioData = monoData;
}
}
Example Client¶
Recommendations¶
We recommend that your custom client:
-
Use the Canvas API and
requestAnimationFrame
- Leverage the Canvas API together with requestAnimationFrame for smooth, high-performance audio visualizations (waveforms, spectrograms, etc.).
-
Keep it lightweight
- Minimize dependencies and bundle size: tree-shake and lazy-load assets.
- Defer non-essential scripts and styles to speed up initial load.
-
Ensure full responsiveness
- Use fluid units (
vw
/vh
,%
) and CSS functions (min()
,max()
,clamp()
). - Define an explicit
aspect-ratio
on your root container so OBS and other streaming tools can size the widget correctly. - Auto-scroll long text only when overflow is detected (e.g., marquee or CSS scroll).
- Use fluid units (
-
Provide clear fallbacks
- Detect feature support (e.g., OffscreenCanvas, Web Audio) and gracefully degrade.
- Show a static thumbnail or simple text view if real-time audio data isn’t available.
-
Keep theming flexible
- Expose CSS variables (e.g.,
--primary-color
,--font-size
) for easy overrides. - Structure your HTML with semantic classes so users can swap out layouts without touching core logic.
- Expose CSS variables (e.g.,