248 lines
8.5 KiB
Markdown
248 lines
8.5 KiB
Markdown
# Stream Control Page (`stream-ctrl.html`)
|
|
|
|
## Purpose
|
|
|
|
A personal live-streaming control dashboard for the streamer. Unlike `stream.html` (which is viewer-facing), `stream-ctrl.html` is the streamer's cockpit — managing NIP-53 stream events, posting kind 1 announcements, monitoring viewer count, and participating in stream chat.
|
|
|
|
## Architecture Overview
|
|
|
|
```mermaid
|
|
flowchart TD
|
|
subgraph StreamCtrl[stream-ctrl.html]
|
|
A[Kind 1 Announcement Composer]
|
|
B[Stream Controls - NIP-53 kind:30311]
|
|
C[Video Player - self-monitor]
|
|
D[Viewer Count Display]
|
|
E[Chat Composer - kind:1311]
|
|
F[Chat Feed]
|
|
end
|
|
|
|
subgraph Server[laantungir.net]
|
|
G[nginx-rtmp stats endpoint]
|
|
H[Public JSON stats proxy]
|
|
end
|
|
|
|
subgraph Nostr[Nostr Relays]
|
|
I[kind:1 announcements]
|
|
J[kind:30311 stream events]
|
|
K[kind:1311 chat messages]
|
|
end
|
|
|
|
A -->|publishEvent| I
|
|
B -->|publishEvent| J
|
|
E -->|publishEvent| K
|
|
D -->|poll /stream/stats| H
|
|
H -->|parse XML| G
|
|
D -->|update current_participants| J
|
|
```
|
|
|
|
## Changes from `stream.html`
|
|
|
|
### What stays
|
|
- Video player (for self-monitoring your own stream)
|
|
- Stream info section with controls (Save Planned / Go Live / End Stream)
|
|
- Stream control inputs (URL, title, summary, image)
|
|
- Chat composer (kind:1311) and chat feed
|
|
- Sidenav with relay/blossom/AI sections
|
|
- All existing auth, hamburger, footer infrastructure
|
|
|
|
### What changes
|
|
|
|
| Area | Change |
|
|
|------|--------|
|
|
| Page title | `STREAM` → `STREAM CTRL` |
|
|
| Header text | `STREAM` → `STREAM CTRL` |
|
|
| Default streaming URL | Pre-fill `rtmp://laantungir.net:1935/live/stream` |
|
|
| Discover streams sidebar | **Remove** — not needed for control page |
|
|
| Kind 1 composer | **Add** — new announcement composer section above stream info |
|
|
| Viewer count | **Add** — live viewer count from server stats endpoint |
|
|
| CSS imports | **Add** `post-composer.css` for the announcement composer styling |
|
|
|
|
### What gets removed
|
|
- `#divDiscoverStreams` HTML section from sidenav
|
|
- `#divDiscoverStreamsList`, `.discoverStreamCard`, `.discoverStreamThumb`, `.discoverStreamMeta`, `.discoverStreamStatus` CSS
|
|
- `subscribeToDiscoveredStreams()`, `renderDiscoveredStreams()`, `upsertDiscoveredStream()` JS functions
|
|
- `discoverSubId`, `discoveredStreams` variables
|
|
|
|
## Detailed Implementation
|
|
|
|
### 1. Page Title and Header
|
|
|
|
Update `<title>` to `STREAM CTRL` and the `divHeaderText.textContent` assignment in `main()` from `'STREAM'` to `'STREAM CTRL'`.
|
|
|
|
### 2. Pre-fill Default Streaming URL
|
|
|
|
In `initializeStreamPage()`, after setting up `streamAuthorPubkey` and before `subscribeToStream()`, set:
|
|
```js
|
|
if (!inputStreamingUrl.value) {
|
|
inputStreamingUrl.value = 'rtmp://laantungir.net:1935/live/stream';
|
|
}
|
|
```
|
|
|
|
### 3. Kind 1 Announcement Composer
|
|
|
|
Add a new HTML section between the video player and stream info:
|
|
|
|
```html
|
|
<div id="divAnnouncementComposer" class="divPostItem">
|
|
<div class="divPostHeader">
|
|
<div class="divPostAuthorName">Announcement</div>
|
|
<div class="divPostTime">kind:1 post</div>
|
|
</div>
|
|
<!-- composer mounts here dynamically -->
|
|
</div>
|
|
```
|
|
|
|
Mount using the same pattern as [`mountTopComposer()`](www/post.html:721):
|
|
|
|
```js
|
|
async function mountAnnouncementComposer() {
|
|
const container = document.getElementById('divAnnouncementComposer');
|
|
if (!container) return;
|
|
|
|
const hostEl = document.createElement('div');
|
|
hostEl.className = 'topPostComposerInput';
|
|
container.appendChild(hostEl);
|
|
|
|
let composer = null;
|
|
composer = mountComposer(hostEl, {
|
|
currentPubkey,
|
|
followedProfiles: [],
|
|
showUploadIcon: true,
|
|
showPreview: true,
|
|
autoHideOnSubmit: false,
|
|
onSubmit: async (content) => {
|
|
const text = String(content || '').trim();
|
|
if (!text) return;
|
|
|
|
await publishEvent({
|
|
kind: 1,
|
|
content: text,
|
|
tags: [],
|
|
created_at: Math.floor(Date.now() / 1000)
|
|
});
|
|
|
|
composer?.clear();
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
Call `mountAnnouncementComposer()` from `initializeStreamPage()` after auth check.
|
|
|
|
### 4. Remove Discover Streams
|
|
|
|
- Delete the `#divDiscoverStreams` div from the sidenav HTML
|
|
- Remove all discover-related CSS rules
|
|
- Remove `subscribeToDiscoveredStreams()` call from `initializeStreamPage()`
|
|
- Remove `renderDiscoveredStreams()` call from `initializeStreamPage()`
|
|
- Remove the JS functions: `renderDiscoveredStreams`, `upsertDiscoveredStream`, `subscribeToDiscoveredStreams`
|
|
- Remove the `discoverSubId` and `discoveredStreams` variables
|
|
- Remove the discover stream handling from `handleIncomingEvent()`
|
|
|
|
### 5. CSS Import
|
|
|
|
Add `<link rel="stylesheet" href="./css/post-composer.css" />` in the `<head>` section, and add width rules for `#divAnnouncementComposer` to match the existing content width pattern.
|
|
|
|
### 6. Server: Public Stats Endpoint
|
|
|
|
On `laantungir.net`, add an nginx `location` block that proxies to the internal RTMP stats XML and converts it to a simple JSON response. Two approaches:
|
|
|
|
**Option A — nginx + XSLT module** (preferred, no extra service):
|
|
```nginx
|
|
location /stream/stats {
|
|
add_header Access-Control-Allow-Origin *;
|
|
add_header Content-Type application/json;
|
|
# Use a small proxy_pass + sub_filter or a tiny script
|
|
proxy_pass http://127.0.0.1:8080/stat;
|
|
# Parse with a small server-side script
|
|
}
|
|
```
|
|
|
|
**Option B — Small Python/bash CGI** (simpler to implement):
|
|
Add a small Python HTTP handler (similar to `rtmp-auth.py`) that:
|
|
1. Fetches `http://127.0.0.1:8080/stat` (nginx-rtmp XML stats)
|
|
2. Parses the XML to extract `nclients` for the `live/stream` application
|
|
3. Returns JSON: `{"viewers": N, "live": true/false}`
|
|
|
|
Managed as a systemd service (`stream-stats.service`) on a local port (e.g., 8092), proxied through nginx at `https://laantungir.net/stream/stats`.
|
|
|
|
### 7. Viewer Count Display
|
|
|
|
Add a viewer count element in the stream info section:
|
|
|
|
```html
|
|
<div id="divViewerCount" class="divPostTime">👁 — viewers</div>
|
|
```
|
|
|
|
Poll the stats endpoint every 15 seconds:
|
|
|
|
```js
|
|
const STATS_URL = 'https://laantungir.net/stream/stats';
|
|
const STATS_POLL_INTERVAL = 15000;
|
|
let viewerCount = 0;
|
|
let statsIntervalId = null;
|
|
|
|
async function pollViewerCount() {
|
|
try {
|
|
const res = await fetch(STATS_URL);
|
|
const data = await res.json();
|
|
viewerCount = data.viewers || 0;
|
|
const el = document.getElementById('divViewerCount');
|
|
if (el) el.textContent = `👁 ${viewerCount} viewer${viewerCount !== 1 ? 's' : ''}`;
|
|
} catch (e) {
|
|
console.warn('[stream-ctrl] Stats poll failed:', e);
|
|
}
|
|
}
|
|
|
|
function startViewerCountPolling() {
|
|
pollViewerCount();
|
|
statsIntervalId = setInterval(pollViewerCount, STATS_POLL_INTERVAL);
|
|
}
|
|
```
|
|
|
|
### 8. Update NIP-53 Event with Viewer Count
|
|
|
|
In `publishStreamStatus()`, include the `current_participants` tag from the polled viewer count:
|
|
|
|
```js
|
|
// Inside buildStreamTags or publishStreamStatus:
|
|
if (viewerCount > 0) {
|
|
tags.push(['current_participants', String(viewerCount)]);
|
|
}
|
|
```
|
|
|
|
## Page Layout (top to bottom)
|
|
|
|
```
|
|
┌─────────────────────────────────┐
|
|
│ Header: STREAM CTRL │
|
|
├─────────────────────────────────┤
|
|
│ Video Player (self-monitor) │
|
|
├─────────────────────────────────┤
|
|
│ Announcement Composer (kind:1) │
|
|
│ [rich text input + send button] │
|
|
├─────────────────────────────────┤
|
|
│ Stream Info + Controls │
|
|
│ Title / Status / Summary │
|
|
│ 👁 N viewers │
|
|
│ [URL] [Title] [Summary] [Image] │
|
|
│ [Save Planned] [Go Live] [End] │
|
|
├─────────────────────────────────┤
|
|
│ Chat Composer (kind:1311) │
|
|
├─────────────────────────────────┤
|
|
│ Chat Feed │
|
|
├─────────────────────────────────┤
|
|
│ Footer: relay status │
|
|
└─────────────────────────────────┘
|
|
```
|
|
|
|
## Files Modified
|
|
|
|
| File | Type | Description |
|
|
|------|------|-------------|
|
|
| `www/stream-ctrl.html` | Modified | All client-side changes (HTML, CSS, JS) |
|
|
| Server: nginx config | New location block | `/stream/stats` proxy |
|
|
| Server: stats service | New Python script + systemd unit | Parse RTMP XML → JSON |
|
|
| `self-streaming/README.md` | Updated | Document the new stats endpoint |
|