Files
client/plans/vj-streaming-spec-alignment.md
T
2026-04-17 16:52:51 -04:00

271 lines
11 KiB
Markdown

# VJ Page — Streaming Spec Alignment Plan
Aligns [`vj.html`](www/vj.html) and [`vj-stream.mjs`](www/js/vj-stream.mjs) with the updated self-streaming spec at [`self-streaming/README.md`](../self-streaming/README.md).
## Current State vs Spec
| Area | Current | Spec requires |
|------|---------|---------------|
| Show identity | Hardcoded `DEFAULT_STREAM_D_TAG = 'stream-main'` | User-selectable show slug as d-tag |
| Show selector UI | None | Dropdown of user's published kind:30311 events + create new |
| Episode tracking | None | `episode` tag, auto-increment on each Go Live |
| Default streaming URL | `https://laantungir.net/hls/stream.m3u8` | `https://laantungir.net/stream/{slug}/stream.m3u8` |
| Video element src | Hardcoded `src="https://laantungir.net/hls/stream.m3u8"` | Dynamic, set after slug is known |
| Stats URL | `https://laantungir.net/stream/stats` | `https://laantungir.net/api/stream/stats?show={slug}` |
| Stats response shape | `{ viewers, hls_viewers, rtmp_viewers, live }` | `{ live, viewers, variants: { src: {viewers}, ... } }` |
| Viewer count display | `TOTAL: N \| HLS: N \| RTMP: N` | `TOTAL: N \| src: N \| 720p: N \| 480p: N` |
| NIP-53 tags | `d, title, status, summary, image, streaming` | Add `episode`, `web` tags |
| OBS config display | None | Show OBS server + key derived from slug |
| Kind:30078 playlist | None | Episode playlist with track + viewers tags |
| URL derivation | Manual input | All URLs derived from slug automatically |
| OBS server format | N/A | `rtmp://laantungir.net:1935/publish` with key `{slug}/src/{SECRET_KEY}` |
## Implementation Steps
### Step 1 — Remove hardcoded d-tag and add show slug state
In [`vj-stream.mjs`](www/js/vj-stream.mjs):
- Remove `const DEFAULT_STREAM_D_TAG = 'stream-main'` at [line 3](www/js/vj-stream.mjs:3)
- Add a helper to derive all URLs from a slug:
```js
const STREAM_BASE_URL = 'https://laantungir.net';
function deriveShowUrls(slug) {
return {
masterPlaylist: `${STREAM_BASE_URL}/stream/${slug}/stream.m3u8`,
viewerPage: `${STREAM_BASE_URL}/stream/${slug}`,
stats: `${STREAM_BASE_URL}/api/stream/stats?show=${encodeURIComponent(slug)}`,
obsServer: 'rtmp://laantungir.net:1935/publish',
obsKeyTemplate: `${slug}/src/{SECRET_KEY}`,
};
}
```
- Replace every reference to `DEFAULT_STREAM_D_TAG` with the selected show slug
- Replace the hardcoded `STATS_URL` constant with a dynamic URL derived from the slug
### Step 2 — Add show selector/creator UI to vj.html
Add new HTML elements at the top of [`#vjStreamCard`](www/vj.html:1311), before the video element:
```html
<div class="musicSidebarTitle">SHOW</div>
<select id="selectShow" class="streamInput">
<option value="">— Select a show —</option>
</select>
<div style="display:flex; gap:6px; align-items:center;">
<input id="inputNewShowSlug" class="streamInput" placeholder="new-show-slug" style="flex:1;" />
<button id="btnCreateShow" class="btn" type="button">Create</button>
</div>
```
Wire up in [`vj-stream.mjs`](www/js/vj-stream.mjs):
- Add `selectShow`, `inputNewShowSlug`, `btnCreateShow` to the `els` object
- On initialize, fetch the user's published `kind:30311` events and populate the dropdown
- On show selection: set `streamDTag` to the slug, derive URLs, pre-fill title/summary/image/episode from the last event
- On create: validate slug format (lowercase, hyphens, no reserved words), add to dropdown, select it
- Slug validation regex: `/^[a-z0-9][a-z0-9-]*[a-z0-9]$/` (min 2 chars, no leading/trailing hyphens)
### Step 3 — Add episode tracking
In [`vj.html`](www/vj.html), add episode field after the title field (around [line 1322](www/vj.html:1322)):
```html
<div id="streamEpisodeIdDisplay" class="streamFieldDisplay" title="Episode ID (timestamp)">Episode ID: —</div>
<div id="streamEpisodeDescriptionDisplay" class="streamFieldDisplay" title="Click to edit episode description">Episode description (optional)</div>
<input id="inputStreamEpisodeDescription" class="streamInput hidden" placeholder="Episode description (optional)" />
```
In [`vj-stream.mjs`](www/js/vj-stream.mjs):
- Add `streamEpisodeIdDisplay`, `streamEpisodeDescriptionDisplay`, and `inputStreamEpisodeDescription` to `els`
- Keep **episode ID** as a timestamp (generated on Go Live; reused through the live session)
- Keep **episode description** as optional editable text (`001`, `Season 2`, freeform)
- Store/persist only the optional episode description in local draft/settings
### Step 4 — Update `buildStreamTags()` to include `episode` and `web` tags
In [`buildStreamTags()`](www/js/vj-stream.mjs:116):
```js
function buildStreamTags({ dTag, title, summary, image, streamingUrl, episode, status }) {
const tags = [
['d', dTag],
['title', String(title || '').trim() || 'Untitled stream'],
['status', String(status || 'planned').trim() || 'planned']
];
const safeSummary = String(summary || '').trim();
const safeImage = String(image || '').trim();
const safeStreaming = String(streamingUrl || '').trim();
const safeEpisode = String(episode || '').trim();
if (safeSummary) tags.push(['summary', safeSummary]);
if (safeImage) tags.push(['image', safeImage]);
if (safeStreaming) tags.push(['streaming', safeStreaming]);
if (safeEpisode) tags.push(['episode', safeEpisode]);
// web tag: viewer page URL derived from slug
const webUrl = `${STREAM_BASE_URL}/stream/${dTag}`;
tags.push(['web', webUrl]);
return tags;
}
```
Update [`publishStreamStatus()`](www/js/vj-stream.mjs:376) to pass `episode` from the input field.
### Step 5 — Update all hardcoded URLs to slug-derived URLs
Replace every occurrence of `https://laantungir.net/hls/stream.m3u8`:
| Location | Change |
|----------|--------|
| [`vj-stream.mjs:290`](www/js/vj-stream.mjs:290) | Use `deriveShowUrls(streamDTag).masterPlaylist` |
| [`vj-stream.mjs:336`](www/js/vj-stream.mjs:336) | Same |
| [`vj-stream.mjs:459`](www/js/vj-stream.mjs:459) | Same |
| [`vj-stream.mjs:786`](www/js/vj-stream.mjs:786) | Same |
| [`vj.html:1313`](www/vj.html:1313) | Remove hardcoded `src` attribute from `<video>` |
| [`vj.html:1323`](www/vj.html:1323) | Remove hardcoded display text |
| [`vj.html:1324`](www/vj.html:1324) | Remove hardcoded `value` from input |
The streaming URL input should auto-populate when a show is selected, and the video player source should be set dynamically.
### Step 6 — Update stats URL and response parsing
In [`vj-stream.mjs`](www/js/vj-stream.mjs):
- Remove the static `const STATS_URL = 'https://laantungir.net/stream/stats'` at [line 4](www/js/vj-stream.mjs:4)
- In [`pollViewerCount()`](www/js/vj-stream.mjs:440), use the slug-derived stats URL:
```js
const statsUrl = deriveShowUrls(streamDTag).stats;
const res = await fetch(statsUrl);
```
- Update response parsing to handle the new shape:
```js
const data = await res.json();
viewerCount = Number(data.viewers) || 0;
const isLive = Boolean(data.live);
const variants = data.variants || {};
if (els.divViewerCount) {
const variantParts = Object.entries(variants)
.map(([name, v]) => `${name}: ${v.viewers || 0}`)
.join(' | ');
els.divViewerCount.textContent = `TOTAL: ${viewerCount}${variantParts ? ' | ' + variantParts : ''}`;
}
```
### Step 7 — Add OBS configuration display
In [`vj.html`](www/vj.html), add after the stream controls div (around [line 1340](www/vj.html:1340)):
```html
<div id="divObsConfig" style="font-family:monospace; font-size:11px; padding:6px 8px; background:rgba(255,255,255,0.02); border-radius:6px; color:var(--text-color); line-height:1.6;">
<div>OBS Server: <span id="spanObsServer">rtmp://laantungir.net:1935/publish</span></div>
<div>OBS Key: <span id="spanObsKey">{slug}/src/{SECRET_KEY}</span></div>
</div>
```
In [`vj-stream.mjs`](www/js/vj-stream.mjs):
- Add `spanObsServer` and `spanObsKey` to `els`
- Update OBS key display whenever the show slug changes:
```js
if (els.spanObsKey) {
els.spanObsKey.textContent = `${streamDTag}/src/{SECRET_KEY}`;
}
```
### Step 8 — Update video element in HTML
In [`vj.html:1313`](www/vj.html:1313), change:
```html
<video id="videoStream" playsinline src="https://laantungir.net/hls/stream.m3u8" muted></video>
```
To:
```html
<video id="videoStream" playsinline muted></video>
```
The source is set dynamically by [`setStreamPlayerSource()`](www/js/vj-stream.mjs:204) after the show slug is known.
### Step 9 — Add kind:30078 episode playlist support (song-triggered updates)
Add to [`vj-stream.mjs`](www/js/vj-stream.mjs):
**New constants/state:**
```js
const PLAYLIST_KIND = 30078;
let playlistDTag = ''; // show-playlist:{show}:{episodeTimestamp}
let playlistTracks = [];
let playlistViewerSnapshots = [];
```
**Behavior change (approved):**
- No periodic 30s timer.
- Update/publish the `kind:30078` event **only when a new song starts**.
- On each song start:
1. Append next `track` tag
2. Capture one viewer snapshot (`viewers` tag) from latest stats data
3. Republish full replaceable event for this episode
**Integration points:**
- On "Go Live": generate timestamp episode ID and initialize 30078 state for this episode
- In [`announceNowPlaying()`](www/js/vj-stream.mjs:573): append track + viewer snapshot and republish `kind:30078`
- On "End Stream": publish final `kind:30078` with `status=ended`
This keeps event volume low and naturally aligns viewer snapshots with track changes.
### Step 10 — Fetch user's shows on initialization
In [`initialize()`](www/js/vj-stream.mjs:771), after auth is resolved:
- Subscribe to the user's `kind:30311` events (all d-tags, not just one)
- Collect unique d-tags as the user's shows
- Populate the show selector dropdown
- If a URL parameter specifies a show (via `?a=` or `?naddr=`), auto-select it
- If no URL parameter, select the most recently updated show (or leave blank for new users)
This requires a one-time query:
```js
subscribe({
kinds: [STREAM_KIND],
authors: [streamAuthorPubkey],
limit: 50
}, { closeOnEose: true, cacheUsage: 'CACHE_FIRST' });
```
Collect results, deduplicate by d-tag (keep latest), populate dropdown.
## File Change Summary
| File | Changes |
|------|---------|
| [`www/js/vj-stream.mjs`](www/js/vj-stream.mjs) | Remove `DEFAULT_STREAM_D_TAG` and `STATS_URL` constants; add `deriveShowUrls()` helper; add show selector logic; add episode tracking; update `buildStreamTags()` with `episode` + `web` tags; update `pollViewerCount()` for new stats shape; add kind:30078 playlist support; update `initialize()` to fetch user's shows; replace all hardcoded URLs |
| [`www/vj.html`](www/vj.html) | Add show selector/creator UI; add episode field; add OBS config display; remove hardcoded `src` from video element; remove hardcoded URL defaults from display/input elements |
## Dependency Diagram
```mermaid
flowchart TD
S1["Step 1: Remove hardcoded d-tag, add deriveShowUrls"] --> S2["Step 2: Show selector UI"]
S1 --> S5["Step 5: Replace all hardcoded URLs"]
S1 --> S6["Step 6: Update stats URL + parsing"]
S2 --> S3["Step 3: Episode tracking"]
S2 --> S10["Step 10: Fetch user shows on init"]
S3 --> S4["Step 4: Update buildStreamTags with episode + web"]
S5 --> S8["Step 8: Update video element"]
S2 --> S7["Step 7: OBS config display"]
S4 --> S9["Step 9: Kind:30078 episode playlist"]
S6 --> S9
```
Steps 1-8 are the core alignment work. Step 9 (kind:30078 playlist) is the largest addition and can be implemented as a follow-up if needed. Step 10 is required for the show selector to be useful.