Media Player

Video and audio playback with composable controls, preset layouts, keyboard shortcuts and gestures over a native media element.

<tp-media-player>
Media Player
Default density Compact density Restart from the initial state Show the copyable source

Usage

Put a native <video> or <audio> and one layout inside the player. The player owns a single media store; every control reads state from it and changes playback through its request pipeline, so buttons, keys and gestures always agree. The media store, request types, events and the target contract are also exported from @tweakpad/ui/media.

Media discovery. A registered custom media element wins (the last registration). Otherwise the first <video> or <audio> in the player’s light DOM is attached, and subtree changes re-run discovery. A custom media element implements the media target subset (any of playback, time, buffer, volume, rate, error, tracks, renditions, presentation, remote, live, content data) and registers on connection with requestMediaRegistration(element) from @tweakpad/ui/media. It calls the returned release on disconnection. Missing capabilities keep defaults and report unsupported.

Lifecycle. Disconnection detaches synchronously. Destruction is deferred two frames and cancelled if the player reconnects (a DOM move), which keeps state and subscriptions.

Audio

An <audio> element works the same way. tp-media-audio-layout is the standard audio player: an always-visible bar in the page’s color scheme with play, the timeline, speed and volume, plus skip buttons when there is room. It takes the same hide and seek-buttons options as the video layout.

Audio layout
Default density Compact density Restart from the initial state Show the copyable source

For your own selection of controls, use an in-flow tp-media-controls with visibility="always" so the bar stays visible in the page’s color scheme, and pick the controls a podcast or voice note needs.

Audio
Default density Compact density Restart from the initial state Show the copyable source

Choosing controls

hide removes built-in controls from a layout by name, and seek-buttons adds skip back and forward next to play. Seek distance comes from the player’s seek-step.

Trimmed layout
Default density Compact density Restart from the initial state Show the copyable source

Hidden controls are gone, not disabled. Controls the media cannot support, such as picture-in-picture in some browsers, hide themselves.

Layout regions and speeds

The video layout’s top, center, bottom-start and bottom-end slots replace that region’s default content, here the title in top. playback-rates sets the options of the speed menu as a space-separated list. Tooltips, menus and popovers portal into the container, so they stay visible in fullscreen. Layouts use container queries on the layout box, which fills the container. They never use viewport queries.

Custom top region
Default density Compact density Restart from the initial state Show the copyable source
Lesson 4 of 12 Composition

Captions and chapters

<track> children of the media drive the text-track controls. Caption and subtitle tracks add the captions button and a Captions submenu in settings; a chapters track splits the timeline and names the chapter under the pointer.

Text tracks
Default density Compact density Restart from the initial state Show the copyable source

The tracks here are inline data: URLs to keep the example self-contained; usually they are .vtt files served alongside the video.

Custom composition

Skip the layout and place the constituents yourself when you need a different bar. Each control finds its player through the tree, and tp-media-hotkey adds or overrides a key binding. A control outside the player can name it with player="<player id>".

Custom bar
Default density Compact density Restart from the initial state Show the copyable source

Live streams

A stream is live when the media reports streamType: 'live' or an infinite duration, and it has a DVR window when it reports targetLiveWindow. The video layout then switches to its live variant: a live button that seeks to the edge and a timeline that spans only the window. Streaming engines report these members through their media element; this example adds them to a looping clip to simulate a 20-second window. Without DVR the time slider, rate controls and seek keys are disabled or hidden, and play jumps to the live edge.

Live with DVR (simulated)
Default density Compact density Restart from the initial state Show the copyable source

Quality and audio tracks

Media without native rendition or audio-track lists, such as an HLS or DASH engine, supplies them through mediaAdapter. Each list is array-like and an event target; renditions add selectedIndex, where -1 means adaptive. The settings menu then gains Quality and Audio submenus. This mock records the selection without changing the video. Getters are re-read on loadstart. selectVideoRendition('auto') restores adaptive selection.

Tracks adapter (mock)
Default density Compact density Restart from the initial state Show the copyable source

Localization

messages replaces any control text, as a string with {placeholders} or a function of the parameters, and locale formats times and rates. The locale only formats: clock text is digital with the duration as a guide (1:05, 01:05, 1:01:05), and spoken text uses Intl units (“1 minute, 5 seconds”). Rates use 1.5×. Diagnostics are not localized.

French controls
Default density Compact density Restart from the initial state Show the copyable source

Keyboard

With focus anywhere in the player, Space or K plays and pauses, the arrow keys seek and change volume, M mutes, F toggles fullscreen and C toggles captions. Shortcuts are published through aria-keyshortcuts and appear in the layout’s tooltips. Set hotkey-scope="document" to route keys from anywhere on the page to the most recently used player. Keys are ignored while typing in inputs and editable content, and while a menu or dialog consumes them.

KeysActionSpace, KPlay / pauseMMute / unmuteArrowRight, LSeek forward by seek-step (repeats while held)ArrowLeft, JSeek backward by seek-stepArrowUp / ArrowDownVolume ± volume-step0–9Seek to 0 %–90 %Home / EndSeek to the start / end> / <Playback rate up / down (clamped to playbackRates)FFullscreenCCaptionsIPicture-in-picture

Live streams without DVR disable the seek keys; all live streams disable the rate keys. Sliders, menus and buttons keep their own keys while focused.

Gestures

gestures="default" (enabled by the video layout) registers:

GestureActionTap, mousePlay / pauseTap, touchShow / hide controlsDouble tap, left regionSeek backward by seek-stepDouble tap, right regionSeek forward by seek-stepDouble tap, center regionFullscreen

A double tap cancels the pending single tap. Taps on controls never reach the gesture surface. Every gesture has a control and a key equivalent.

Accessibility

  • The container is a labelled group with a tab stop. Every control is a native button or a native range with a state-dependent name, published aria-keyshortcuts and focusable aria-disabled when unavailable.
  • Slider value text is localized: “1 minute, 5 seconds of 4 minutes” for time, the percent (or “50%, muted”) for volume. Slider value changes are never announced through a live region.
  • One polite live region per player announces paused/playing, captions, fullscreen, picture-in-picture and rate changes at once. Volume, mute and completed seeks are debounced (200 ms) and suppressed while a slider has focus. Element-originated changes and buffering are not announced. announcements="off" disables it.
  • Indicators, the slider preview, thumbnails and chapter segments are aria-hidden. Layout tooltips are visual only (describes="none") and add no description that would repeat the button’s name.
  • Autohide never hides a focused control, and focus is never moved by hiding.
  • Captions render natively, so operating-system caption preferences apply. In the video layout they are reachable from the captions button (from lg) and from the settings menu; the live variant uses a captions menu.
  • Controls meet --tp-target-size-min on coarse pointers.
  • The error dialog is an alertdialog, modal within the container (container siblings inert, focus trapped, aria-modal="false" because the rest of the page stays available), that returns focus when dismissed.

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptioncontent-titlecontentTitlestring | nullnullTitle; falls back to media content data. An authored '' stops the fallback.posterposterstring | nullnullPoster URL; falls back to media content data, then video.poster.stream-typestreamTypeMediaStreamTypeConfig'auto'Whether the media is on demand or live; auto reads it from the media's duration.playback-ratesplaybackRatesreadonly number[]DEFAULT_PLAYBACK_RATESRate options; must be sorted and positive (invalid entries are diagnosed and ignored).seek-stepseekStepnumber10Seconds for seek hotkeys, gestures and seek buttons without a value.volume-stepvolumeStepnumber0.05Volume step (0–1) for hotkeys and indicators.idle-delayidleDelaynumber2000Controls autohide delay in milliseconds; ≤ 0 disables autohide.hide-over-controlshideOverControlsbooleanfalseAllow hiding while the pointer is over the controls.hotkeyshotkeysMediaHotkeysMode'default'default registers the default key map; none keeps only tp-media-hotkey bindings.hotkey-scopehotkeyScopeMediaHotkeyScope'player'player listens on the container; document routes to the most recently active player.gesturesgesturesMediaGesturesMode'none'default registers the default video tap-gesture set.orientation-lockorientationLockMediaOrientationLock'none'Lock the screen orientation while fullscreen.announcementsannouncementsMediaAnnouncementsMode'polite'off disables the status live region.localelocalestring''Formatting locale (never selects translations). Empty: lang, then the document.

Properties

PropertyTypeDefaultDescriptionmessagesMediaPlayerMessages{}Localized strings with English fallbacks, inherited by constituents.mediaAdapterMediaTracksAdapter | nullnullEngine-provided renditions and audio tracks.stateread-onlyMediaState—The latest frozen state snapshot.mediaread-onlyMediaTarget | null—The attached media target, or null.containerread-onlyHTMLElement | null—The container: a tp-media-container descendant, else this root (null before connection).attachedread-onlyboolean—Whether media is attached.resolvedLocaleread-onlystring | undefined—The formatting locale in effect.mediaMessagesread-onlyMediaMessagesResolver—Root messages resolver (English fallbacks under messages).

Events

EventTypeDescriptiontp-media-requestTpMediaRequestEventCancelable proposal for every change (detail.action, value, reason, sourceEvent, trigger).tp-media-request-failedTpMediaRequestFailedEventA request was refused or failed (detail.error).tp-media-state-changeTpMediaStateChangeEventPublished state batch (changed, state, previousState, reason).tp-media-errorTpMediaErrorEventThe media error became non-null.tp-media-attachTpMediaAttachEventThe store attached to {media, container}.tp-media-detachTpMediaDetachEventThe store detached from {media, container}.tp-shortcut-changeTpShortcutChangeEventThe keyboard shortcuts changed, for example after a hotkey binding was added; controls refresh the shortcuts they show.

Methods

MethodReturnsDescriptionrequest(action, args)Promise<MediaRequestOutcome<A>>Proposes a media change through the request pipeline: a cancelable tp-media-request, gating (disabled player, capability), execution and tp-media-request-failed. Before attach it rejects with InvalidStateError.requestAvailability(action)MediaAvailabilityAvailability gating action right now.play()Promise<MediaRequestOutcome<'play'>>Starts playback; resolves with the outcome of the request.pause()Promise<MediaRequestOutcome<'pause'>>Pauses playback.togglePaused()Promise<MediaRequestOutcome<'toggle-paused'>>Plays when paused and pauses when playing.seek(time)Promise<MediaRequestOutcome<'seek'>>Seeks to time seconds; resolves the actual position after seeked.seekBy(delta)Promise<MediaRequestOutcome<'seek-by'>>Seeks delta seconds forward, or back when negative.seekToLiveEdge()Promise<MediaRequestOutcome<'seek-to-live-edge'>>Jumps to the live edge of a live stream.setVolume(volume)Promise<MediaRequestOutcome<'set-volume'>>Sets the volume, from 0 to 1.setMuted(muted)Promise<MediaRequestOutcome<'set-muted'>>Mutes or unmutes.setPlaybackRate(rate)Promise<MediaRequestOutcome<'set-playback-rate'>>Sets the playback speed, such as 1.5.requestFullscreen(options?)Promise<void>Requests fullscreen through the presentation service (container, WebKit presentation, then media). While that service is entering fullscreen on this root, the call is the native one.exitFullscreen()Promise<MediaRequestOutcome<'exit-fullscreen'>>Leaves fullscreen.requestPictureInPicture()Promise<MediaRequestOutcome<'request-picture-in-picture'>>Moves the video into a floating picture-in-picture window.exitPictureInPicture()Promise<MediaRequestOutcome<'exit-picture-in-picture'>>Returns the video from picture-in-picture.promptRemotePlayback()Promise<MediaRequestOutcome<'prompt-remote-playback'>>Opens the browser's prompt for playing on a remote device.selectTextTrack(id)Promise<MediaRequestOutcome<'select-text-track'>>Shows the text track with id (captions/subtitles), or none with null.toggleCaptions(force?)Promise<MediaRequestOutcome<'toggle-captions'>>Shows or hides captions; force sets the state instead of toggling.selectAudioTrack(id)Promise<MediaRequestOutcome<'select-audio-track'>>Switches to the audio track with id, such as another language.selectVideoRendition(id)Promise<MediaRequestOutcome<'select-video-rendition'>>Selects a rendition by id, or 'auto' to restore adaptive selection.toggleControls(force?)Promise<MediaRequestOutcome<'toggle-controls'>>Shows or hides the controls; force sets the state instead of toggling.dismissError()Promise<MediaRequestOutcome<'dismiss-error'>>Clears the store error (the media is not reloaded).requestControlsLock(reason?)() => voidReference-counted controls lock; releasing the last one restarts the idle delay.subscribe(selector, callback, options)() => voidObserves a state slice: callback runs when selector(state) changes under equality (default shallowEqual). Returns the unsubscribe; signal aborts it too.shortcut(action, value?)KeyShortcut | undefinedThe keyboard shortcut bound to action (and value), for showing it in a tooltip or menu.lockInteractions()() => voidSuspends hotkeys and gestures until the returned release runs (idempotent).registerControlsRegion(element)() => voidRegisters a controls region measured for --tp-media-caption-offset (light or shadow DOM).registerHotkey(source)() => voidRegisters a declarative key binding (tp-media-hotkey); read again whenever bindings sync.registerGesture(source)() => voidRegisters a declarative gesture binding (tp-media-gesture).refreshBindings()voidRe-reads registered bindings (after a tp-media-hotkey/tp-media-gesture change).

Slots

SlotDescriptiondefaultThe media element and all constituents.

<tp-media-video-layout>

Properties

PropertyTypeDefaultDescriptionvariantread-onlyMediaVideoLayoutVariant—The current variant (on-demand, live, live-dvr).

Slots

SlotDescriptiontopReplaces the title.centerReplaces the buffering indicator.bottom-startReplaces play, seek and volume (live: play and live).bottom-endReplaces captions and settings (live: volume and captions).

CSS parts

PartDescription::part(poster)The composed Poster.::part(top)The top region.::part(center)The center region.::part(controls)The composed Controls region.::part(bar)The bottom bar (a Controls group).::part(bottom-start)The bar's start region.::part(time)The time group.::part(bottom-end)The bar's end region.::part(secondary)The secondary group.

<tp-media-controls>

Attributes

AttributePropertyTypeDefaultDescriptionvisibilityvisibilityMediaControlsVisibility'auto'auto follows controls visibility; always stays visible (audio layout).

Properties

PropertyTypeDefaultDescriptionvisibleread-onlyboolean—Whether the region is currently shown.

Slots

SlotDescriptiondefaultControls and tp-media-controls-group elements.

CSS parts

PartDescription::part(backdrop)Decorative scrim behind the controls (aria-hidden).

<tp-media-play-button>

Slots

SlotDescriptionplayIcon while paused.pauseIcon while playing.replayIcon after the end.

CSS parts

PartDescription::part(button)The native control of the composed Button.::part(mark)The wrapper of the state's icon or text.::part(icon)The fallback state icon (tp-icon).::part(text)Visible text of text-bearing buttons (playback rate, live).

<tp-media-time-slider>

Attributes

AttributePropertyTypeDefaultDescriptionstepstepnumber1Arrow-key step in seconds.large-steplargeStepnumber10Page Up/Down and Shift+Arrow step in seconds.live-seekliveSeekbooleanfalseAlso seek during a drag (throttled by changeThrottle).change-throttlechangeThrottlenumber100Leading and trailing live-seek throttle in milliseconds.pause-on-dragpauseOnDragbooleanfalsePause while dragging and resume afterwards if playback was running.show-chaptersshowChaptersbooleantrueRender chapter segments when a chapters track exists (show-chapters="false" hides).

Properties

PropertyTypeDefaultDescriptiontimeSliderread-onlyTpMediaTimeSlider—The time slider whose preview this is.previewTimeread-onlynumber | null—The time under the pointer or drag, kept after leaving; null before any.previewRatioread-onlynumber | null—Where previewTime sits along the timeline, from 0 to 1; null before any.previewingread-onlyboolean—Whether the pointer is over the track or a drag is in progress.keyboardInteractionread-onlyboolean—Whether the slider has keyboard focus without pointing or dragging.sliderValueread-onlynumber—The value the slider shows: the media time, or the drag value.boundsread-onlyMediaTimeBounds—The current time-range bounds.

Events

EventTypeDescriptiontp-media-preview-changeCustomEvent<MediaPreviewDetail>The preview changed: detail.time under the pointer and whether the user is previewing. Does not bubble.tp-value-changeTpValueChangeEvent<number>Re-emitted cancelable Slider proposal (detail.value in seconds).tp-value-commitTpValueCommitEvent<number>Re-emitted Slider commit.

Slots

SlotDescriptiondefaultA tp-media-time-slider-preview.

CSS parts

PartDescription::part(slider)Forwarded Slider root; also slider-track, slider-range, slider-buffer, slider-chapter and slider-thumb.

<tp-media-time>

Attributes

AttributePropertyTypeDefaultDescriptiontypetypeMediaTimeType'current'current, duration, remaining, or pointer (inside a time-slider preview).toggletogglebooleanfalseRender a button that switches between elapsed (or duration) and remaining time.negative-signnegativeSignstring'-'Sign rendered before remaining time (aria-hidden).valuevaluenumber | nullnullSeconds that replace the media position (current, remaining) or give the pointer time.

Properties

PropertyTypeDefaultDescriptionshownread-onlyMediaTimeShown—The type currently shown (a toggle may show remaining for type="current").timeViewread-onlyMediaTimeView | undefined—The current clock text, phrase and datetime.

Methods

MethodReturnsDescriptiontoggleShown()voidSwitches a toggle between its two types (no-op without toggle or a time range).focus(options?)voidToggle mode: focus goes to the composed Button's control.blur()voidRemoves focus from the time's toggle button.

CSS parts

PartDescription::part(time)The native <time> element.::part(sign)The remaining-time sign (aria-hidden).::part(value)The clock text.::part(button)The toggle's native control (toggle mode).

<tp-media-hotkey>

Attributes

AttributePropertyTypeDefaultDescriptionkeyskeysstring''The key pattern, such as k, Space or Mod+ArrowRight: comma-separated alternatives.actionactionMediaRequestAction | ''''The media request the keys run, such as toggle-paused or seek-by.valuevaluenumber | nullnullThe request's value in place of its default, such as the seconds of a seek-by.