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 clients exported through the app's Generate Client feature, are part of the project's AGPL-3.0 codebase.
- You may download, use, and modify these clients privately under the terms of the AGPL-3.0.
- If you redistribute a client or a modified version, you must comply with the AGPL-3.0 requirements, including providing the corresponding source under the same license.
- If you operate a modified version in a way that allows users to interact with it remotely over a network, the AGPL-3.0 may also require you to offer those users its corresponding source.
Conversely, if you build a brand-new widget from scratch and do not copy from or create a derivative work of AGPL-3.0–licensed client code, you may license that independent widget under terms of your choice.
For the complete and authoritative terms, 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
WaveFormatEncodingfromNAudio, 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-ratioon 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.,