Shorts: - Status now "decisions locked"; intro lists the real scope (exclusion + publish). - Remove stale "candidate signals / confirm in Phase 0" preamble (D4 locked). - Fix inaccuracy: native shorts are kind-30078 + isShort, not kind-22. - Moderation section says locked + notes the NSFW no-autoplay rule. - Rewrite Risks to the locked decisions: main-feed-exclusion regression risk, native-marker-must-round-trip risk, WebTorrent-swarm-storm on autoplay, search scope sub-decision. Drop the now-inverted double-listing/aspect-ratio risks. Live: - Status "decisions locked"; scope reads read→post chat + host zaps. - Config flags corrected: introduces FEATURE_LIVE_INGEST *and* FEATURE_LIVE_CHAT_POST (Phase 3b); "two flags" footprint fixed; chat-post UI gated on both. - Phase 0 now "confirm field mapping + default relays + zap target" (not "lock decisions", already locked). - Moderation: read-chat WoT-mute filtering + post-chat rate/stance as design items. - Add risks: high-volume chat buffering, planned/scheduled streams, multiple streaming renditions, private/token-gated streams. Zap-target resolution noted. Co-Authored-By: Claude Opus 4.8 <[email protected]>
22 KiB
Shorts (short-form vertical video) — Dev Plan
TODO ref: #16b in todo/TODO_2026-06-20_pre-launch.md.
Status: DECISIONS LOCKED — ready to build (D1–D5, D7, D8 locked; D6 is a
build-time implementation choice). Not started.
Short-form vertical video is NIP-71 kind 22 (the short counterpart to kind 21 normal video), plus the addressable short kind 34236. It is a different feature from live streams (#16, NIP-53) — do not conflate them.
The good news: bitvid already ingests kind 22. The inbound NIP-71 adapter
(js/nostr/nip71IngestAdapter.js, NIP71_KINDS = {21, 22, 34235, 34236}) parses
short-form notes today and they already flow into the main feed. So this feature
is not new ingest code — it is (a) a single isShort discriminator (native
marker / foreign kind), (b) a dedicated Shorts sidebar tab with a vertical
UI, (c) a feed source that lists only shorts, (d) exclusion of shorts from the
main/discovery feeds (DECISION 3), and (e) a publish-side content-type
selector + native short marker (DECISION 7) so bitvid creators can post shorts.
Everything ships behind a config flag that leaves no trace when off.
Decisions needed
DECISION 1 — Discovery scope. ✅ LOCKED: Option A (same whitelist scope). Shorts pulls from the same whitelisted/WoT authors as the main feed and reuses the existing render-time moderation filter — no new moderation surface, no extra relay fan-out. Rationale (maintainer): the existing moderation + ingest system is already good; keep Shorts consistent with it. A broader/Explore-style scope can be added later if desired.
DECISION 2 — Moderation / NSFW. ✅ LOCKED: Option A (inherit, no special handling). Shorts pass through the same render-time
filterVideospath as every grid (event blacklist / author blacklist / WoT mute / NSFW / private). No short-specific moderation. Note: the NSFW gate already excludes NSFW from users who haven't opted in, so the immersive autoplay feed carries no extra NSFW-exposure risk — opted-in users chose to see it, everyone else never receives it. Rationale (maintainer): no reason to treat shorts differently. One agreed safeguard: even for opted-in users, an NSFW short does NOT autoplay in the immersive feed — it stays blurred/paused with a tap-to-play affordance (autoplay resumes on the next non-NSFW short). This is the only short-specific behavior; all filtering still inherits the shared pipeline.
DECISION 3 — Do shorts also appear in the main feed? ✅ LOCKED: Option B (Shorts tab ONLY; excluded from the main/discovery feeds). Rationale (maintainer): shorts should not dilute the main feed at all — the Shorts tab is their only home. Build implication: add an
isShortexclusion to the general feeds — Recent, For You, Explore, Trending, Kids — so kind-22/short videos never render there (they currently do via NIP-71 ingest). The Shorts feed is the inverse filter (isShortonly). This touches each feed source/sorter, so verify no feed regressions (couples with #20/#21 feed-identity work). Sub-point (recommend, not blocking): a creator's shorts SHOULD still appear on their Channel profile page (it's their full catalogue) and in Subscriptions is TBD — default: exclude from Subscriptions too (it's a discovery feed), keep on Channel profile. Confirm during Phase 1.
DECISION 4 — What counts as a "short"? ✅ LOCKED: Option A (kind is the definer). A video is a short iff its event kind is 22 or 34236; kind 21/34235 = regular. No aspect-ratio heuristic — the publisher's declared kind decides. Rationale (maintainer): shorts are typically vertical, but the protocol kind is the source of truth; a vertical video mis-published as regular just renders in the 16:9 card (thumbnail center-cropped via
object-fit:cover; the player letterboxes it viaobject-fit:contain, never distorts) and stays in the main feed — the publisher's choice. Aspect-ratio fallback can be added later if too many real shorts leak in as kind-21.
DECISION 5 — UI ambition for v1. ✅ LOCKED: Option C (phased). Phase 1 ships the grid + portrait card + the main-feed exclusion (DECISION 3) so shorts are corralled into their own tab immediately (the maintainer's priority); Phase 2 adds the full immersive autoplay/swipe feed later. Front-loads "shorts out of the main feed," defers the heavier immersive build.
DECISION 6 — Card design. Shorts need a new portrait (9:16) card — the existing
js/ui/components/VideoCard.jshardcodes a 16:9 landscape media ratio (ratio-16-9), so it can't render shorts correctly as-is. Two options: (a) a newShortCardcomponent (portrait thumbnail, compact meta, its owndata-testid); or (b) a portrait "mode"/variant ofVideoCarddriven by a flag (e.g.variant: "short"swapping the ratio class + layout). Recommendation: (b) a portrait variant ofVideoCardif the internals are clean enough to parameterize the ratio + layout — it inherits thumbnail binding, moderation badges, engagement controls, and the ⋯ menu for free; fall back to (a) a dedicatedShortCardonly if forcing a variant makesVideoCardunwieldy or pushes it past its size cap. Either way this is a first-class deliverable, not an afterthought — the grid can't ship without it.
DECISION 7 — Publish side: content-type selector + native short marker. ✅ AGREED (maintainer-proposed). Add an upload-modal content-type selector (Regular video ⟷ Short) with visual + text helpers so creators pick the right type and understand placement — e.g. landscape/portrait icons + copy like "Shorts appear only in the Shorts tab, not the main feed." This is not just UX: bitvid's canonical event is kind 30078 (not 21/22), and the schema has no short marker today (short is only inferred from dimensions at mirror time). So this decision also adds an explicit
isShort(orformat: "short") field to the v3 content schema, set by the selector, which (a) lets the Shorts feed filter + main-feed exclusion catch bitvid-native shorts, and (b) forces the NIP-71 mirror to kind 34236. Detection becomes: a video is a short iffkind ∈ {22,34236}(foreign/ingested) ORisShort === true(bitvid-native). Open sub-choices for build time: default selection (recommend "Regular"), whether to auto-suggest "Short" when the uploaded file is portrait, and exact copy/icons.
DECISION 8 — Native short representation. ⏳ LEANING: marker on kind 30078 (both options mirror to 34236). Hard requirement (maintainer): a native short MUST keep bitvid's WebTorrent advantage AND mirror to the standard NIP-71 short kind via the existing mirror system. Both viable options satisfy that — they produce the same external result (a WebTorrent-rich native event + a standard 34236 mirror carrying the magnet via
imeta):
- Option 1 (recommended) — kind 30078 +
isShortmarker. Native short = a normal bitvid video flagged short; the existing mirror emits 34236. No new kind. Reuses 100% of the publish/edit/delete/moderation/view-count/zap/mirror pipeline (all keyed on 30078). Least code, least risk.- Option 2 — a distinct bitvid-short kind that mirrors to 34236. A separate kind number for rich native shorts + the mirror to 34236 for interop. Trade-off: the external result is identical to Option 1, but the distinct native kind is non-standard (only the 34236 mirror is recognized by other clients) and it forks every pipeline that's currently keyed on 30078 — i.e. more work for the same outcome. A new kind buys a distinct native kind number; it does not buy WebTorrent or interop that Option 1 lacks.
Net: Option 1 already delivers "WebTorrent + mirror to the standard kind" without minting a kind. Only pick Option 2 if a distinct native kind number is itself a requirement.
✅ LOCKED: Option 1 (marker on kind 30078), provided shorts are reliably distinguishable. They are: a single
isShortboolean discriminates them everywhere — set from theisShort/format:"short"content field for native videos and fromkind ∈ {22,34236}for ingested ones (see Field mapping). The Shorts feed filtersisShort === true; the main feeds exclude it.
Config flag (off = no trace)
Add FEATURE_SHORTS to config/instance-config.js (default false), threaded
through js/config.js → js/constants.js exactly like FEATURE_NIP71_INGEST.
When the flag is off, all of the following must be absent (the "as if it were never added" requirement):
- Sidebar tab — the Shorts link is not rendered in
components/sidebar.html(not merelyhidden). - Feed registration —
FEED_TYPES.SHORTSis not registered injs/app/feedCoordinator.js(no pipeline, no sorter). - Subscription/source — no shorts-specific relay subscription is created.
- Route —
#view=shortsfalls back to the default view (a stale bookmark can't resurrect the tab). - Publish UI — the upload-modal content-type selector (DECISION 7) is not rendered; uploads are plain regular videos exactly as today.
- Management UI — no Shorts sub-tab in profile → My Videos (see below).
- Config surface — the flag is the only footprint.
Publish & management UI (flag-gated)
Both the publish and management surfaces must obey "off = no trace" (points 5–6 above), and reuse patterns already in the app.
- Upload modal — content-type selector (DECISION 7). Regular ⟷ Short, with
visual + text placement helpers ("Shorts appear only in the Shorts tab").
Rendered only when
FEATURE_SHORTSis on; otherwise uploads are plain regular videos exactly as today. - Profile → My Videos — sub-tabs. Reuse the admin-pane sub-tab toolbar
pattern just shipped (
ProfileAdminControllersub-tabs) to split My Videos into Videos / Shorts / Live sub-tabs for easier content management. Each sub-tab appears only when its feature flag is on:FEATURE_SHORTSoff → no Shorts sub-tab.- Live sub-tab is tied to publishing live (#16c /
FEATURE_LIVE_PUBLISH), NOT ingest (#16) — with ingest-only there is nothing of the user's to manage, so no Live sub-tab. - All flags off → no sub-tab bar at all, just the plain My Videos list exactly as today (true "no trace").
- Videos sub-tab lists regular bitvid/NIP-71 videos (excludes
isShort); Shorts sub-tab lists the user'sisShortvideos.
Field mapping — detecting a "short"
Per DECISION 4 + 7 (locked), isShort is derived from kind for foreign/ingested
videos and from an explicit marker for bitvid-native videos (kind 30078):
| Signal | Applies to | Meaning |
|---|---|---|
kind === 22 |
ingested foreign | NIP-71 regular short |
kind === 34236 |
ingested foreign | NIP-71 addressable short |
isShort / format:"short" in kind-30078 content |
bitvid-native | set by the upload-modal content-type selector (DECISION 7) |
imeta dim height > width |
(future fallback) | portrait aspect; not used in v1 (DECISION 4) — kept only as a possible later fallback |
Plan: surface a single isShort boolean on the bitvid video object —
nip71IngestAdapter.js sets it from the source kind (it already tracks kind);
the native publish path sets it from the new content-schema field. The Shorts
feed filters isShort === true; the main feeds exclude isShort === true
(DECISION 3). No re-fetching needed.
Architecture
Reuses the existing feed engine + ingest; new pieces are thin.
- Flag:
FEATURE_SHORTS(instance-config → config → constants). - Feed type: add
SHORTS: "shorts"toFEED_TYPES(js/constants.js). - Feed source/registration: register
FEED_TYPES.SHORTSinjs/app/feedCoordinator.js, sourced from the already-populated active-video cache filtered toisShort(per DECISION 1, scoped to the same authors as the main feed). A dedicated sorter can be chronological or reuse the For-You scorer — TBD, likely chronological for v1. - Sidebar tab: add a Shorts link in
components/sidebar.html, rendered only when the flag is on. - View:
views/shorts.html(a portrait-oriented grid container). - Card design (new): a portrait 9:16 card — either a
variant: "short"mode ofjs/ui/components/VideoCard.js(which is currently 16:9 /ratio-16-9) or a dedicatedShortCard(DECISION 6). Portrait thumbnail, compact title + author, reuse of thumbnail binding, moderation badges, engagement + ⋯ menu. New design tokens if needed (existing--video-card-*tokens live incss/tokens.css). A distinctdata-testidfor e2e selectors. - Player UI: v1 opens a short in the existing player modal with a portrait layout; v2 adds a full-screen vertical swipe/next feed.
- Diversity: apply the existing
spreadAuthorspass (feed sorters) so the Shorts tab doesn't cluster by creator, consistent with For You / Trending.
Short-form UX best practices (researched July 2026)
Short-form is a mobile-first format (~70%+ of social video is watched on phones, held vertically), but bitvid is a desktop-capable web app, so the Shorts experience must be responsive — the same feed, two presentations. Sources at the bottom of this doc.
The immersive feed (both platforms)
- One short at a time, full-height 9:16, not a scroll-list of small cards. The grid (Phase 1) is the entry point; tapping/clicking a short drops into an immersive one-at-a-time feed (Phase 2).
- Autoplay + loop. The active short autoplays and loops. Browser autoplay policy requires muted autoplay — so start muted with an obvious unmute, and persist the user's mute choice across shorts. Exception (agreed): NSFW- flagged shorts do not autoplay even for opted-in users — blurred/paused with tap-to-play (see DECISION 2).
- Preload the next 2–3. Buffer upcoming shorts in the background so advancing is instant. "If your platform shows a loading spinner between videos, you've already lost the session." (This is the single most-cited make-or-break detail.)
- Overlaid chrome, safe zones. Title/author bottom-left, an action rail bottom-right, controls over the frame — keep them inside 9:16 safe zones so they don't collide with the video edges.
- Action rail (bitvid mapping): Zap (bitvid's primary engagement, via the existing NWC/zap system — replaces the generic "like"), Comment, Share, and author avatar + follow/subscribe. Reuse the existing engagement + ⋯ menu wiring.
- Captions/muted-friendliness. A large share of viewing happens muted at some point; surface captions when a short provides them and make the mute state obvious. (bitvid can't generate captions — show them when present.)
- Accessibility: respect
prefers-reduced-motion(no auto-advance / disable scroll-snap animation), full keyboard support, focus management on advance.
Mobile presentation
- Full-screen vertical player; swipe up/down = next/previous; tap = pause/play; the action rail sits on the right thumb-reach edge.
Desktop presentation
- Centered vertical player with letterboxed / blurred side panels — never stretch a 9:16 short to fill a wide viewport (this is the YouTube-Shorts-on- desktop pattern).
- Keyboard navigation is a real differentiator. YouTube Shorts on desktop has notoriously weak keyboard support (users install extensions to get it). bitvid should ship it natively: ↑/↓ = prev/next short, Space = pause/play, M = mute, ←/→ = seek (or prev/next — pick one and document it), Esc = exit to grid.
- Action rail beside the player (to the right of the letterboxed video) rather than overlaid, since desktop has the horizontal room.
- Advance via scroll-snap (wheel/trackpad) and the arrow keys; a subtle next/prev affordance for mouse users.
bitvid-specific notes
- Playback rides the existing URL-first + WebTorrent pipeline (
playbackService); shorts are ordinary videos (native kind-30078 +isShort, or ingested 21/22/34235/34236), so no new transport. - WebTorrent + autoplay caution: an autoplaying scroll feed must NOT spin up a torrent swarm per short as the user flies past. In the immersive feed, prefer URL-first for the active/preloaded shorts and only fall back to WebTorrent on demand (e.g. when the URL fails, or the short is dwelt on) — otherwise scrolling seeds dozens of swarms and thrashes the connection budget. (Risks section.)
- Zaps (not likes) are the headline action — a genuine differentiator vs the incumbents that already exists in bitvid.
- View counts, watch history, comments, and zaps reuse the existing per-video machinery — shorts are just videos to those systems.
- Everything still flows through the render-time moderation filter (DECISION 2).
Phases (each flag-gated from day one)
- Phase 0 — Detection + flag (small). Add
FEATURE_SHORTS(off), theisShortcontent-schema field (DECISION 7), andisShortderivation (foreign = kind, native = marker) in the ingest adapter + mirror, with unit tests. No UI. - Phase 0b — Publish-side selector (small–medium, DECISION 7). Upload-modal
content-type selector (Regular ⟷ Short) with visual + text placement helpers;
wires the
isShortmarker + forces mirror kind 34236. Lets bitvid creators actually publish shorts (without it, the Shorts tab only shows ingested foreign shorts). - Phase 1 — Tab + feed + portrait card + main-feed exclusion (medium).
FEED_TYPES.SHORTS, sidebar link,views/shorts.html, feed registration filtered toisShort, route gated by flag, and the new portrait 9:16 card (DECISION 6). Per DECISION 3 (B): add theisShortexclusion to the main/discovery feeds (Recent, For You, Explore, Trending, Kids; likely Subscriptions) so shorts never appear outside the Shorts tab — with regression checks on each feed. Opens in the standard player for now. - Phase 2 — Immersive vertical feed (medium–large). The one-at-a-time
responsive feed described in "Short-form UX best practices": muted autoplay +
loop, preload next 2–3 (no spinner between shorts), overlaid action rail
(Zap/Comment/Share/follow), captions-when-present,
prefers-reduced-motionhandling. Mobile: full-screen + swipe up/down. Desktop: centered letterboxed player + native keyboard nav (↑/↓ prev/next, Space, M, Esc) + side action rail. Grid (Phase 1) remains the entry point. - Phase 3 — Polish. Mute toggle persistence, per-tab empty states, captions surfacing, seek affordance, and the URL-first-for-autoplay WebTorrent guard.
Moderation, whitelist & NSFW
Per DECISION 2 (locked: inherit): shorts pass through the same render-time
filterVideos path (blacklist event ids, author blacklist, WoT mute, NSFW gate,
private exclusion) as every other grid. No new moderation surface. The admin
per-event block list (#25) already applies to any event id, shorts included. The
one short-specific behavior is that NSFW-flagged shorts do not autoplay in the
immersive feed (tap-to-play), even for opted-in users — a playback rule, not a
filtering change.
Risks / watch-items
- Main-feed exclusion touches every feed (DECISION 3). Adding the
isShortexclusion to Recent / For You / Explore / Trending / Kids (and likely Subscriptions) is the riskiest part — a bug drops the wrong videos or lets shorts leak. Regression-test each feed; couples with the #20/#21 feed-identity work. A single sharedisShortpredicate (used positively by Shorts, negatively by the others) keeps it consistent. - A native short missing its marker leaks into the main feed. Since native
detection is the
isShortcontent field (not aspect ratio), a bitvid short must reliably carry the marker on publish AND on edit/re-publish, or it reappears in the main feed. Cover publish + edit paths; the marker must round-trip through the mirror/auto-sync. - WebTorrent swarm storm in the autoplay feed. Preloading + autoplaying a scroll feed over WebTorrent would spin up many swarms at once. Prefer URL-first for active/preloaded shorts; fall back to WebTorrent on demand (see Architecture → bitvid notes). Respect the existing connection budget.
- Empty tab — a whitelist-scoped source (DECISION 1) on a small instance may have few shorts; ship a clear empty state.
- Search scope (sub-decision, defer to build). Should shorts appear in search? They're excluded from the browse feeds (DECISION 3) but a user searching a creator/topic arguably still wants their shorts. Recommend: shorts remain searchable and open in the Shorts player context; confirm during Phase 1.
Sources
- NIP-71 (video events; kinds 21/22 regular, 34235/34236 addressable).
- Existing code:
js/nostr/nip71IngestAdapter.js,js/nostr/nip71Mirror.js,js/app/feedCoordinator.js,js/feedEngine/sorters.js,components/sidebar.html,js/ui/components/VideoCard.js(16:9 card to variant/replace). docs/nip71-migration-plan.md(dual-event model, short=34236 heuristic).- Short-form UX research (July 2026):
- Top 10 Features Every Short Video App Needs in 2026 — Primocys (full-screen player, autoplay/loop, swipe-next, preload next 2–3)
- Short-Form Video Strategy 2026 — Teleprompter (9:16 mobile-first, overlaid chrome/safe zones, captions)
- Why Vertical Video Is Becoming the Default Format — Oyelabs (mobile share, vertical engagement)
- Keyboard shortcuts for YouTube — YouTube Help & YouTube Arrow Keys Fix (Chrome Web Store) (desktop Shorts keyboard-nav gap → do it natively)