Appearance
Config
approvedThe 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": {}
}| Section | Owns |
|---|---|
layout | The shape of the instance: landscape or portrait |
language | Interface language selection from the shipped catalogs |
content | Facts about the video: sources, poster, title, chapters, tracks |
controls | Visibility of core elements, boolean or object per control |
capabilities | Enablement and options of LT capabilities |
strings | Per-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
metadatasections mirror the context panel's views and tabs one to one. Every section accepts ageneratedByAiflag, which drives the AI disclaimer in the panel. - Key concept items may carry
startto make their chips seek. - Dropped from the previous generation, deliberately:
bio,abstract, andinstitution(no surface in the redesigned player), CMS asset identifiers (posterId,vttId), and the presentation flags (type,theme,showLogo,isEmbedded,background), which are superseded bylayout,controls, and theming. Share URLs live undercapabilities.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. falseremoves. 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:
| Capability | Switch | Options | Data that makes it available |
|---|---|---|---|
chapters | boolean only | none | content.chapters non-empty |
captions | boolean only | none | content.tracks with subtitle or caption kinds |
speed | boolean or object | options: non-empty ascending rate list; shipped default [1, 1.2, 1.5, 2] | none, always available |
share | boolean or object | canonicalUrl, embedUrl, playerTrigger, targets, templates | its own canonicalUrl |
contextPanel | boolean only | none | any 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,contextPanelare 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.speedacceptsoptions, 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.shareis the one capability whose data lives in its own options rather than incontent, because share targets describe pages, not the video file. WithoutcanonicalUrlthe capability is dormant and no share triggers render. OmittingembedUrlhides 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.playerTriggerdefaults totrue; setting it tofalsehides only the player-owned trigger while the capability andopenShare()page command remain available.targetsis an optional allowlist forlink,embed,facebook,x,linkedin,whatsapp, andmore; the interface always retains that designed order. An explicit empty list produces no usable share surface.templates.messageoverrides the text-carrying target template with{title},{author}, and{url}interpolation. The demo tooling injects both URLs from a video document'ssharesection 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
contentand 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:
| Layer | Owner | Content | Lifetime |
|---|---|---|---|
| 3 · User state | The user | Volume and mute, playback rate, captions on/off and language | Survives every update and every visit (see User state) |
| 2 · Instance configuration | The page | The full configuration JSON | Until the next update replaces it |
| 1 · Built-in defaults | The player | Every control visible, every available capability enabled, English, shipped option sets | Constant |
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:
| Property | What is remembered |
|---|---|
| Volume and mute | The last volume level and mute state the user set |
| Playback rate | The last rate the user chose |
| Captions | On 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
layoutis absent or not one oflandscapeandportrait(at mount).content.sourcesis absent or empty.- 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
layoutitself 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
nameorstart, a chapter whosestartis negative or beyond the video's duration, a track without asrc. 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
languagefalls back per the documented language resolution; unknownstringskeys 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.