@playdeck/provider-youtube
v1.0.0
Published
YouTube IFrame Player API provider for Playdeck.
Maintainers
Readme
@playdeck/provider-youtube
The YouTube provider for Playdeck, over the IFrame Player API.
pnpm add @playdeck/provider-youtube@playdeck/react loads this for you when the source resolves to youtube.
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 {
API_READY_TIMEOUT_MS,
PLAYBACK_CONFIRMATION_TIMEOUT_MS,
PLAYER_READY_TIMEOUT_MS,
createYouTubeProvider,
loadYouTubeIframeApi,
resetYouTubeIframeApiLoader
} from '@playdeck/provider-youtube';
declare const mount: HTMLElement;
const controller = new PlayerController();
// The embed defaults to youtube-nocookie.com; `host` opts back out of it.
controller.setProvider(createYouTubeProvider(mount, 'dQw4w9WgXcQ'));
// The iframe API is loaded for you. Call this directly only to warm it before
// a player mounts.
export const warm = (): Promise<unknown> => loadYouTubeIframeApi();
// Drops the memo of the API load — for tests that need a clean load, not for
// app code.
export const reset = (): void => resetYouTubeIframeApiLoader();
// How long the script is given to hand over the API before the load is
// reported as failed. A response that is 200 OK but is not the API fires no
// error event, so without this every player on the page would wait for ever.
export const apiReadyTimeout = API_READY_TIMEOUT_MS; // 15000
// How long a play command waits for YouTube to confirm playback started before
// it is reported as blocked, rather than resolving a promise that never lands.
export const confirmationTimeout = PLAYBACK_CONFIRMATION_TIMEOUT_MS; // 3000
// How long the *player* is given to answer the constructor with `onReady`,
// which the two deadlines above do not cover: one bounds the script, the other
// bounds a play command. A page CSP without `frame-src
// www.youtube-nocookie.com`, a blocking extension or a captive portal leaves
// the frame present and silent, and without this the player waits for ever
// with no error to render (#327).
export const playerReadyTimeout = PLAYER_READY_TIMEOUT_MS; // 15000The embed host defaults to https://www.youtube-nocookie.com, and the API
script is loaded from https://www.youtube.com/iframe_api once per document.
host is honoured only for the two origins YouTube serves the embed from —
https://www.youtube.com and https://www.youtube-nocookie.com, matched on
the parsed origin. Any other value falls back to the default rather than
throwing, so a misconfigured host still plays.
The embedding origin is declared to the player so it can validate the
postMessage traffic it exchanges with the iframe. 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 |
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| createYouTubeProvider | Builds the adapter over a mount element and a video id. |
| YouTubeProviderOptions | controls, loop, startTime, endTime, host, and loadIframeApi to supply the API yourself. Through Player.Root, controls, loop, startTime and endTime are its own props (ADR-0004), not bag keys. |
| YouTubeProviderAdapter | The adapter's own type. |
| PLAYBACK_CONFIRMATION_TIMEOUT_MS | How long a play() waits for the player to confirm it (3 seconds). |
| resetYouTubeIframeApiLoader | Drops the memo of the API load — for tests that need a clean load. |
| API_READY_TIMEOUT_MS | How long the API script is given to hand over the API before the load is reported as failed (15 seconds). |
| YouTubeIframeApi | What loadYouTubeIframeApi resolves to: the API object the script installs, whose Player is the constructor. |
| YouTubePlayerConstructor | That Player, constructed over an iframe and a YouTubePlayerOptions. |
| YouTubePlayerOptions | The constructor's second argument. This adapter hands the constructor an iframe that already carries the embed, so events is what it passes through here. |
| YouTubePlayerEventHandlers | That events bag — the onReady, onStateChange and sibling callbacks the player invokes. |
| YouTubePlayer | A constructed player, as the API members this adapter uses. |
The YouTube* names above describe Google's IFrame Player API rather than
Playdeck. The API arrives as a script that installs a global rather than as a
module this package imports, so they are declared here, in src/loader.ts, as
the members this adapter calls, and they name only those. Their contents belong
to Google: what a member does, what it reports, whether it exists at all is
settled by whatever the script serves, and nothing this project can do makes
those shapes stable for you. They are exported so that a caller
holding what loadYouTubeIframeApi returns — or supplying its own API through
loadIframeApi — has names for it; read them as a description of the API the
page loaded, not as a contract Playdeck controls.
What it reports honestly
selectQualityisunavailable/provider. YouTube can enumerate levels but will not honour a choice: measured against the live IFrame API,setPlaybackQualitywas accepted and discarded for every level the player itself offered — including when followed by a seek, and when passed asloadVideoById({ suggestedQuality }), where the player announced its own choice regardless. Asking for the lowest rung fails the same way as the highest, which rules out a bandwidth ceiling. So no ladder is published, andqualitiesstays empty rather than filling a menu with rungs that do nothing.bufferedis one range, anchored where playback entered it. The IFrame API exposes no ranges — onlygetVideoLoadedFraction(), which reports the end of the range holding the playhead. Buffer loaded before you arrived is invisible. Everything reported is genuinely buffered; it reports less than it holds, never more.- Captions are drawn by YouTube (
captionRendering: 'provider'), orunavailablewhen the embed exposes no tracks. Track discovery uses the undocumentedcaptionsmodule, so it follows community-observed conventions rather than a published contract. pictureInPictureisunavailable: the embed owns its own video element.- Chapters are never reported. The IFrame Player API documents no chapter
method and no chapter event, and the Data API's video resource has no chapter
property, so
chaptersstays empty andcapabilities.chaptersreportsunavailable/provider. No command fails over it. liveis never reported. The IFrame Player API surface this adapter declares (src/loader.ts) carries no liveness member, and nothing else the API offers separates a broadcast from a video on demand: on a live streamgetDuration()returns the time elapsed since the broadcast began, which is the same shape as a VOD duration and grows the same way one does while metadata is still settling. So the adapter publishes nolivekey at all rather than a guess — the field is absent from every patch, not present holdingnull. The undocumentedgetVideoData().isLivewas considered and rejected: it is not part of the declared surface, Google does not support it, and there is no live-stream fixture in this repo to verify it against, so taking it would trade an honest gap for a claim nobody here can check. Pinned by "pins the liveness gap" intest/index.test.ts(#187).startTimeandendTimeare enforced by this adapter, not by YouTube. Thestartplayer var is written as a load hint so the embed does not load from zero, but it is whole-second only, so the adapter seeks to the exact start once the player is ready. Theendvar is not written at all: it is whole-second too, its interaction with theloopplus single-entry-playlist pair is undocumented, and it is not known to publish the state change the adapter needs. The end boundary comes from the 250 ms position poll instead, so it can overshoot by up to that much beforeendedis published — the publishedcurrentTimeis pinned to the boundary and the playhead is seeked back onto it, so what is on screen and what is published agree (#381).startTimeis a floor, not just where playback starts. Every polled position below it is pulled back to it, whatever moved the playhead — including the viewer's own drag of YouTube's scrub bar undercontrols: true, which reaches this adapter as nothing but a position. 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, YouTube's playlist loop restarts at zero, which is where the window already begins, so this adapter has nothing to correct and passes the ENDED state change through 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.
License
MIT.
