Skip to content

Config

approved

The instance configuration: shape, content versus capabilities split, merge semantics, and validation.

Contract versioning

The configuration version equals the player's major version: player 3.x reads configuration version: 3, and the number only ever bumps together with a player major release. One number to know, for the player and its contract alike.

Version 3 redefines the configuration completely relative to the previous player generation; this was a sanctioned breaking change, made while both the player and its consumers are controlled by the same team. The legacy shape (recognizable by its top-level body wrapper) is rejected with a clear legacy-configuration error rather than half-parsed.

Shape

An instance configuration is one JSON object: a version marker, two selectors (layout, language), and four sections:

json
{
  "version": 3,
  "layout": "landscape",
  "language": "de",
  "content": {},
  "controls": {},
  "capabilities": {},
  "strings": {}
}
SectionOwns
layoutThe shape of the instance: landscape or portrait
languageInterface language selection from the shipped catalogs
contentFacts about the video: sources, poster, title, chapters, tracks
controlsVisibility of core elements, boolean or object per control
capabilitiesEnablement and options of LT capabilities
stringsPer-instance wording overrides on top of the selected language

Only layout and content.sources are required; every other section has complete defaults, and the default language is English.

Content

content carries the facts about the video, and nothing about presentation. Time values are always named start and expressed in seconds, everywhere.

json
"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 and previews.",
  "poster": "https://cdn.example.org/videos/842/cover.jpg",
  "duration": 609.4,
  "sources": [
    { "src": "https://cdn.example.org/videos/842/master.m3u8", "type": "application/x-mpegURL" },
    { "src": "https://cdn.example.org/videos/842/video.mp4", "type": "video/mp4" }
  ],
  "tracks": [
    { "kind": "subtitles", "label": "English", "srclang": "en", "src": "https://cdn.example.org/videos/842/en.vtt", "default": true }
  ],
  "chapters": [
    { "name": "Question", "start": 9 },
    { "name": "Method", "start": 62 }
  ],
  "metadata": {
    "inANutshell": { "generatedByAi": true, "keyTakeaway": "…", "summary": "…" },
    "keyConcepts": { "generatedByAi": true, "items": [{ "label": "Induction", "description": "…", "start": 74 }] },
    "researchField": { "generatedByAi": true, "sections": [{ "title": "Climate", "description": "…" }] },
    "transcript": { "generatedByAi": true, "entries": [{ "start": 0, "text": "…" }] },
    "underlyingPublication": { "items": [{ "title": "…", "authors": ["…"], "journal": "…", "abstract": "…", "url": "https://…" }] },
    "relatedVideos": { "generatedByAi": true, "related": [{ "title": "…", "poster": "https://…", "url": "https://…" }], "forYou": [] }
  }
}

Rules and lineage:

  • All asset references are plain URLs; the player performs no asset resolution.
  • The six metadata sections mirror the context panel's views and tabs one to one. Every section accepts a generatedByAi flag, which drives the AI disclaimer in the panel.
  • Key concept items may carry start to make their chips seek.
  • Dropped from the previous generation, deliberately: bio, abstract, and institution (no surface in the redesigned player), CMS asset identifiers (posterId, vttId), and the presentation flags (type, theme, showLogo, isEmbedded, background), which are superseded by layout, controls, and theming. Share URLs live under capabilities.share, so missing share data makes that capability dormant.

Capabilities

The capabilities section switches and shapes the LT capabilities. Behavior lives in each capability's own document; this section is the binding reference for the configuration shape.

The pattern

Every capability is one key. The value is a boolean, or an object where the capability has options:

  • Omitted means enabled. Every capability is enabled by default; an absent key is true.
  • false removes. The capability's surfaces do not exist for this instance, and no user preference resurrects them (removal beats preference).
  • An object enables and configures. Providing options implies true.

Enablement is only half the story: a capability also needs its data. Enabled without data, it is dormant: no surface, no error, exactly as if it were not there. The capability model document defines this availability model; the per-capability data requirements are:

CapabilitySwitchOptionsData that makes it available
chaptersboolean onlynonecontent.chapters non-empty
captionsboolean onlynonecontent.tracks with subtitle or caption kinds
speedboolean or objectoptions: non-empty ascending rate list; shipped default [1, 1.2, 1.5, 2]none, always available
shareboolean or objectcanonicalUrl, embedUrl, playerTrigger, targets, templatesits own canonicalUrl
contextPanelboolean onlynoneany content.metadata section present
json
"capabilities": {
  "chapters": true,
  "captions": true,
  "speed": { "options": [1, 1.2, 1.5, 2] },
  "share": {
    "canonicalUrl": "https://lt.org/publication-plus/example/",
    "embedUrl": "https://lt.org/embed/publication-plus/example/",
    "playerTrigger": false,
    "targets": ["link", "embed", "facebook", "x", "linkedin", "whatsapp", "more"],
    "templates": {
      "message": "Watch \"{title}\" by {author} on Latest Thinking: {url}"
    }
  },
  "contextPanel": true
}

Why booleans at all

chapters, captions, and contextPanel missing from the configuration are considered true: with their data present, the capability is simply on. The reason these keys exist is the other direction: to disable a capability even though the content for it is there. The data stays in content, untouched; the boolean removes the function.

Per-capability rules

  • chapters, captions, contextPanel are pure booleans. They carry no options object, deliberately, until a real need appears. Their entire behavior is driven by their data: chapter entries, tracks, and metadata sections. The context panel's three views appear and disappear purely per metadata section presence; there is no per-view configuration.
  • speed accepts options, replacing the shipped rate list wholesale (arrays replace, per the merge semantics). The list must be non-empty and sorted ascending; a malformed list soft-degrades to the shipped default.
  • share is the one capability whose data lives in its own options rather than in content, because share targets describe pages, not the video file. Without canonicalUrl the capability is dormant and no share triggers render. Omitting embedUrl hides the embed tab in the share dialog while link sharing keeps working; this omission is the intended lever for sites that do not want embedding. playerTrigger defaults to true; setting it to false hides only the player-owned trigger while the capability and openShare() page command remain available. targets is an optional allowlist for link, embed, facebook, x, linkedin, whatsapp, and more; the interface always retains that designed order. An explicit empty list produces no usable share surface. templates.message overrides the text-carrying target template with {title}, {author}, and {url} interpolation. The demo tooling injects both URLs from a video document's share section unless the configuration sets its own (see the demo data document).
  • Unknown capability keys, like unknown keys anywhere, are ignored with the consolidated soft-degrade warning.

Language and strings

The player translates itself. Interface texts (visible labels, tooltips, and the aria-labels of every interactive control) ship inside the player as complete per-language catalogs, and the configuration selects which one an instance uses. Hosting pages never carry translations of the player's interface; they state a language the same way they state a layout.

json
{
  "language": "de"
}

language is optional. When it is omitted, the instance is English. When it is present, it accepts one BCP 47 tag or an ordered preference list, for example ["de-AT", "de"]. The first tag with a shipped catalog wins; English is the terminal fallback and always ships. The selected language also sets the lang attribute on the player root for assistive technology.

The player never auto-detects a language. The page already knows its own locale (from user choice, browser preferences, or geography) and renders its surrounding content accordingly; the player must match the page, not the browser.

Key shape

String keys are flat camelCase (coverButton, tapToUnmute, captionsOff): one level, never nested by owner. Override objects stay flat and copy-pasteable.

Interpolation

String values may declare plain single-brace placeholders such as {seconds}. Each owning module's string table lists the exact placeholder names accepted by each key.

  • Substitution replaces an exact placeholder name with the supplied value converted to text. Repeated occurrences are all replaced.
  • A placeholder without a supplied value, or one not declared for that key, stays verbatim. Extra supplied values are ignored.
  • There is no escaping syntax, plural engine, grammatical inflection, or locale-sensitive number formatting. Defaults with variable counts use abbreviations where practical ({seconds}s, {count} min, {count} sec) so singular and plural values remain valid.
  • Per-instance overrides use the same placeholders as the key they replace. Introducing different placeholder names in an override does not make those names active.

Message templates that carry share content remain capability options rather than catalog strings. Their documented placeholders belong to those templates, independently of this string interpolation contract.

Shipped catalogs

  • Every supported language ships inside the player bundle as a complete catalog. Nothing is fetched at runtime. Catalogs are small, so bundling all supported languages is deliberate.
  • English is the reference catalog and the only catalog at launch. Each capability and core control declares the string keys it owns, with their English values, in its own module, and the framework aggregates them. Additional languages arrive as per-language catalog files over the same key set through ordinary player releases, so translation quality is controlled with the player itself.
  • A catalog that misses a key falls back to English for that key, so partial translations degrade gracefully.

Instance overrides

The strings section overrides individual keys on top of the selected language: custom wording for one instance (a specific cover button label) or a spot correction. It is an escape hatch, not a translation mechanism.

json
{
  "language": "de",
  "strings": {
    "coverButton": "Jetzt ansehen"
  }
}

Resolution order per key: instance override, then the selected language catalog, then English.

Unknown keys never fail an instance. Validation logs one console warning per instance listing them, so integrator typos surface during development; users are unaffected. The player ships as a single production bundle, so there is no separate development build to scope warnings to.

What is and is not a string

  • Strings are interface text owned by the player: labels, tooltips, accessibility names.
  • Content is not a string: titles, author names, chapter names, and track labels arrive in content and render as data, already in the page's language when the page's content system translates them.
  • Message templates that carry content (for example a share message built from the video title) live with their capability's options, not in strings. Templates and strings may both use {placeholder} interpolation where their documentation says so.

Ownership and the media engine

  • The LT Player owns every visible string. Each control and capability documents its string keys in its own specification document; a key that no document lists does not exist.
  • The bundled media engine's internal internationalization is not part of the public contract. Where a bundled surface renders text or accessibility labels, the player feeds it from the resolved values through an internal adapter, in one direction only. Consumers never interact with the media engine's language APIs.

Time display

The timeline clock (12:34) is locale-neutral and renders identically in every language. This is deliberate; revisit only if a real need appears.

Merge semantics

How one instance turns a configuration into behavior. Resolution runs at mount and at every update, by exactly the same rules.

The three layers

Every resolved value comes from one of three layers. Per key, the highest layer that has something to say wins:

LayerOwnerContentLifetime
3 · User stateThe userVolume and mute, playback rate, captions on/off and languageSurvives every update and every visit (see User state)
2 · Instance configurationThe pageThe full configuration JSONUntil the next update replaces it
1 · Built-in defaultsThe playerEvery control visible, every available capability enabled, English, shipped option setsConstant

Precedence has one nuance, and it is absolute: explicit removal beats user preference, and user preference beats defaults. A configuration that disables a function (controls.fullscreen: false, capabilities.captions: false) removes it entirely; no user choice resurrects it. Where a function exists, the user's choice wins over the configuration's initial values. The user layer overrides configured values at exactly two points: a track's default: true (the user's captions choice wins) and the starting playback rate (the user's chosen rate wins, always within the configured rate list).

Updates replace, they never patch

An update always carries a complete configuration, exactly what a mount would receive. There is no patch format:

  • An omitted key never means "keep the previous value". It means the same thing it means at mount: the built-in default applies.
  • Layers 1 and 2 are rebuilt from scratch on every update; nothing of the previous configuration leaks into the next resolution. Only two things survive an update by design: the media element (never recreated, preserving playback permissions such as the audio activation grant) and the user state.
  • Arrays (sources, tracks, chapters, rate lists, any array anywhere) are replaced wholesale by the new configuration. Array elements are never merged.
  • The player diffs internally to apply the change efficiently; that diffing is an implementation detail and never changes the semantics above.

layout is the one immutable key: an instance is landscape or portrait for its whole life, in both directions, feed scrolling included. An update naming a different layout is hard-invalid (see Validation).

User state

The user owns exactly three properties, and they outrank the configuration within the bounds the configuration allows:

PropertyWhat is remembered
Volume and muteThe last volume level and mute state the user set
Playback rateThe last rate the user chose
CaptionsOn or off, and the chosen track language

The rules, each of them binding:

  • Remembered as intent, applied as best realizable. The memory stores what the user wanted, not what a particular video could deliver. A user at 1.5x meeting a video whose rate list lacks 1.5x plays at the closest offered rate, and returns to 1.5x on the next video that offers it. Captions stay on with the best available track when the chosen language is missing, and are gracefully absent on a video without tracks; the intent is untouched throughout. Volume is always realizable and never degrades.
  • Survives every configuration update. This is the framework principle "user choices outlive configuration updates": the shorts feed swaps configurations freely and the user's choices ride across all of them.
  • Persisted across visits. The user state is backed by localStorage, so preferences also survive page loads and later visits on the same site. The storage layer below defines the mechanism completely.
  • One autoplay exception. The shorts feed always starts muted regardless of the stored mute state, because muted autostart is browser autoplay policy, not a preference. The stored volume applies once the user unmutes (see the shorts document).

The storage layer

The persistence mechanism has its own contract: key, schema, versioning, behavior, and privacy are specified completely in the storage contract. In short: one lazily created key (lt-player:user:v1), one JSON object holding the user state, written through on every explicit user change, fail-safe in every environment.

Validation

Validation runs inside mount and inside every update, before anything is applied. A rejected call applies nothing: there are no partial updates.

Hard-invalid, exactly three conditions

  1. layout is absent or not one of landscape and portrait (at mount).
  2. content.sources is absent or empty.
  3. An update names a layout different from the instance's.

A configuration in the legacy pre-3 shape (recognizable by its top-level body wrapper) always trips condition 1 or 2 by construction and additionally gets the dedicated legacy-configuration error message in the console, so integrators migrating old embeds see instantly what happened.

A hard-invalid configuration produces two things, together:

  • For the developer: the mount or update call fails, and one console error names the exact violated condition. The page API document defines the failure surface of the calls.
  • For the user: the instance renders the config error state: a black surface with a short, calm, generic message (a string key in the language catalogs, so it is translatable and rewordable). No technical details, and no Retry control, because retrying cannot fix a configuration. On a rejected update the instance keeps its layout and the previous video does not continue: the error state replaces playback. At mount, when layout itself is the invalid part, the error shell renders in landscape.

Soft-degrade, everything else

Every other irregularity degrades without ceremony, because a public embedded player must never punish users for an integrator's typo:

  • Unknown keys anywhere in the configuration are ignored.
  • A value of the wrong type falls back to the built-in default for that key.
  • Malformed collection entries are dropped individually: a chapter without a name or start, a chapter whose start is negative or beyond the video's duration, a track without a src. The rest of the collection stays in force; a collection whose entries are all dropped leaves its capability dormant, exactly as if it had not been provided.
  • An unavailable language falls back per the documented language resolution; unknown strings keys follow the strings section's own rule.

Every soft finding is reported in one consolidated console warning per configuration application (one per mount or update), listing everything that was dropped or defaulted. Warnings are for developers; users see a working player.

What validation is not

Runtime media failure is not a configuration error. A source that validates but does not play (unreachable file, dying stream) is handled by the core player's error state with its Retry behavior, defined in the core player document.

Examples

Minimal

The smallest valid configuration: layout and sources. Everything else defaults: all controls visible, all capabilities enabled (and dormant where data is missing), English.

json
{
  "version": 3,
  "layout": "landscape",
  "content": {
    "sources": [{ "src": "https://cdn.example.org/videos/842/video.mp4", "type": "video/mp4" }]
  }
}

Complete

Everything on: every content key present, all five capabilities active with their options, and one string override. A German portrait instance with the full metadata set, so every context panel view renders.

json
{
  "version": 3,
  "layout": "portrait",
  "language": "de",
  "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://cdn.example.org/videos/842/cover.jpg",
    "duration": 609.4,
    "sources": [{ "src": "https://cdn.example.org/videos/842/video.mp4", "type": "video/mp4" }],
    "tracks": [
      {
        "kind": "subtitles",
        "label": "Deutsch",
        "srclang": "de",
        "src": "https://cdn.example.org/videos/842/de.vtt",
        "default": true
      },
      {
        "kind": "subtitles",
        "label": "English",
        "srclang": "en",
        "src": "https://cdn.example.org/videos/842/en.vtt"
      }
    ],
    "chapters": [
      { "name": "Question", "start": 9 },
      { "name": "Method", "start": 62 },
      { "name": "Findings", "start": 198 }
    ],
    "metadata": {
      "inANutshell": { "generatedByAi": true, "keyTakeaway": "…", "summary": "…" },
      "keyConcepts": {
        "generatedByAi": true,
        "items": [{ "label": "Induction", "description": "…", "start": 74 }]
      },
      "researchField": {
        "generatedByAi": true,
        "sections": [{ "title": "Demography", "description": "…" }]
      },
      "transcript": { "generatedByAi": true, "entries": [{ "start": 0, "text": "…" }] },
      "underlyingPublication": {
        "items": [
          {
            "title": "…",
            "authors": ["…"],
            "journal": "…",
            "abstract": "…",
            "url": "https://doi.org/…"
          }
        ]
      },
      "relatedVideos": {
        "generatedByAi": true,
        "related": [{ "title": "…", "poster": "https://…", "url": "https://…" }],
        "forYou": []
      }
    }
  },
  "capabilities": {
    "chapters": true,
    "captions": true,
    "speed": { "options": [1, 1.2, 1.5, 2] },
    "share": {
      "canonicalUrl": "https://lt.org/publication-plus/example/",
      "embedUrl": "https://lt.org/embed/publication-plus/example/"
    },
    "contextPanel": true
  },
  "strings": {
    "coverButton": "Jetzt ansehen"
  }
}

Reading it against the contract: controls is omitted, so every core control is visible; all five capabilities are explicitly enabled and all have their data, so nothing is dormant: chapters segment the timeline, two caption tracks feed the captions menu, share offers both link and embed, and every context panel view has its metadata section. The user's stored rate and captions choices apply on top, within the offered rates and tracks.