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

12 KiB

Streaming Page Plan

Overview

Add a streaming page (stream.html) for users who stream live content. The page implements NIP-53 Live Activities using kind:30311 (Live Streaming Event) and kind:1311 (Live Chat Message). The streamer can manage their live event, view their stream, and interact with chat — all using existing components like mountComposer() and post-interactions.mjs.

NIP-53 Summary

kind:30311 — Live Streaming Event (addressable)

An addressable event the streamer publishes and updates throughout the stream lifecycle:

tags:
  ["d", "<unique-id>"]
  ["title", "<stream title>"]
  ["summary", "<description>"]
  ["image", "<preview image url>"]
  ["streaming", "<url>"]           // HLS/m3u8 or other stream URL
  ["status", "planned|live|ended"]
  ["starts", "<unix timestamp>"]
  ["ends", "<unix timestamp>"]
  ["t", "<hashtag>"]
  ["current_participants", "<n>"]
  ["total_participants", "<n>"]
  ["p", "<pubkey>", "<relay>", "<role>"]
  ["relays", "wss://...", ...]
  ["recording", "<url>"]           // post-stream recording
  ["pinned", "<event-id>"]         // pinned chat message

kind:1311 — Live Chat Message

Chat messages scoped to a live event via an a tag:

tags:
  ["a", "30311:<pubkey>:<d-tag>", "<relay>", "root"]
content: "<chat message text>"

Architecture

graph TD
    A[stream.html] --> B[init-ndk.mjs - auth + NDK]
    A --> C[post-composer.mjs - chat input]
    A --> D[post-interactions.mjs - render chat messages]
    A --> E[Video Player - HLS embed]
    
    subgraph Nostr Events
        F[kind:30311 - Live Event - publish/subscribe]
        G[kind:1311 - Chat Messages - publish/subscribe]
    end
    
    A --> F
    A --> G
    C -->|onSubmit publishes kind:1311| G
    D -->|renders incoming kind:1311| G

CSS Strategy — Reuse client.css

The existing client.css and post-composer.css provide nearly everything needed. Here is the mapping:

Existing classes to reuse (NO new CSS needed)

Element Existing class/ID Source
Body flex column layout #divBody override pattern client.css:356 — override with flex-direction: column !important per rules
Buttons .btn client.css:591
Post cards for chat messages .divPostItem client.css:900
Author header in chat .divPostHeader, .divPostHeaderMain, .divPostProfileLink client.css:1360
Avatar .divPostAvatar, .avatar--sm client.css:1424, client.css:1395
Author name .divPostAuthorName client.css:1438
Post content .divPostContent client.css:930
Timestamp .divPostTime client.css:973
Interaction bar .divPostInteractions, .interaction-item client.css:990
Comment threads .divCommentThread, .divCommentsList client.css:1148
Hidden/visible .clsHidden, .clsVisible client.css:878
Nostr embeds .nostr-embed, .nostr-mention, etc. client.css:215
Composer wrapper .post-composer-wrapper, .post-composer-wrapper--inline post-composer.css:1
Video in posts .divPostContent video client.css:951

Minimal page-specific <style> additions (inline in stream.html)

Only these styles need to be added in the page <style> block, following the CSS placement rules:

/* Override divBody to column layout per client.css rules */
#divBody {
  flex-direction: column !important;
  flex-wrap: nowrap !important;
  align-items: center !important;
  justify-content: flex-start !important;
  align-content: flex-start !important;
  gap: 10px;
  overflow-y: auto;
}

/* Width constraint for all main sections — same pattern as post.html */
#divStreamPlayer,
#divStreamInfo,
#divChatFeed,
#divChatComposer {
  width: 80%;
  min-width: 300px;
  max-width: 700px;
}

/* Video player: 16:9 responsive container */
#divStreamPlayer {
  position: relative;
  padding-top: 56.25%; /* 16:9 */
  background: #000;
  border-radius: 10px;
  overflow: hidden;
}

#divStreamPlayer video {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* Stream info card — uses CSS vars, no hardcoded colors */
#divStreamInfo {
  background: var(--card-background, var(--secondary-color));
  border: 1px solid var(--border-color);
  border-radius: 10px;
  padding: 12px;
}

/* Status badge */
.stream-status-badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--border-radius);
  font-size: 80%;
  font-weight: bold;
  border: 1px solid var(--border-color);
}

.stream-status-badge.live {
  color: var(--accent-color);
  border-color: var(--accent-color);
}

/* Chat feed: scrollable, fixed height */
#divChatFeed {
  max-height: 400px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 5px;
}

/* Compact chat message — lighter than full .divPostItem */
.chatMessage {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 5px 10px;
  border-bottom: 1px solid var(--border-color);
  font-size: 90%;
}

.chatMessage .divPostAuthorName {
  font-size: 12px;
}

/* Chat composer override for inline mode */
#divChatComposer .post-composer-wrapper {
  width: 100%;
}

These are all page-specific, use only CSS variables, and follow the CSS placement rules documented in client.css.

Page Layout

+--------------------------------------------------+
| HEADER: STREAM                    [avatar]        |
+--------------------------------------------------+
|                                                    |
|  +--------------------------------------------+   |
|  |                                            |   |
|  |          VIDEO PLAYER AREA                 |   |
|  |     embed via streaming tag URL            |   |
|  |     or placeholder if no stream URL        |   |
|  |                                            |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | STREAM INFO                                |   |
|  | Title / Summary / Status / Tags            |   |
|  | [Go Live] [End Stream] [Edit]              |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | LIVE CHAT                                  |   |
|  | [chat message 1]                           |   |
|  | [chat message 2]                           |   |
|  | [chat message 3]                           |   |
|  | ...                                        |   |
|  +--------------------------------------------+   |
|                                                    |
|  +--------------------------------------------+   |
|  | CHAT COMPOSER (post-composer)               |   |
|  | [type message...] [Send]                   |   |
|  +--------------------------------------------+   |
|                                                    |
+--------------------------------------------------+
| FOOTER                                            |
+--------------------------------------------------+

Implementation Steps

1. Update stream.html boilerplate

  • Change title from TEMPLATE to STREAM
  • Change header text to STREAM
  • Add CSS stylesheets: post-composer.css
  • Add viewport meta tag

2. Add page-specific CSS

  • #divBody flex column layout, centered, scrollable
  • Video player container: 16:9 aspect ratio, max-width 700px, responsive
  • Stream info card: styled like profile card in post.html
  • Chat feed: scrollable container, max-height, auto-scroll to bottom
  • Chat messages: compact card style with avatar, name, timestamp, content
  • Chat composer: fixed at bottom of chat area

3. Add HTML structure in divBody

  • #divStreamPlayer — video element or placeholder
  • #divStreamInfo — title, summary, status badge, tags, controls
  • #divStreamControls — Go Live / End Stream / Edit buttons
  • #divChatFeed — scrollable chat message list
  • #divChatComposer — host element for mountComposer

4. JavaScript module imports

Add to existing imports:

5. Subscribe to own kind:30311 live event

On auth:

subscribe(
  { kinds: [30311], authors: [currentPubkey] },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);

Listen for ndkEvent and filter for kind 30311 to populate stream info.

6. Publish kind:30311 live event

When streamer clicks Go Live:

publishEvent({
  kind: 30311,
  content: '',
  tags: [
    ['d', streamId],
    ['title', title],
    ['summary', summary],
    ['streaming', streamUrl],
    ['status', 'live'],
    ['starts', String(Math.floor(Date.now() / 1000))],
    ['t', ...hashtags],
    ['image', imageUrl]
  ]
});

When streamer clicks End Stream, republish with status: ended and ends timestamp.

7. Video player

  • Use native <video> element for direct URLs
  • For HLS streams (.m3u8), use hls.js CDN: https://cdn.jsdelivr.net/npm/hls.js@latest
  • Detect URL type and initialize accordingly
  • Show placeholder when no streaming URL is set

8. Subscribe to kind:1311 chat messages

Once the live event a tag is known:

subscribe(
  { kinds: [1311], '#a': [`30311:${currentPubkey}:${dTag}`] },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);

Render each incoming chat message in the chat feed using a simplified version of renderPostItem or a custom compact renderer.

9. Wire post-composer for chat

mountComposer(divChatComposer, {
  currentPubkey,
  showUploadIcon: false,
  showPreview: false,
  onSubmit: async (content) => {
    await publishEvent({
      kind: 1311,
      content: content,
      tags: [
        ['a', `30311:${currentPubkey}:${dTag}`, '', 'root']
      ]
    });
  }
});

10. Chat message rendering

For each kind:1311 event received:

  • Fetch/cache author profile (avatar, display name)
  • Render compact chat bubble: [avatar] [name] [time]: [message]
  • Auto-scroll chat feed to bottom on new messages
  • Support NIP-27 nostr: mentions via hydrateNostrEntities

11. Stream controls

  • Go Live button: Prompts for title, streaming URL, optional summary/image/tags. Publishes kind:30311 with status=live.
  • End Stream button: Updates kind:30311 with status=ended, adds ends timestamp.
  • Edit button: Allows updating title, summary, image, tags while live.
  • Status badge: Shows planned/live/ended with color coding.

12. URL parameter support

  • ?naddr=... or ?a=30311:pubkey:dtag — View someone else's stream
  • When viewing another stream, hide stream controls, show chat in read+write mode
  • Parse naddr using NostrTools.nip19.decode

Files Modified

File Changes
www/stream.html Full page implementation: HTML structure, CSS, JS module

No New JS Modules Required

All functionality is achieved by reusing:

Dependencies

  • hls.js (CDN) — for HLS stream playback, loaded only when needed