@playdeck/provider-vimeo
v1.0.0
Published
Vimeo player SDK provider for Playdeck.
Readme
@playdeck/provider-vimeo
The Vimeo provider for Playdeck, over the
@vimeo/player SDK.
pnpm add @playdeck/provider-vimeo@playdeck/react loads this for you when the source resolves to vimeo. The SDK
is bundled as a dependency and imported dynamically — nothing is fetched from a
Vimeo CDN. Provider setup lists the URL
forms that resolve to it, the ones that do not, and the options reachable
through Player.Root.
import { PlayerController } from '@playdeck/core';
import {
createVimeoProvider,
loadVimeoSdk,
PLAYER_READY_TIMEOUT_MS,
resetVimeoSdkLoader
} from '@playdeck/provider-vimeo';
import type { VimeoMountElement } from '@playdeck/provider-vimeo';
declare const mount: VimeoMountElement;
const controller = new PlayerController();
// `hash` is the privacy hash of an unlisted video, which `detectSource` keeps
// when it recognises one in the URL. `dnt` asks Vimeo not to track the session.
controller.setProvider(
createVimeoProvider(
mount,
{ type: 'vimeo', videoId: '76979871', hash: '8272103f6e' },
{ controls: false, dnt: true }
)
);
// The SDK is loaded on demand and cached across players. Pass your own importer
// to serve it from somewhere other than the default module.
export const warm = (): Promise<unknown> => loadVimeoSdk();
// Drops the cached SDK — for tests that need a clean load, not for app code.
export const reset = (): void => resetVimeoSdkLoader();
// How long `player.ready()` is given before the attach reports a recoverable
// error. Command readiness is declared earlier, at player construction, because
// the SDK queues calls it receives before its own ready resolves — but that is
// not a bound, and without this a frame blocked by the page CSP, an extension
// or the network leaves the player loading for ever with no error to render
// (#327).
export const playerReadyTimeout = PLAYER_READY_TIMEOUT_MS; // 15000The embed is chromeless by default (controls: false) so Playdeck's own controls
are the only ones on screen, and dnt is on unless you turn it off. See
Third-party requests and CSP for the full
origins list and what a page's CSP has to allow.
Exports
| Export | What it is |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| createVimeoProvider | Builds the adapter over a mount element and a VimeoSource. |
| VimeoProviderOptions | controls, dnt, loop, startTime, endTime, customControls, suppressSeoMetadata. Through Player.Root, controls, loop, startTime and endTime are its own props (ADR-0004), not bag keys. |
| VimeoSdkLoadOptions | What loadVimeoSdk takes beyond its importer: suppressSeoMetadata, honoured only by the call that actually imports the SDK. |
| VimeoMountElement | What the adapter can mount into. |
| VimeoProviderAdapter | The adapter's own type. |
| VimeoSdkConstructor | What loadVimeoSdk resolves to: the @vimeo/player class, constructed over an iframe. |
| VimeoSdkPlayer | An instance of that class, as the SDK members this adapter uses. |
| VimeoSdkEventListener | The listener VimeoSdkPlayer's on and off take. Its payload is unknown — the SDK decides the shape per event. |
| VimeoSdkModule | The imported SDK module, whose default is the constructor. What an importer handed to loadVimeoSdk has to resolve. |
| VimeoSdkQuality | An entry as getQualities() reports it, auto among them, with active marking the one the player is honouring rather than the rung being rendered. Distinct from core's published PlayerQuality. |
| VimeoSdkChapter | A chapter as getChapters() reports it — a start, a title and a 1-based index, with no end. Distinct from core's published Chapter. |
| VimeoSdkTextTrack | A track as getTextTracks() reports it, including the SDK's own mode. Distinct from core's published TextTrack. |
The VimeoSdk* names above describe @vimeo/player rather than Playdeck. They
are declared here, in src/loader.ts, as the SDK members this adapter uses, so
the loader and the adapter can be typed without binding to the SDK's own
typings — these name only what the adapter calls. Their contents belong to
Vimeo: what a member does, what it reports, whether it exists at all is settled
in an SDK release rather than here, and nothing this project can do makes those
shapes stable for you. They are exported so that a caller holding
what loadVimeoSdk returns has names for it; read them as a description of the
SDK you have installed, not as a contract Playdeck controls.
What it reports honestly
- Chromeless playback needs a paid plan, and checking for one is opt-in.
Pass
customControls: trueandcustomControlsresolves from the account tier behind the video, via a request to Vimeo's public oEmbed endpoint: free and basic accounts reportunavailable/provider-plan, paid tiers reportavailable, and a tier we do not recognise stays unresolved rather than being guessed at. WithoutcustomControls: true, no request is made — the capability staysunknown/provider-check— so no viewer is disclosed to Vimeo before anyone has asked for the capability. That holds throughPlayer.Roottoo: the option is reachable asproviderOptions={{ vimeo: { customControls: true } }}, and the probe fires only when it is set. selectQualityisavailablewith a ladder from the SDK'sgetQualities(). The rung'sheightis Vimeo's own name for it, not a measurement — the rung it labels240prenders at 480×270 — andwidthandbitratearenullbecause the SDK reports neither.autoarrives as a member of the list but is a mode, not a rung, so it is reported asselectedQualityId: null. A quality id that the player never offered is refused before the SDK sees it:setQualitywith an unoffered id never settles at all, so forwarding one would hang the command forever.- Chapters come from the SDK's own chapter list, read once the player is
ready, and kept current by its
chapterchangeevent rather than by polling. The SDK reports a start and a title per chapter and no end at all, so everyendTimeis derived: each chapter ends where the next begins, and the last takes the duration, ornullwhere the duration is not known. - Captions are Playdeck's to draw (
captionRendering: 'custom'): the track is enabled withshowing: false, which makes Vimeo emitcuechangewithout drawing the cues itself.setCaptionRenderer('native')hands drawing back and reportsprovider. Vimeo's cue payload is markup, not plain text — WebVTT tags survive in it and lines are joined with U+21B5 — so it is parsed into plain text rather than passed through. - Cue timings are not reported. The payload carries no start or end, so a cue reports the position it became active at for both bounds.
bufferedis every range, including the gaps a seek leaves behind.liveis never reported.@vimeo/[email protected]publishes no liveness signal at all: its typings (types/player.d.ts,types/events.ts) carry no liveness member on the player and noliveentry inPlayerEventMap, and neither does the subset this adapter declares for itself (src/loader.ts). What the SDK does offer —getDuration(),getSeekable(),getBuffered()and thedurationchangeevent — describes a live event and a video on demand identically: a duration that grows as playback runs on is also what a VOD reports while its metadata settles. So the adapter publishes nolivekey at all rather than a guess — the field is absent from every patch, not present holdingnull. Pinned by "pins the liveness gap" intest/index.test.ts(#187).- The
[startTime, endTime]window is this adapter's to enforce. Vimeo carries a start as a#t=fragment on the embed url, which only keeps the embed from loading at zero — the seek this adapter issues when the player is ready is what the start rests on. There is no end mechanism at all, so the adapter watchestimeupdate: crossingendTimepauses the embed, seeks the playhead back onto the boundary and publishesendedthere, and the pause it caused is not reported as one.loopcomposes with both —loop=1stays on the embed and wraps to zero, and the adapter puts the playhead back atstartTimeafterwards, which also covers the embeds where Vimeo never firesended. Reaching the boundary while looping restarts instead of ending. Sanitisation matches every other provider: a non-finite or non-positive start is no start, an end that is not finite or not above the start is no end, and an end past the duration is clamped to it. startTimeis a floor, not just where playback starts. A reported position below it is pulled back to it, whatever moved the playhead — the SDK's own url-parameter seek, a repeatready, or the viewer dragging Vimeo's scrub bar.seekedis corrected as well astimeupdate, because a paused embed reports no time update after a seek. AseekToorseekBybelow the start is clamped to the same value, so the two agree rather than correcting one position twice, and a correction never triggers another: the position it seeks to is one the window accepts (#381).- A plain looping embed publishes
endedon every iteration, where the native provider publishes none. Withloopand nostartTime,loop=1restarts the embed at zero, which is where the window already begins, so this adapter has nothing to correct and passes Vimeo's ownendedthrough as it always has. The native provider is the one that differs: it swallowsendedfor a looping video and just restarts. That is pre-existing embed behaviour, deliberately left alone by #214 — that change fannedstartTimeandendTimeout to the embeds and did not revise howloopfans out. AstartTimeis what makes this adapter step in. - The SDK sends the embedding page's full URL to the embed, and
suppressSeoMetadatais how you stop it. When the embed answers the SDK's readiness handshake,@vimeo/player's own module-scope listener replies to it withwindow.location.href— path and query included — overpostMessage. The iframe'sreferrerpolicy="strict-origin-when-cross-origin"does not prevent this: it narrows the iframe's own request header, and this travels as a message afterwards. Neither doesdnt. PasssuppressSeoMetadata: true— reachable fromPlayer.RootasproviderOptions={{ vimeo: { suppressSeoMetadata: true } }}— and Playdeck sets the SDK's own guard before the SDK is imported, so the listener is never installed. It is off by default, and with it off nothing about this changes. Two things to know before switching it on. First, the effect is page-wide, not per-embed: the SDK's guard is awindowglobal, so this silences the handshake for every Vimeo embed on the page, including embeds Playdeck did not create, and that blast radius is yours to accept rather than the library's to decide. Second, it takes effect on the first Vimeo attach and holds for the life of the page: the SDK module is imported once and cached, and it reads the guard while it evaluates, so a page that attaches one Vimeo source without the option and a later one with it gets no suppression at all. That is the vendor's design, not something Playdeck works around. A page that has set the guard itself keeps its own value, in either direction; Playdeck only ever writes it when it is not already set. Where that leaves the handshake installed — a later attach, or a guard the page pinned tofalse— the adapter says so: it publishes a non-fatalconfigurationnotice onPlayerState.error, so a fall back to the SDK's default is detectable at runtime and not only readable here. A guard someone else already set totrueis the other direction: suppression is in effect, the request was honoured, and there is nothing to report.
License
MIT.
