Page API
approvedHow a hosting page creates, controls, and observes player instances.
Mounting and unmounting
- Creating a player takes a host element and a configuration and returns an instance handle; destroying it releases everything.
- Updating an instance swaps the video in place: the media element is reused, which preserves playback permissions such as the audio activation grant. This operation is the primitive the publications feed is built on.
- Mount and update validate before applying anything. A hard-invalid configuration (the config contract's validation section defines exactly three conditions, with the legacy pre-3
bodyshape called out) makes the call fail with a console error naming the violation, and the instance renders the config error state; a rejected update applies no partial changes and does not continue the previous video. Soft irregularities never fail a call: they degrade to defaults with one consolidated console warning. - A hard-invalid initial call still mounts the configuration-error state before it reports and throws, and it returns no handle. When the supplied layout is valid, the error state uses that layout; landscape is the fallback only when the layout value is absent or invalid. The failed tree stays associated with its host internally so a later valid
createPlayercall can replace and dispose it. - An update rejects a configuration whose layout differs from the instance's: layout is immutable per instance, in both directions, and the error says so plainly.
- One live player owns one host. A second
createPlayercall on an already-owned host throws an error naming the ownership conflict and leaves the existing instance untouched. Destroying the owner releases the host for a later player.
Feed mounting
The feed entry point accepts an optional initial index:
import { createFeed } from '@latest-thinking/lt-player/feed';
const feed = createFeed(host, configurations, { startIndex: 5 });
const unsubscribe = feed.on('indexChange', ({ index }) => {
// Synchronize page state with the active publication.
});startIndex must be an integer within the configuration array. Negative, fractional, non-finite, non-number, and out-of-range values fall back to 0. Omitting the options argument remains source-compatible and still starts at 0.
The feed mounts and starts playback directly on the selected item; it does not mount or play item 0 before moving. If the host has no usable layout at creation, the feed waits for its first usable size, reapplies the selected scroll position, and only then attaches intersection observers. It emits one asynchronous indexChange for the initial index, so a subscription registered immediately after createFeed returns receives the initial state. Later committed moves continue to emit indexChange once per move.
This initial event is a consumer-visible addition for every feed mount, including two-argument calls. Existing indexChange subscribers receive one additional asynchronous callback after creation and should treat it as initial state synchronization rather than a navigation initiated after mount.
Instance handles
Creating a player is one call: createPlayer(host, config) takes a host element and a complete configuration and returns the instance handle. The handle is the entire surface a page ever holds; there is no other object and no engine access. It is a stable plain object whose method identities do not change.
handle.update(config); // apply a complete configuration; throws on hard-invalid
handle.destroy(); // release everything
handle.getState(); // one snapshot object, see below
handle.play();
handle.pause();
handle.seek(seconds);
handle.setVolume(volume); // 0 to 1
handle.setMuted(muted);
handle.setRate(rate); // within the offered rate list
handle.setCaptions({ enabled, language });
handle.openShare(); // the external share trigger
handle.openPanel(view); // open a specific context panel view
handle.closePanel();
handle.on(event, handler); // returns an unsubscribe functiondestroy() is idempotent. Once it has released the instance, mutating commands, update(), and new subscription attempts through that stale handle are silent no-ops; on() returns an inert unsubscribe function. getState() remains readable and returns the final detached snapshot. The unsubscribe returned by a live subscription is also idempotent.
State
getState() returns one snapshot, deliberately instead of many getters:
{
"playback": "playing",
"currentTime": 12.4,
"duration": 609.9,
"volume": 0.8,
"muted": false,
"rate": 1.5,
"captions": { "enabled": true, "language": "de" },
"pip": false,
"layout": "portrait"
}playback is one of idle, buffering, playing, paused, ended, error (the core player's six states; their derivation and transitions live in the core player document). The snapshot is a plain serializable object, computed at call time; it never updates itself. Pages that want continuous values subscribe to events.
Setter semantics
- Setters express user-equivalent actions: they update the user state exactly as the player's own controls would (
setRateremembers intent,setCaptionssets the preference), and the same bounds apply: a rate outside the offered list snaps to the closest offered value. - Setters on removed functions are silent no-ops: calling
setCaptionson an instance withcapabilities.captions: falsedoes nothing, matching the removal-beats-preference rule. openPanel(view)with a view whose metadata section is absent is a silent no-op (the view is dormant);openShare()without share data likewise.openShare()opens the share dialog in its page placement, centered in the viewport; the player's own trigger uses the in-player placement (the share capability's Placement section defines both).openPanel(view)on an instance whose layout owns no panel surface is a silent no-op, even when that view's metadata section is present.- The public context-panel view values are
knowledge,underlyingPublication, andrelatedVideos.
Events
handle.on(event, handler) subscribes; the returned function unsubscribes. Handlers receive one payload object, and every payload carries currentTime, read when the event is dispatched. Commands do not emit optimistically: an event follows the observed engine or LT-module state change, regardless of whether the trigger was the page API, player UI, lock screen, or browser. The list is deliberately small and grows in ordinary releases when a real need appears:
| Event | Additional payload fields | Fires |
|---|---|---|
ready | duration | Metadata and a first frame are available and the instance can play |
play | none | Playback starts or resumes |
pause | none | Playback pauses |
ended | none | Playback reaches the end (standalone; the feed loops instead) |
seeked | none | A seek completes |
buffering | buffering: boolean | Published buffering starts or recovers |
error | kind: "configuration" | "startup" | "mid-playback" | The corresponding error state is entered |
timeUpdate | none | Playback position updates at the media element's native cadence |
chapterChange | chapter: PlayerChapter | null | The current chapter changes, including the unlabeled intro |
volumeChange | volume, muted | Volume or mute changes |
rateChange | rate | Playback rate changes |
captionsChange | captions: { enabled: boolean; language?: string } | Captions toggle or the selected track changes |
pipChange | pip: boolean | Picture-in-picture is entered or left |
shareOpen | source: "player" | "page" | The share dialog opens; source matches the trigger |
panelOpen | view: "knowledge" | "underlyingPublication" | "relatedVideos" | The context panel opens or switches to that view |
panelClose | none | The context panel closes |
videoChange | none | A successful update is live |
Every error also reaching the console is a rule from the core player document; events are for pages, the console is for developers.
Multiple players on one page
- Multiple independent instances are supported. Cross-instance coordination exists only where a module defines it; the publications feed's single-instance model keeps the common case trivial.