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
#divBodyflex 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:
mountComposerfrom post-composer.mjsrenderPostItem,renderAuthorHeader,formatTimeAgofrom post-interactions.mjsinitPostCardsfrom post-interactions2.mjs
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:
post-composer.mjs— chat inputpost-interactions.mjs— message rendering, entity hydrationinit-ndk.mjs— auth, subscribe, publishEvent- Existing template boilerplate (sidenav, footer, relay UI, etc.)
Dependencies
- hls.js (CDN) — for HLS stream playback, loaded only when needed