Skip to content

Core player

approved

The core is the baseline every player instance ships, regardless of configuration. Core elements are not capabilities: they cannot be absent from the build, only tuned per instance.

Scope of the core

Playback machinery, the cover, basic controls, gestures, keyboard shortcuts, and the state surfaces (buffering, errors). Everything else is a capability.

Rules

  1. Visible by default. Every core control defaults to on; configuration can switch each off per instance under the controls namespace.
  2. Off removes the function everywhere. Disabling a control removes its UI and all its input paths: buttons, hotkeys, gestures. A player configured without fullscreen cannot enter fullscreen by any means.
  3. Boolean or object. Every control key accepts false/true or an object: visibility sub-switches (the cover is the compound example) or documented behavior options (the timeline's seekStep is the first). Sub-keys exist only where a real need is named in this document; adding one later is backward compatible.
  4. Reveal, never invent. Controls only show or hide what content provides. Absent content renders nothing, independent of control switches.
  5. Chromeless is legal. Turning every control off yields a bare video surface driven by the page API and is a supported end state. There is no shorthand for it; each element is switched explicitly.

Control inventory

Config keyElementfalse removes
coverPre-playback surface (compound, see below)The whole cover; playback starts on the first frame
playPausePlay and pause controlButton, space and k hotkeys, tap-to-toggle
timelineSeek sliderSlider, keyboard seeking, scrubbing, double-seek
timeTime display (current and duration)The display only
volumeVolume and mute controlControl, m hotkey, volume keys
fullscreenFullscreen controlButton, f hotkey, double-tap
pipPicture-in-picture controlButton, i hotkey, and auto-PiP eligibility
bufferingBuffering indicatorFluid 3rem to 5rem spinner
inputFeedbackVisual confirmation of actionsYellow play/pause feedback plus secondary indicators
logoLT branding elementThe logo
gesturesMaster switch for the gesture layerAll gestures, including those of enabled functions
hotkeysMaster switch for the keyboard layerAll shortcuts, including those of enabled functions

Two inventory notes:

  • Play/pause input feedback uses the native status-indicator lifecycle with yellow play and pause icons in place of its text labels.
  • Double-click and double-tap seeking use the left and right thirds in both layouts. The center is neutral in portrait and on landscape touch, and remains reserved for landscape mouse fullscreen.
  • volume adapts to the device, not the layout: a full volume control on desktop; on mobile devices it collapses to a mute toggle and volume belongs to the hardware buttons.
  • fullscreen resolves from the same enabled default in both layouts. Portrait omits its fullscreen surface and silently ignores configuration that enables it (see the layouts document).
  • pip follows platform availability: where the browser offers no picture-in-picture, the control is absent regardless of configuration (see Picture-in-picture).

The cover

The cover is the pre-playback surface and the only compound control today.

Sub-keyElementNotes
imageCover imageURL from content.poster; absent, the first video frame stands in
titleTitle overlayControls the cover title; portrait playback identity uses content directly
authorAuthor overlaySame
buttonPlay affordancePlain icon by default; { "label": "..." } renders a labeled button

Clicking anywhere on the cover starts playback; the button is presentation, not the only hit target. Identity on the Latest Thinking website typically runs title: false, author: true because the page owns the title.

Strings

The core owns these catalog entries. Content values such as the video title and author do not enter the string catalog.

KeyEnglish defaultPlaceholdersAppears in
playerLabelMedia playernonePlayer-root aria label
logoLabelLatest ThinkingnoneBranding accessible label
coverButtonPlaynoneCover affordance label
playPlaynonePlay control aria label and tooltip
pausePausenonePause control aria label and tooltip
replayReplaynoneReplay control aria label and tooltip
muteMutenoneMute control aria label and tooltip
unmuteUnmutenoneUnmute control aria label and tooltip
seekSeeknoneTimeline aria label
seekForwardSeek forward {seconds}ssecondsSeek-forward label
seekBackwardSeek backward {seconds}ssecondsSeek-backward label
enterFullscreenEnter fullscreennoneFullscreen control label
exitFullscreenExit fullscreennoneFullscreen control label
enterPictureInPictureEnter picture-in-picturenonePicture-in-picture control label
exitPictureInPictureExit picture-in-picturenonePicture-in-picture control label
currentTimeCurrent timenoneCurrent-time aria label
durationDurationnoneDuration aria label
remainingTimeRemainingnoneRemaining-time aria label
timePosition{current} of {duration}current, durationTimeline value text
volumeVolumenoneVolume-slider aria label
volumeMutedValue{percent}, mutedpercentMuted slider value text
volumeFeedbackVolume {value}valueVolume action announcement
mutedMutednoneMute action announcement
pausedPausednonePause action announcement
playingPlayingnonePlay action announcement
fullscreenEnteredFullscreennoneFullscreen announcement
pictureInPictureEnteredPicture in picturenonePicture-in-picture entry announcement
pictureInPictureExitedExited picture in picturenonePicture-in-picture exit announcement
seekedToSeeked to {time}timeSeek announcement
bufferingBufferingnoneBuffering status
mediaErrorMessageThis video cannot be played right now.noneMedia-error surface
retryRetrynoneMedia-error action
configErrorMessageThis player cannot be loaded.noneConfiguration-error surface

States

A player instance is always in exactly one of six states: idle, buffering, playing, paused, ended, error. The set is deliberately minimal: every state earns its place in all three lenses at once: it looks different (design), it is derived differently (implementation), and a hosting page can act on it (public contract). What has no distinct presentation and no distinct meaning for a page is not a state.

getState().playback reports the current state, and the page API events announce the changes. Engine flags never appear in the public contract.

The state set

StateOn screen (design)A page reads it as (contract)
idleThe cover element: cover image, identity, play affordanceNothing consumed yet
bufferingThe buffering indicator, after 300 millisecondsPlayback is stalled or starting
playingThe video, under the control visibility rulesThe user is watching
pausedThe video paused, controls pinned visibleThe user paused
endedThe frozen last frame, play control becomes replayThe video completed
errorThe calm dark error surfacePlayback or configuration failed

Mapping from the engine

The media engine has no single state value: its store models playback as independent flags (paused, ended, started, waiting, canPlay, error). The LT states are named, derived views over those flags, and implementers map them exactly like this:

LT stateEngine condition
idle!started: no playback has begun on this video
bufferingwaiting, including the span between the play gesture and the first frame; surfaced only after the 300 millisecond delay
playingstarted && !paused && !waiting && !ended
pausedstarted && paused && !ended
endedended (standalone only; the feed loops instead)
errorerror != null for media failures; the config error state is LT's own and has no engine flag

There is deliberately no loading state. Initialization happens inside idle and is invisible by design (see Idle); the moment a page can act on, "the instance can play", is the ready event, not a state.

Transitions

The complete transition graph; nothing else is legal:

  • idle to buffering: the user starts playback (cover click, play control, page API).
  • buffering to playing: frames flow.
  • playing to paused and back: pause and resume, from any input.
  • playing to buffering and back: a mid-playback stall and its recovery.
  • playing to ended: the video completes (standalone; the feed loops and never enters ended).
  • ended to buffering: the replay affordance restarts from zero.
  • Any state to error: a media failure, or a hard-invalid configuration.
  • error to buffering: Retry, on media failures only; config errors have no Retry.
  • update() with a new video returns a standalone instance to idle (new cover); the shorts feed goes straight to buffering, since it autoplays (and skips the intro, see the chapters and shorts documents).

Idle

The pre-playback state; what it shows is the cover element (see The cover). Without content.poster, the first video frame stands in as the cover image; identity and the play affordance render regardless.

Initialization is invisible inside idle, by design: from the first paint the player surface is a themed dark background, so nothing shifts when assets arrive. Identity and the cover button render immediately (they need no network), and the cover image streams in progressively over the dark background. There is no spinner and no layout jump, and this is why no loading state exists.

Buffering

One indicator serves both startup (centered over the still-visible cover, between the play gesture and the first frame) and mid-playback stalls (centered over the video). It appears only after 300 milliseconds of actual buffering, so brief stalls never flicker the indicator. During feed transitions the incoming video's poster covers this span (see the shorts document).

Playing and paused

Normal playback under the control visibility rules (see Control visibility). Paused pins the controls visible.

Ended

Standalone: the last frame stays on screen and the play control becomes a replay affordance; one activation restarts from the beginning. The shorts feed overrides this state entirely: feed videos loop (see the shorts document).

Error

  • Unplayable at start (unreachable source, unsupported format): the surface shows a calm dark state with a short message and a Retry control. No technical details face the user.
  • Mid-playback failure (network loss, stalled source): a stall becomes a failure after 10 continuous seconds of waiting without readiness or playback-position progress. Any readiness or position progress resets that timer; leaving waiting cancels it. An engine media error starts recovery immediately without waiting for stall promotion. The player presents only the buffering indicator while recovery runs. Automatic resume attempt one starts after 250 milliseconds; if it fails, attempt two starts after a further 500 milliseconds, and there is no third automatic attempt. An attempt that does not restore playback within 10 seconds counts as failed. Only when both fail does the message-and-Retry state appear. Retry resumes from the position where playback stalled, never from zero, and re-arms a fresh two-attempt automatic cycle.
  • The message and the Retry label are string keys in the language catalogs.
  • Every error fires a page API event so hosting pages can log or react.
  • Configuration errors are a distinct case within the error state: a hard-invalid configuration at mount or update (defined in the config contract's validation section) renders the config error state: a black surface with a short, calm, generic message and no Retry control, since retrying cannot fix a configuration. The exact violation goes to the console for the developer. A rejected update keeps the instance's layout and replaces playback with this state; a mount whose layout is itself invalid renders the shell in landscape. The message is a string key in the language catalogs.

Control visibility

  • Landscape hide delay: while playing, after 3 seconds without interaction, the control bar, the top actions, and (in fullscreen) the cursor fade out together. The configured logo remains visible.
  • Revealing: on desktop, any mouse movement over the player; on touch, a tap on the video surface. The revealing tap only reveals, it never toggles playback; with controls visible, taps and clicks act normally.
  • Paused: controls stay pinned visible.
  • Hover and open menus: while the pointer hovers the control bar, or any player menu or dialog is open, the hide timer is suspended.
  • Keyboard focus: while any control has keyboard focus, controls never hide. This is an accessibility requirement, not a preference.
  • Portrait: never auto-hides anything: its persistent overlay (captions, identity, trigger rail, bottom bar) is the design.

Autoplay

Standalone instances have no autoplay: playback starts from a user gesture on the cover or the controls, and no configuration key exists to change that. The shorts feed's muted autostart is defined in the shorts document and is not per-instance configurable either.

Background playback and lock screen

The player integrates with the operating system's media controls through the Media Session API, so playing content appears as "now playing" on the iOS lock screen and control center, in the Android notification shade, and on desktop through hardware media keys and the system's now-playing surfaces.

Media Session is document-global, so player instances coordinate one owner:

  • The first live player provisionally owns the session. The most recent player to transition to playing takes ownership, and pausing retains ownership so the operating system's play action resumes the same video.

  • Only the owner publishes metadata, actions, playback state, and position state. An owner update refreshes that data immediately.

  • Destroying the owner transfers ownership to the most recently active remaining player, or to the first remaining provisional player. Cleanup from the old owner never clears data published by its successor.

  • Metadata mapping: content.title becomes the title, content.author the artist line, and content.poster the artwork.

  • Actions: play, pause, seek backward, seek forward, and absolute seeking where the platform offers a scrubber. The seek step is 10 seconds by default, configurable:

json
{
  "controls": {
    "timeline": { "seekStep": 10 }
  }
}

The same step drives every seek input: the widget buttons, keyboard arrows, and seek gestures.

Control switches gate only Media Session action handlers:

  • controls.playPause gates the play and pause handlers.
  • controls.timeline gates the seek-backward, seek-forward, and absolute-seek handlers.
  • Control switches never suppress metadata, playback-state, or position-state publication. A playing chromeless instance still publishes its title, artist, artwork, playback state, and position; it simply installs no LT action handlers.
  • Page API commands remain available for chromeless integrations.

On a final media error or a configuration error, the owner removes every LT action handler, clears position state, and sets playbackState to none. Ownership transfers to the most recently active eligible player when one exists. Otherwise the failed video's metadata remains published without presenting it as playing or seekable.

Media Retry makes that participant eligible again. If no other player owns the session, it immediately republishes metadata and applicable handlers; position remains cleared until the engine reports a valid duration and position. If another player owns the session, Retry does not steal ownership: resumed playback does. A valid configuration update recovering from a configuration error follows the same eligibility rule.

  • Metadata follows the instance: a configuration update (the shorts feed) refreshes the lock screen metadata to the new video.
  • Platform honesty: background continuation is platform-dependent and not fully ours to promise. Android generally keeps audio playing with the media notification. iOS pauses web playback when the screen locks; the widget then shows with our metadata, and one tap on its play control resumes audio while the screen stays locked. The player guarantees correct metadata and working controls on the widget; the exact continuation behavior belongs to the platform.
  • The shorts feed additionally maps the widget's previous and next track controls to feed navigation (see the shorts document); standalone instances do not register those actions, so the platform shows no such buttons for them. A standalone owner explicitly clears stale previous- and next-track handlers, but never installs non-null handlers for them.
  • Unsupported platform actions are silently omitted. Browser-owned fallback UI remains platform-dependent.

Picture-in-picture

Delivery priority: last. Picture-in-picture is specified here and binding, but it is implemented only after everything else on the player is finished.

The player supports element picture-in-picture: the video frames lift into a floating window owned by the operating system, and playback continues there while the user scrolls, switches tabs, or leaves the browser. The behavior follows the reference experience of Chromium news sites: minimal, browser-native, no custom window.

What the player owns, and all it owns:

  • The pip control in the bar, the I hotkey, and the engine wiring.
  • Auto-PiP eligibility: the player registers the Media Session enter-picture-in-picture handler and leaves picture-in-picture enabled on the media element unless controls.pip: false removes the function. Firefox is excluded so its browser-owned PiP overlay does not cover the LT controls. Eligibility is what lets Chromium float the video automatically.

Everything about the window itself belongs to the platform, and differs by design:

BehaviorChromium (Chrome, Brave, Edge)Safari macOSiOS
Automatic entryOn scroll away and tab switch, while playingNone, manual onlyOn app switch, per system setting
Window lookMinimal: bare video frames, controls on hoverNative framed window with system controlsNative iOS PiP
PositioningFree, any position, any screenSnaps to the four screen cornersCorners, collapsible to the screen edge
ReturnScrolling back or returning to the tab restores inline playbackManualManual or app return

The window's appearance is never ours, on any platform. Every browser renders its own picture-in-picture design, and the player neither styles nor fights it.

One limitation, inherent to the API: LT-rendered surfaces do not travel into the PiP window. Captions (the styled chip), chapters, and all controls exist in the page; the window carries video frames only, with the platform's own transport controls.

Gestures and keyboard shortcuts

Default bindings ship with the player. They are implemented through the media engine's hotkey and gesture primitives and their action vocabulary (composition, not reinvented input handling), and the bindings deliberately follow the engine's reference set plus established player conventions. The two master switches (controls.hotkeys, controls.gestures) disable whole input layers; per-function inputs already die with their function (rule 2): a removed fullscreen ignores F. All shortcut-related accessibility texts come from the strings namespace.

Every seek input below uses the one shared seek step, controls.timeline.seekStep, 10 seconds by default (see the lock screen section).

Keyboard (while the player has focus)

KeysAction
Space, KPlay, pause
ArrowLeft, ArrowRightSeek backward, forward by the seek step
J, LSeek backward, forward by the seek step
ArrowUp, ArrowDownVolume up, down in small increments
MMute toggle
FFullscreen toggle, where the layout has the slot
CCaptions toggle
IPicture-in-picture toggle, where the platform supports it
<, >Previous, next offered playback rate, wrapping at the list boundaries
0 to 9Jump to 0 through 90 percent of the video
Home, EndJump to start, end
EscapeClose the open menu, dialog, or panel; exit fullscreen

Gestures and pointer

InputLayoutAction
Touch tap on the surfaceLandscapeReveal controls only, never toggles playback (see control visibility)
Touch tap on the surfacePortraitPlay and pause toggle: portrait never hides controls, so no reveal exists
Double-tap left or right zoneLandscape onlySeek backward or forward by the seek step, with the seek indicator
Mouse click on the surfaceBothPlay and pause toggle
Mouse double-click on the surfaceLandscapeFullscreen toggle
Vertical swipePortrait feedOwned by the shorts feed (advance), never a player gesture

No pinch and no long-press gestures exist. Double-tap seeking is deliberately absent in portrait: it would race the feed's fast swiping.