@playdeck/provider-native
v1.1.0
Published
HTMLMediaElement provider for Playdeck: progressive files and native HLS.
Maintainers
Readme
@playdeck/provider-native
The HTMLMediaElement provider for Playdeck:
progressive MP4/WebM, and HLS in browsers that play it natively (Safari, iOS).
pnpm add @playdeck/provider-native@playdeck/react loads this for you when the source resolves to video — an
.mp4 or .webm path, or an explicit { type: 'video' } source; see
Provider setup.
See "Without React" below for driving a PlayerController yourself.
import * as Player from '@playdeck/react';
// A native source is an `.mp4`/`.webm` URL in the `source` prop.
// `loop`, `startTime` and `endTime` are `Player.Root`'s own props on every
// provider (ADR-0004), never keys in a provider's option bag — native takes no
// `providerOptions` key of its own at all.
export const ClipWithCaptions = () => (
<Player.Root
loop
startTime={30}
endTime={45}
source="https://example.com/clip.mp4"
>
<Player.Viewport>
{/* `textTracks` reaches native playback directly, unlike the embed
providers, where only captions a provider discovers for itself are
available. */}
<Player.Media
textTracks={[
{ src: '/captions.en.vtt', srcLang: 'en', label: 'English' }
]}
/>
<Player.Captions />
<Player.Controls>
<Player.PlayButton />
<Player.SeekSlider />
<Player.Time type="current" />
<Player.CaptionsButton />
<Player.PipButton />
{/* Renders only where there is a receiver to cast to. */}
<Player.AirPlayButton />
<Player.FullscreenButton />
</Player.Controls>
<Player.ErrorDisplay />
</Player.Viewport>
</Player.Root>
);Without React
Reach for this package directly when you are writing a provider adapter, or hosting a player somewhere other than React.
import { PlayerController } from '@playdeck/core';
import { createNativeProvider } from '@playdeck/provider-native';
declare const videoElement: HTMLVideoElement;
const controller = new PlayerController();
// Plays anything the element itself can play — MP4, WebM, and HLS on Safari,
// where the browser has its own HLS support.
controller.setProvider(
createNativeProvider(videoElement, {
loop: true,
// A clip out of a longer file: playback is clamped to this window.
startTime: 30,
endTime: 45
})
);
export const play = (): Promise<unknown> => controller.play();Exports
| Export | What it is |
| ----------------------- | ----------------------------------------------- |
| createNativeProvider | Builds the adapter over a <video> element. |
| NativePlaybackOptions | loop, startTime, endTime. |
| NativeProviderAdapter | The adapter's own type, if you need to name it. |
What it reports honestly
Seeking is clamped to the element's
seekableranges intersected with anystartTime/endTimeyou configured. A seek with nowhere legal to land is refused withprovider-errorrather than snapped somewhere outside your bounds.A
startTimethe source cannot be positioned at publishes a non-fatalconfigurationnotice onPlayerState.errorrather than disappearing. The offset is considered exactly once per load, at the firstloadedmetadata; it is bounded by the media's own duration, so an offset past the end of the clip is still refused, and the element'sseekableranges decide whether the element will move at all rather than where it lands — a window that does not reach the offset is a refusal, never a nudge onto its nearest edge. The playhead is then read back to confirm it arrived, so an element that takes the write and stays put is reported too. The notice is how you tell any of that apart from a setting you mis-wired. It does not make the offset apply.The playhead is confirmed on the same tick, and again once the element reports it is no longer seeking, which is what makes the notice reliable on WebKit too. The first read is in the same tick as the write; chromium and firefox clamp before the write's setter returns, so that read already sees a refusal there. Where it does not, the provider watches
media.seeking, the same flag the HTML seek algorithm itself clears once a seek concludes, refusal or not, and takes its deferred read the moment that flag reads false -- catching an engine whose setter answers the write before its own seek has finished deciding.A refusal at that single attempt is permanent for the load. Nothing reconsiders it afterward — not a
seekablewindow that later widens past the requested offset, not any other change of state before the next load. Theconfigurationnotice above is the record of the refusal: once it has fired for a load, nothing re-applies the offset or retracts the notice for that same load. Only a fresh load — a new source, or an explicitretry— gives the offset another attempt, and that attempt's own decision replaces the previous one: a retry whose reload reaches the requested offset withdraws the notice, leavingPlayerState.errorclear, exactly as a retry that refuses again keeps it standing.selectQualityisunavailablewith reasonsource: the browser picks its own rendition for native HLS and there is nothing to enumerate. It is notunknown, because that would promise an answer that never comes.airPlayfollows WebKit'swebkitplaybacktargetavailabilitychanged, so it means "there is a receiver to cast to", not "this browser has the picker API". It goes back tounavailablewhen the route disappears.Captions are Playdeck's to draw by default (
captionRendering: 'custom');setCaptionRenderer('native')hands them back to the browser's own renderer.livecomes from the element's own signals: an endlessdurationand the movingseekablewindow, measured against the playhead. Never from the source URL. A file with a finite duration reportsnull, and the value is published again only when it changes.commandsReadyis declared aftermedia.load(), becauseload()resetsplaybackRateand anything applied earlier would be silently undone.Chapters come from a
kind="chapters"text track. Its mode is moved tohidden, because a track's cues are never obtained while its mode isdisabled, and its cues are read on the track'scuechangeand the<track>element'sload— not at the mode write, where there is nothing to read yet. The track stays out oftextTracks: chapters are their own collection.
License
MIT.
