347 lines
12 KiB
Markdown
347 lines
12 KiB
Markdown
# Streaming Page Plan
|
|
|
|
## Overview
|
|
|
|
Add a streaming page (`stream.html`) for users who stream live content. The page implements [NIP-53 Live Activities](reference_repos/nips/53.md) 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()`](www/js/post-composer.mjs:152) and [`post-interactions.mjs`](www/js/post-interactions.mjs:1).
|
|
|
|
## 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
|
|
|
|
```mermaid
|
|
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`](www/css/client.css:1) and [`post-composer.css`](www/css/post-composer.css:1) 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`](www/css/client.css:356) — override with `flex-direction: column !important` per rules |
|
|
| Buttons | `.btn` | [`client.css:591`](www/css/client.css:591) |
|
|
| Post cards for chat messages | `.divPostItem` | [`client.css:900`](www/css/client.css:900) |
|
|
| Author header in chat | `.divPostHeader`, `.divPostHeaderMain`, `.divPostProfileLink` | [`client.css:1360`](www/css/client.css:1360) |
|
|
| Avatar | `.divPostAvatar`, `.avatar--sm` | [`client.css:1424`](www/css/client.css:1424), [`client.css:1395`](www/css/client.css:1395) |
|
|
| Author name | `.divPostAuthorName` | [`client.css:1438`](www/css/client.css:1438) |
|
|
| Post content | `.divPostContent` | [`client.css:930`](www/css/client.css:930) |
|
|
| Timestamp | `.divPostTime` | [`client.css:973`](www/css/client.css:973) |
|
|
| Interaction bar | `.divPostInteractions`, `.interaction-item` | [`client.css:990`](www/css/client.css:990) |
|
|
| Comment threads | `.divCommentThread`, `.divCommentsList` | [`client.css:1148`](www/css/client.css:1148) |
|
|
| Hidden/visible | `.clsHidden`, `.clsVisible` | [`client.css:878`](www/css/client.css:878) |
|
|
| Nostr embeds | `.nostr-embed`, `.nostr-mention`, etc. | [`client.css:215`](www/css/client.css:215) |
|
|
| Composer wrapper | `.post-composer-wrapper`, `.post-composer-wrapper--inline` | [`post-composer.css:1`](www/css/post-composer.css:1) |
|
|
| Video in posts | `.divPostContent video` | [`client.css:951`](www/css/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](www/css/client.css:7):
|
|
|
|
```css
|
|
/* 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](www/css/client.css:7) 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:
|
|
- [`mountComposer`](www/js/post-composer.mjs:152) from post-composer.mjs
|
|
- [`renderPostItem`](www/js/post-interactions.mjs:803), [`renderAuthorHeader`](www/js/post-interactions.mjs:216), [`formatTimeAgo`](www/js/post-interactions.mjs:906) from post-interactions.mjs
|
|
- [`initPostCards`](www/js/post-interactions2.mjs:5) from post-interactions2.mjs
|
|
|
|
### 5. Subscribe to own kind:30311 live event
|
|
On auth:
|
|
```js
|
|
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:
|
|
```js
|
|
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:
|
|
```js
|
|
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
|
|
```js
|
|
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`](www/stream.html:1) | Full page implementation: HTML structure, CSS, JS module |
|
|
|
|
## No New JS Modules Required
|
|
All functionality is achieved by reusing:
|
|
- [`post-composer.mjs`](www/js/post-composer.mjs:1) — chat input
|
|
- [`post-interactions.mjs`](www/js/post-interactions.mjs:1) — message rendering, entity hydration
|
|
- [`init-ndk.mjs`](www/js/init-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
|