Files
client/plans/vj-streaming-site-selector.md
2026-04-17 16:52:51 -04:00

14 KiB

VJ Page — Streaming Site Selector

Overview

Currently vj-stream.mjs hardcodes all streaming infrastructure URLs to laantungir.net. This plan adds a streaming site selector — a dropdown of remembered site configurations — so users can stream from any compatible server, not just laantungir.net.

Problem

The following are hardcoded in vj-stream.mjs:

Constant/Location Hardcoded Value
STREAM_BASE_URL https://laantungir.net
deriveShowUrls().obsServer rtmp://laantungir.net:1935/publish
deriveShowUrls().obsKeyTemplate {slug}/src/{SECRET_KEY}
deriveShowUrls().masterPlaylist https://laantungir.net/stream/{slug}/stream.m3u8
deriveShowUrls().viewerPage https://laantungir.net/stream/{slug}
deriveShowUrls().stats https://laantungir.net/api/stream/stats?show={slug}
OBS Server display in vj.html rtmp://laantungir.net:1935/publish

The monochrome/music URL (https://monochrome.laantungir.net) stays separate in the auto-announce section — it is not part of this feature.

Streaming Site Data Model

A streaming site configuration bundles three pieces of infrastructure:

{
  name: 'laantungir.net',           // Human-readable label for the dropdown
  streamBaseUrl: 'https://laantungir.net',  // Base URL for HLS, viewer page, stats API
  rtmpServer: 'rtmp://laantungir.net:1935/publish',  // OBS RTMP server URL
  obsKeyTemplate: '{slug}/src/{SECRET_KEY}',  // OBS stream key pattern
}

URL Derivation from Site Config

Given a site config and a show slug, all URLs are derived:

URL Pattern
Master playlist {streamBaseUrl}/stream/{slug}/stream.m3u8
Viewer page {streamBaseUrl}/stream/{slug}
Stats API {streamBaseUrl}/api/stream/stats?show={slug}
OBS Server {rtmpServer} (verbatim)
OBS Key {obsKeyTemplate} with {slug} replaced

Default Site

The laantungir.net configuration ships as a built-in default:

const DEFAULT_STREAMING_SITE = {
  name: 'laantungir.net',
  streamBaseUrl: 'https://laantungir.net',
  rtmpServer: 'rtmp://laantungir.net:1935/publish',
  obsKeyTemplate: '{slug}/src/{SECRET_KEY}',
};

Persistence

Saved sites are stored in the existing Nostr kind:30078 user settings event under the vjPage namespace, alongside existing settings like streamDraft, autoAnnounce, and columns:

{
  "vjPage": {
    "streamingSites": [
      {
        "name": "laantungir.net",
        "streamBaseUrl": "https://laantungir.net",
        "rtmpServer": "rtmp://laantungir.net:1935/publish",
        "obsKeyTemplate": "{slug}/src/{SECRET_KEY}"
      },
      {
        "name": "my-server.example.com",
        "streamBaseUrl": "https://my-server.example.com",
        "rtmpServer": "rtmp://my-server.example.com:1935/live",
        "obsKeyTemplate": "{slug}/{SECRET_KEY}"
      }
    ],
    "selectedSiteName": "laantungir.net",
    "streamDraft": { ... },
    "autoAnnounce": { ... },
    "columns": { ... }
  }
}

This syncs across devices via Nostr relays using the existing patchUserSettings() / getUserSettings() / onUserSettings() infrastructure.

Architecture

graph TD
    subgraph Settings - kind:30078
        S1[vjPage.streamingSites - array of site configs]
        S2[vjPage.selectedSiteName - last used site]
    end

    subgraph UI - vj.html Column 1
        U1[Site Selector Dropdown]
        U2[Add/Edit Site Form]
        U3[Delete Site Button]
    end

    subgraph vj-stream.mjs
        V1[currentSiteConfig - active site object]
        V2[deriveShowUrls - slug + site -> all URLs]
        V3[OBS hints display]
        V4[Stats polling]
        V5[HLS player source]
        V6[Stream event publishing]
    end

    S1 -->|load on init| U1
    S2 -->|restore selection| U1
    U1 -->|on change| V1
    U2 -->|save| S1
    V1 --> V2
    V2 --> V3
    V2 --> V4
    V2 --> V5
    V2 --> V6

UI Design

Site Selector Dropdown

Placed at the very top of the STREAM section in Column 1, above the SHOW selector. This is the first thing the user configures — which server am I streaming to?

┌─────────────────────────────────┐
│ STREAM SITE                     │
│ ┌─────────────────────────────┐ │
│ │ laantungir.net          ▼  │ │  <-- dropdown of saved sites
│ └─────────────────────────────┘ │
│ [+ Add Site]  [✎ Edit]  [🗑]   │  <-- action buttons
│                                 │
│ SHOW                            │
│ ┌─────────────────────────────┐ │
│ │ — Select a show —       ▼  │ │
│ └─────────────────────────────┘ │
│ ...                             │

Add/Edit Site Form

A collapsible panel that appears when clicking "+ Add Site" or "✎ Edit":

┌─────────────────────────────────┐
│ Site Name                       │
│ ┌─────────────────────────────┐ │
│ │ my-server.example.com       │ │
│ └─────────────────────────────┘ │
│ Stream Base URL                 │
│ ┌─────────────────────────────┐ │
│ │ https://my-server.example.. │ │
│ └─────────────────────────────┘ │
│ RTMP Server                     │
│ ┌─────────────────────────────┐ │
│ │ rtmp://my-server.example..  │ │
│ └─────────────────────────────┘ │
│ OBS Key Template                │
│ ┌─────────────────────────────┐ │
│ │ {slug}/src/{SECRET_KEY}     │ │
│ └─────────────────────────────┘ │
│ [Save Site]  [Cancel]           │
└─────────────────────────────────┘

Implementation Steps

Step 1 — Define site data model and refactor deriveShowUrls in vj-stream.mjs

File: www/js/vj-stream.mjs

  • Add DEFAULT_STREAMING_SITE constant with laantungir.net config
  • Add let currentSiteConfig = { ...DEFAULT_STREAMING_SITE } to module state
  • Refactor deriveShowUrls(slug) to use currentSiteConfig instead of hardcoded STREAM_BASE_URL:
    function deriveShowUrls(slug) {
      const site = currentSiteConfig;
      const base = site.streamBaseUrl.replace(/\/+$/, '');
      const safeSlug = normalizeShowSlug(slug);
      if (!safeSlug) {
        return {
          masterPlaylist: '',
          viewerPage: '',
          stats: '',
          obsServer: site.rtmpServer,
          obsKeyTemplate: site.obsKeyTemplate,
        };
      }
      return {
        masterPlaylist: `${base}/stream/${safeSlug}/stream.m3u8`,
        viewerPage: `${base}/stream/${safeSlug}`,
        stats: `${base}/api/stream/stats?show=${encodeURIComponent(safeSlug)}`,
        obsServer: site.rtmpServer,
        obsKeyTemplate: site.obsKeyTemplate.replace(/\{slug\}/g, safeSlug),
      };
    }
    
  • Remove the module-level STREAM_BASE_URL constant (replaced by currentSiteConfig.streamBaseUrl)
  • Add a setCurrentSiteConfig(siteConfig) function that updates currentSiteConfig, refreshes OBS hints, and re-derives URLs for the current show
  • Export setCurrentSiteConfig and getCurrentSiteConfig from the panel return object

Step 2 — Add streaming site selector HTML to vj.html

File: www/vj.html

Insert a new "STREAM SITE" section at the top of #vjStreamCard, before the existing STREAM section:

<div class="streamSection">
  <div class="musicSidebarTitle">STREAM SITE</div>
  <select id="selectStreamingSite" class="streamInput" aria-label="Select streaming site">
    <option value="laantungir.net">laantungir.net</option>
  </select>
  <div style="display:flex; gap:4px; margin-top:4px;">
    <button id="btnAddStreamingSite" class="btn musicMiniBtn" type="button">+ Add</button>
    <button id="btnEditStreamingSite" class="btn musicMiniBtn" type="button">✎ Edit</button>
    <button id="btnDeleteStreamingSite" class="btn musicMiniBtn" type="button">🗑</button>
  </div>
  <div id="streamingSiteFormPanel" class="hidden" style="margin-top:6px;">
    <input id="inputSiteName" class="streamInput" placeholder="Site name (e.g. my-server.com)" />
    <input id="inputSiteStreamBaseUrl" class="streamInput" placeholder="Stream base URL (e.g. https://my-server.com)" />
    <input id="inputSiteRtmpServer" class="streamInput" placeholder="RTMP server (e.g. rtmp://my-server.com:1935/publish)" />
    <input id="inputSiteObsKeyTemplate" class="streamInput" placeholder="OBS key template (e.g. {slug}/src/{SECRET_KEY})" />
    <div style="display:flex; gap:4px; margin-top:4px;">
      <button id="btnSaveStreamingSite" class="btn" type="button">Save Site</button>
      <button id="btnCancelStreamingSite" class="btn" type="button">Cancel</button>
    </div>
  </div>
</div>

Step 3 — Add site management elements to vj-stream.mjs els object

File: www/js/vj-stream.mjs

Add to the els object:

  • selectStreamingSite
  • btnAddStreamingSite, btnEditStreamingSite, btnDeleteStreamingSite
  • streamingSiteFormPanel
  • inputSiteName, inputSiteStreamBaseUrl, inputSiteRtmpServer, inputSiteObsKeyTemplate
  • btnSaveStreamingSite, btnCancelStreamingSite

Step 4 — Wire site selector logic in vj-stream.mjs

File: www/js/vj-stream.mjs

Add to wireButtons():

  • selectStreamingSite change: Update currentSiteConfig from the selected site, refresh all derived URLs, update OBS hints, re-set HLS player source if a show is selected
  • btnAddStreamingSite click: Show the form panel in "add" mode (clear fields)
  • btnEditStreamingSite click: Show the form panel in "edit" mode (pre-fill from current selection)
  • btnDeleteStreamingSite click: Remove the selected site from the list (prevent deleting the last site), persist, select the first remaining site
  • btnSaveStreamingSite click: Validate inputs, add/update the site in the list, persist via callback, select the new/updated site
  • btnCancelStreamingSite click: Hide the form panel

Add new functions:

  • populateSiteDropdown(sites, selectedName) — rebuilds the <select> options
  • getSelectedSiteConfig() — returns the site config matching the dropdown selection
  • applySiteSelection(siteName) — sets currentSiteConfig, refreshes URLs, updates OBS hints

Step 5 — Wire persistence in vj.html

File: www/vj.html

In the initVjStreamPanel() call, pass two new callbacks:

  • onSitesChanged(sites, selectedName) — called when the sites list or selection changes; triggers queueVjSettingsPatch({ streamingSites: sites, selectedSiteName: selectedName })
  • getSavedSites() — returns { sites: [...], selectedName: '...' } from getVjPageSettingsNode(pageSettings)

In applyVjPageSettingsFromState():

  • Read vjNode.streamingSites and vjNode.selectedSiteName
  • If streamingSites is a non-empty array, pass it to the stream panel to populate the dropdown
  • If empty/missing, use the default laantungir.net site
  • Restore the selectedSiteName selection

Step 6 — Update initialize() to accept initial site config

File: www/js/vj-stream.mjs

In initialize():

  • Accept an optional { sites, selectedSiteName } parameter
  • If provided, populate the dropdown and apply the selection before discovering shows
  • If not provided, use the default laantungir.net site

Add to the return object:

  • setSites(sites, selectedSiteName) — for live settings updates via onUserSettings

Step 7 — Remove hardcoded OBS server text from vj.html

File: www/vj.html

Change the hardcoded OBS server display at line 1485:

<!-- Before -->
<span id="spanObsServer" class="streamInfoValue">rtmp://laantungir.net:1935/publish</span>

<!-- After -->
<span id="spanObsServer" class="streamInfoValue"></span>

The actual value will be set dynamically by updateObsHints() using currentSiteConfig.

Files Changed

File Changes
www/js/vj-stream.mjs Refactor URL derivation, add site config state, add site selector wiring, add site management functions, update exports
www/vj.html Add site selector HTML, wire persistence callbacks, restore site selection from settings

Key Technical Notes

  1. Backward compatibility: If no streamingSites exist in settings, the default laantungir.net config is used — existing users see no change
  2. Site name as key: The name field serves as the unique identifier for sites in the dropdown and settings
  3. No validation of server reachability: We trust the user to enter correct URLs; the stats polling and HLS player will naturally show errors if the server is unreachable
  4. OBS key template: The {slug} placeholder in the template is replaced with the actual show slug; {SECRET_KEY} remains as a literal placeholder the user replaces manually in OBS
  5. Monochrome URL is separate: The auto-announce URL base (inputAutoAnnounceUrlBase) is not affected by site selection — it stays in its own section