Skip to content
Player showcase

Demo data ​

approved

The versioned demo library: video documents, configuration shapes, and the dummy feed, with media served from a remote bucket.

Purpose ​

Two layers with distinct jobs: videos own the facts, configs own the shape, and the demo pages marry them. A developer copies a fully expanded configuration from the showcase and sees the player. The player itself never knows this folder exists; it only ever receives complete configurations.

Folder layout ​

text
data/
  README.md                 how this folder works, how to add a video or a config
  videos/
    index.json              ordered array of video ids
    <id>/video.json         one video document per id
  configs/
    index.json              ordered array of config ids
    <id>.json               one configuration shape per id
  feeds/
    publications-feed.json             the dummy feed: ordered portrait video ids

Video documents ​

The content library. Each document carries facts only: the config contract's content section plus the share URLs. It never contains layout, controls, capabilities, or strings decisions.

json
{
  "id": "quantify-genocide-impact",
  "content": {
    "title": "How can we quantify the immediate and lingering impact of genocide?",
    "author": "Diego Alburez-Gutierrez",
    "description": "A short synopsis used by share targets.",
    "poster": "https://demo.lt.org/videos/portrait/quantify-genocide-impact/poster.jpg",
    "duration": 609.4,
    "sources": [
      {
        "src": "https://demo.lt.org/videos/portrait/quantify-genocide-impact/video.mp4",
        "type": "video/mp4"
      }
    ],
    "tracks": [
      {
        "kind": "subtitles",
        "label": "English",
        "srclang": "en",
        "src": "https://demo.lt.org/videos/portrait/quantify-genocide-impact/en.vtt",
        "default": true
      }
    ],
    "chapters": [{ "name": "Question", "start": 9 }],
    "metadata": {
      "inANutshell": { "generatedByAi": true, "keyTakeaway": "…", "summary": "…" }
    }
  },
  "share": {
    "canonicalUrl": "https://lt.org/publication/example",
    "embedUrl": "https://lt.org/embed/example"
  }
}

Library composition:

  • Ten portrait videos, each carrying the full metadata set: every context panel view populated (in a nutshell, key concepts, research field, transcript, underlying publication, related videos), so each publication demonstrates the panel completely, including its refresh across feed swipes.
  • Three landscape videos: one with chapters and two subtitle languages, one with the full metadata set, one minimal.

The library ​

Every video has a dedicated page with the playing video, its document, and copyable bucket links. Portrait titles and metadata are invented demo fiction over real media; chapters are the real ones. Every portrait document carries a default English subtitle track under its authoritative media path and a non-empty transcript generated from the same real-audio transcription. The transcript sets generatedByAi: true, keeps entries chronological and non-empty, and keeps every entry start within the documented video duration. Fictional titles and editorial metadata never influence captions or transcripts.

Portrait ​

Landscape ​

Configuration shapes ​

Each file in configs/ is final-form in everything except content, and sources its content one of two ways:

  • "video": "<id>": the configuration borrows that video document's content and its share URLs.
  • Inline content: the configuration stands entirely on its own, used verbatim.
json
{
  "id": "portrait-standalone",
  "video": "quantify-genocide-impact",
  "config": {
    "version": 3,
    "layout": "portrait",
    "controls": { "fullscreen": false },
    "capabilities": { "chapters": true }
  }
}

Expansion rule: the demo tooling produces the final configuration by taking config, injecting the referenced video's content, and injecting its share URLs into capabilities.share (unless the configuration sets its own). The id and video keys are demo bookkeeping and are never part of the expanded configuration.

The showcase shell ​

The development showcase keeps its complete surface reachable through shared navigation:

The header brand provides the Home destination; Home is not repeated as a separate navigation control.

  • Home (/)
  • Landscape (/landscape/)
  • Portrait (/portrait/)
  • Context panel (/context-panel/)
  • Publications feed (/publications-feed/)
  • Configs (/configs/; see The configs page)
  • States (/states/; see The states page)
  • Embed example (/embed/yannic-vitz/)
  • Docs (/docs/)

The shared navigation and Home page link to a representative embed route. The embed document itself remains intentionally chrome-free so the embedded player is the only interface inside an iframe.

The configs page ​

The showcase renders each configuration shape as a live player next to its fully expanded JSON with a copy control. A video picker over the library re-expands the configuration with another video's data on the spot. What is displayed, and what copy copies, is always the complete, final-form configuration: paste it into any page, get this player.

The states page ​

The showcase route /states/ is a development harness for the core player's configuration error, startup media error, mid-playback recovery, buffering, and ended presentations. Each scenario owns an independent player. The page drives players only through public configuration and the page API; its scoped service worker changes only explicitly tagged demo media requests so one scenario can never disturb another player or another showcase route.

The embed page ​

The showcase route /embed/<video-id>/ resolves <video-id> against the same data/videos/<video-id>/video.json library and mounts one landscape player that fills the viewport. The static deployment rewrites those document-shaped URLs to the shared embed entry without changing the browser URL. Showcase share configuration points its embed URL at this route so the generated iframe snippet completes the round trip on both local and deployed showcase origins.

The dummy feed ​

feeds/publications-feed.json holds { "items": [ …the ten portrait video ids… ] } in feed order. The feed page resolves the ids to documents, composes complete portrait configurations, and hands the feed module exactly its contract input (see the publications feed document).

Media bucket ​

  • All binary assets (video files, posters, subtitle tracks) live in a Cloudflare R2 bucket, grouped by orientation: videos/<orientation>/<id>/poster.jpg, video.mp4, <lang>.vtt, with <orientation> being landscape or portrait. A document's own poster, playback sources, and subtitle tracks must use its id and one shared orientation in that path; this is the authoritative orientation used by the library and feeds. Related-video posters are exempt because they intentionally point at another document and never classify their host. The repository library stays flat (data/videos/<id>/) and ids are unique across orientations.
  • The bucket is managed as infrastructure, outside this repository: lt-demo-media, served at the public base URL https://demo.lt.org (see lt-infra docs/cloudflare-r2.md for the module definition and upload runbook).
  • CORS is a hard requirement: subtitle tracks are fetched with CORS, so the bucket must send Access-Control-Allow-Origin for the showcase origin and local development, and the player sets crossorigin on its media element. Plain video playback would work without it; captions would silently fail.

Fetching model ​

  • Documents, configuration shapes, and the feed file are fetched as static JSON from the showcase itself; the folder ships with the demo build. Media streams from the bucket.
  • Automated tests import the same files and treat media URLs as opaque values; tests never fetch media, so test determinism does not depend on the bucket.
  • Documents and bucket can drift (a document referencing an object that was never uploaded). The demo build guards against this: it HEAD-requests every media URL the documents reference and fails on a missing object.

Adding a video ​

  1. Create videos/<id>/video.json with the document.
  2. Upload the media to the bucket under videos/<orientation>/<id>/.
  3. Append the id to videos/index.json, and to feeds/publications-feed.json if it is a portrait publication. Before adding a portrait video to the feed, give it the full metadata set plus the required default English subtitle track and real-audio transcript described above.

Adding a configuration shape ​

  1. Create configs/<id>.json, referencing a video or inlining content.
  2. Append the id to configs/index.json.