# 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 |
