Files
bitvid/docs/shorts-plan.md
thePR0M3TH3ANandClaude Opus 4.8 c5f91ce683 [docs] Review pass on shorts + live plans: fix stale/contradictory bits, close gaps
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]>
2026-07-01 12:48:58 -04:00

368 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `filterVideos` path 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 `isShort` **exclusion** 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 (`isShort` only). 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 via `object-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.js` hardcodes a **16:9 landscape** media
> ratio (`ratio-16-9`), so it can't render shorts correctly as-is. Two options:
> (a) a **new `ShortCard` component** (portrait thumbnail, compact meta, its own
> `data-testid`); or (b) a **portrait "mode"/variant of `VideoCard`** driven by a
> flag (e.g. `variant: "short"` swapping the ratio class + layout).
> *Recommendation: (b) a portrait variant of `VideoCard` if 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 dedicated `ShortCard` only if forcing a variant makes
> `VideoCard` unwieldy 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` (or `format:
> "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 iff `kind ∈ {22,34236}` (foreign/ingested) **OR** `isShort === 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 + `isShort` marker.** 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 `isShort` boolean discriminates them
> everywhere — set from the `isShort`/`format:"short"` content field for native
> videos and from `kind ∈ {22,34236}` for ingested ones (see Field mapping). The
> Shorts feed filters `isShort === 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):
1. **Sidebar tab** — the Shorts link is not rendered in `components/sidebar.html`
(not merely `hidden`).
2. **Feed registration** — `FEED_TYPES.SHORTS` is not registered in
`js/app/feedCoordinator.js` (no pipeline, no sorter).
3. **Subscription/source** — no shorts-specific relay subscription is created.
4. **Route** — `#view=shorts` falls back to the default view (a stale bookmark
can't resurrect the tab).
5. **Publish UI** — the upload-modal content-type selector (DECISION 7) is not
rendered; uploads are plain regular videos exactly as today.
6. **Management UI** — no Shorts sub-tab in profile → My Videos (see below).
7. **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_SHORTS` is 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 (`ProfileAdminController` sub-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_SHORTS` off → 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's `isShort` videos.
## 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"` to `FEED_TYPES` (`js/constants.js`).
- **Feed source/registration:** register `FEED_TYPES.SHORTS` in
`js/app/feedCoordinator.js`, sourced from the already-populated active-video
cache filtered to `isShort` (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 of `js/ui/components/VideoCard.js` (which is currently 16:9 / `ratio-16-9`)
or a dedicated `ShortCard` (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 in
`css/tokens.css`). A distinct `data-testid` for 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 `spreadAuthors` pass (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), the
`isShort` content-schema field (DECISION 7), and `isShort` derivation (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 `isShort` marker + 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 to `isShort`, route gated by flag, and the **new portrait 9:16 card**
(DECISION 6). **Per DECISION 3 (B):** add the `isShort` **exclusion** 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-motion`
handling. **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 `isShort`
exclusion 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 shared `isShort` predicate (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 `isShort` content 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](https://primocys.com/blog/top-10-short-video-app-features-2026/) (full-screen player, autoplay/loop, swipe-next, preload next 2–3)
- [Short-Form Video Strategy 2026 — Teleprompter](https://www.teleprompter.com/blog/short-form-video-strategy) (9:16 mobile-first, overlaid chrome/safe zones, captions)
- [Why Vertical Video Is Becoming the Default Format — Oyelabs](https://oyelabs.com/why-vertical-video-is-default-content-format/) (mobile share, vertical engagement)
- [Keyboard shortcuts for YouTube — YouTube Help](https://support.google.com/youtube/answer/7631406) & [YouTube Arrow Keys Fix (Chrome Web Store)](https://chromewebstore.google.com/detail/youtube-arrow-keys-fix/hbnlngeljeofecndhmebgpgpccfnkgjb) (desktop Shorts keyboard-nav gap → do it natively)