Buy Me a Coffee

Buy Me a Coffee!

Friday, May 15, 2026

CloudTalkShow: Building a Lobby and Adding Chat

In my last post I talked about getting NGINX and VDO.Ninja up and running on my Proxmox setup. At the time I mentioned that I was planning on using VDO.Ninja to help with production of the Cloud Talk Show and that I wanted to expose the OBS WebSocket interface for the producer. I have done both of those things, and I want to walk through what I built and the problems I ran into along the way.

The Setup

The goal was to replace Microsoft Teams as the coordination and recording tool for the show. Teams compresses video streams significantly and the labor involved in mixing everything into a program was more than we wanted to deal with long term. Here is what I ended up with:

  • VDO.Ninja running in my NGINX LXC to handle peer-to-peer video streams
  • OBS Studio running on a Windows 11 VM to mix the streams into a program
  • OBS-Web running on the NGINX LXC so the producer can control OBS remotely through a browser
  • A lobby web page that everyone uses to meet before the show starts

Each participant pushes two separate streams into VDO.Ninja — one for their camera and one for their screen share. I have five participants, so that is ten streams total. The lobby page pulls all ten of those streams into small preview blocks arranged in two columns, and also shows the live program output from an OBS virtual camera so everyone can see what is going out. I call it the lobby because we use it to get organized before we go live, the same way you would mill around in the lobby before a meeting starts.

Setting up the VDO.Ninja streams was straightforward once I understood the push/view URL pattern. Each participant gets a push URL they open in their browser, and the lobby page uses the corresponding view URLs in iframes. The OBS-Web setup required a bit more NGINX configuration to proxy the WebSocket connection through to the OBS WebSocket server running on the Windows 11 VM at port 4455.

Adding Chat

We have recorded 2 shows using the setup, but there were some shortcomings.  The lobby page was missing one thing that we had in our old MS Teams setup: a way for participants to talk to each other while they are getting set up. VDO.Ninja has a built-in chat feature but it didn't work with how I had the streams set up — I am using completely separate push/view pairs for each stream rather than a room, so the chat context isn't shared across the lobby page.

I wanted something simple and ephemeral. No accounts, no database, messages are gone when the server restarts. I already had Node.js v20 installed on the LXC, so I went with a minimal ws WebSocket server. No Express, no Socket.IO, just the WebSocket library and about 20 lines of code.

Here is the server:

const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 3001 }); wss.on('connection', (ws) => { ws.on('message', (data) => { wss.clients.forEach((client) => { if (client.readyState === 1) { client.send(data.toString()); } }); }); }); console.log('Chat server running on port 3001');

I installed it as a systemd service so it starts automatically and restarts if it crashes:

[Unit] Description=CloudTalkShow Chat Server After=network.target [Service] ExecStart=/usr/bin/node /opt/chat-server/server.js Restart=always User=www-data WorkingDirectory=/opt/chat-server [Install] WantedBy=multi-user.target

Then I added a new NGINX vhost for chat.smithmier.net to proxy WebSocket connections through to port 3001, grabbed a Let's Encrypt cert with Certbot, and dropped a chat widget into the lobby page HTML. The widget is plain JavaScript — a WebSocket connection, an input for your name, an input for the message, and a scrolling message area. Enter key sends. Nothing fancy.

The Problems

Getting here took longer than it should have, and the problems were all interesting enough to be worth documenting.

The Certbot chicken-and-egg problem. I configured the NGINX vhost with the SSL certificate paths before the certificate existed. NGINX failed its config test, Certbot couldn't run because NGINX was broken, and I was stuck. The fix is simple once you know it: start with an HTTP-only vhost, get the cert, then let Certbot add the SSL configuration. Obvious in retrospect.

Hairpin NAT. Once the cert was in place and the server was running, the chat worked when I tested it locally on the LXC but timed out from every other machine. The curl output told the story — the DNS A record pointed to my public IP 173.49.39.254, but my Verizon CR1000A router does not support hairpin NAT. Traffic that leaves the network and tries to come back in through the same public IP just gets dropped. My other subdomains worked because I had already added them to Pi-Hole with the internal IP 192.168.1.50. Adding chat.smithmier.net → 192.168.1.50 to Pi-Hole fixed it immediately.

The Chrome service worker. After all of that, the lobby page itself stopped loading in Chrome and Edge. Firefox was fine. The error in the Chrome console was The FetchEvent resulted in a network error response: the promise was rejected, which is a service worker error, not a network error. OBS-Web is a Svelte PWA and it had registered a service worker in Chrome at some point that got into a broken state. Clearing it from chrome://serviceworker-internals/ fixed it. Edge had the same issue since it runs the same engine.

What Is Next

The lobby and chat are working well. The next thing I want to write about is the overall show workflow — how the producer uses OBS-Web to control the mix, how we handle the recording, and how we get from a raw recording to a finished YouTube upload. I also want to dig into the VDO.Ninja configuration in more detail, because there are some bitrate and quality settings that made a significant difference in stream quality that are worth sharing.

If you are running a similar setup or have questions about any of the pieces, feel free to reach out.