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_SITEconstant with laantungir.net config - Add
let currentSiteConfig = { ...DEFAULT_STREAMING_SITE }to module state - Refactor
deriveShowUrls(slug)to usecurrentSiteConfiginstead of hardcodedSTREAM_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_URLconstant (replaced bycurrentSiteConfig.streamBaseUrl) - Add a
setCurrentSiteConfig(siteConfig)function that updatescurrentSiteConfig, refreshes OBS hints, and re-derives URLs for the current show - Export
setCurrentSiteConfigandgetCurrentSiteConfigfrom 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:
selectStreamingSitebtnAddStreamingSite,btnEditStreamingSite,btnDeleteStreamingSitestreamingSiteFormPanelinputSiteName,inputSiteStreamBaseUrl,inputSiteRtmpServer,inputSiteObsKeyTemplatebtnSaveStreamingSite,btnCancelStreamingSite
Step 4 — Wire site selector logic in vj-stream.mjs
File: www/js/vj-stream.mjs
Add to wireButtons():
- selectStreamingSite change: Update
currentSiteConfigfrom 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>optionsgetSelectedSiteConfig()— returns the site config matching the dropdown selectionapplySiteSelection(siteName)— setscurrentSiteConfig, 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; triggersqueueVjSettingsPatch({ streamingSites: sites, selectedSiteName: selectedName })getSavedSites()— returns{ sites: [...], selectedName: '...' }fromgetVjPageSettingsNode(pageSettings)
In applyVjPageSettingsFromState():
- Read
vjNode.streamingSitesandvjNode.selectedSiteName - If
streamingSitesis 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
selectedSiteNameselection
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 viaonUserSettings
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
- Backward compatibility: If no
streamingSitesexist in settings, the default laantungir.net config is used — existing users see no change - Site name as key: The
namefield serves as the unique identifier for sites in the dropdown and settings - 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
- 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 - Monochrome URL is separate: The auto-announce URL base (
inputAutoAnnounceUrlBase) is not affected by site selection — it stays in its own section