docs(site): add autoplay how-to guide - #1534
Conversation
Explains how browser autoplay policies work, why they block playback, and shows two patterns for handling them — declarative attributes for the simple case, and a store-routed orchestrator (usePlayer / PlayerController with selectPlayback and selectVolume) for when you need to detect failure and fall back to muted retry. Closes #1039. https://claude.ai/code/session_01QLEz6mPy8579e4QXq3h1L8
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
✅ Deploy Preview for vjs10-site ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📦 Bundle Size Report🎨 @videojs/html — no changesPresets (7)
Media (10)
Players (5)
Skins (30)
UI Components (39)
Sizes are marginal over the root entry point. ⚛️ @videojs/react — no changesPresets (7)
Media (9)
Skins (27)
UI Components (33)
Sizes are marginal over the root entry point. 🧩 @videojs/core — no changesEntries (68)
🏷️ @videojs/element — no changesEntries (2)
📦 @videojs/store — no changesEntries (3)
🔧 @videojs/utils — no changesEntries (11)
📦 @videojs/spf — no changesEntries (4)
ℹ️ How to interpretJS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.
Run |
Fixes a TypeScript syntax error in the HTML AutoplayHandlerElement snippet (`typeof this.#playback.value` isn't valid in a type query), restructures #tryPlay to re-read the controller value instead. Also tightens prose throughout: drops a filler 'just', adds a missing oxford comma, replaces a dangling 'doesn't' with 'fails', collapses parallel intro paragraphs into the surrounding FrameworkCase blocks so the markup tracks the reader path more cleanly. Verified by extracting both snippets into the site source and running `pnpm astro check` — 0 errors. https://claude.ai/code/session_01QLEz6mPy8579e4QXq3h1L8
Flattens both handlers so the success/failure tree reads top to bottom without nested catches or method hops. React: switches from .then().catch() to .then(success, failure), drops the Ref suffix on the one-shot guard. HTML: collapses #tryPlay and #emit into update() via an inline async attempt() that returns the status, dispatched once. Both still verified against `pnpm astro check`. https://claude.ai/code/session_01QLEz6mPy8579e4QXq3h1L8
The previous React snippet was a component that called play() in a useEffect — which throws NO_TARGET synchronously, because the provider's store.attach() runs after child effects. Verified via test. React now exposes a useAutoplay() hook that subscribes to the store and waits for store.target before attempting play. Returns the status directly so consumers conditionally render their fallback UI. HTML drops the custom-element wrapper entirely and uses an inline <script type="module"> that reads the store off the <video-player> instance. The subscribe-then-retry pattern handles the case where media hasn't yet attached when the script first runs. Exposes status via a data-autoplay attribute so CSS can drive the fallback. Both verified with vitest behavior tests (playing / muted / blocked status outcomes) using stubbed HTMLMediaElement.prototype.play. https://claude.ai/code/session_01QLEz6mPy8579e4QXq3h1L8
|
@mihar-22 skipping the rest of your comments for now. This is still a draft and a lot of that is going to change. If we even pursue this guide; sounds like we're reopening the "fix this in code instead of guides" conversation again. |
…-doc-fIB5G # Conflicts: # site/src/docs.config.ts
Restructure the autoplay how-to around detecting and recovering from blocked playback, with corrected React (flat-state selector) and HTML (store.state) examples. Add createPlayer/usePlayer snippets to the use-player reference and fix the standalone import.
|
|
||
| ## Detect and recover from blocked autoplay | ||
|
|
||
| The `autoplay` attribute is fire-and-forget: the browser plays your video or quietly does nothing. To know which happened so you can retry muted, to log the outcome, or to show a "click to play" overlay, call `play()` yourself. It returns a promise that resolves on success and rejects when the browser blocks playback. |
There was a problem hiding this comment.
Second sentence reads a little cumbersome.
suggest:
The autoplay attribute is fire-and-forget: the browser plays your video or quietly does nothing. Instead, call play() so you can retry muted, log the outcome, or show a “click to play” overlay. It returns a promise that resolves on success and rejects when the browser blocks playback.
| <FrameworkCase frameworks={["html"]}> | ||
| ```js | ||
| const player = document.querySelector('video-player'); | ||
| player.store.state.play().then(() => { |
There was a problem hiding this comment.
note: player.store.state.play() feels pretty verbose, and state.play() feels a little off. Would love to revisit this at some point. I think it'd be part of the HTML API conversation we didn't make it to last week.
| ``` | ||
| </FrameworkCase> | ||
|
|
||
| Drop the `autoplay` attribute from your media when you do this. Combining the declarative attribute with a manual `play()` call leads to duplicate attempts and inconsistent state. |
There was a problem hiding this comment.
suggest: Move this above the code snippet so it doesn't get missed.
| Drop the `autoplay` attribute from your media when you do this. Combining the declarative attribute with a manual `play()` call leads to duplicate attempts and inconsistent state. | ||
|
|
||
| <Aside type="note"> | ||
| A rejected `play()` promise is a [`DOMException`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException) whose `name` tells you the cause. Two are standardized: `NotAllowedError`, the autoplay policy blocking playback, and `NotSupportedError`, when the source isn't a supported media format. Some browsers also reject with `AbortError` when an unrelated call interrupts playback. Most handling can treat every rejection the same; to log only true autoplay failures, branch on `error.name === 'NotAllowedError'`. |
There was a problem hiding this comment.
suggest: Make this part of the example code. Check for NotAllowedError specifically.
There was a problem hiding this comment.
Got rid of the aside and reorganized it into something more tolerable, including moving NotAllowedError into the example code
|
|
||
| When muted autoplay fails too, playback won't start without the viewer. Treat that as expected, not as an error: | ||
|
|
||
| - **Keep the poster visible.** The player isn't loading, it's waiting. Don't show a buffering spinner. |
There was a problem hiding this comment.
This isn't the reader's job, it's Video.js's.
suggest: Verify video.js handles this well when using the code above, and then tell the reader that's video.js will do for them.
| When muted autoplay fails too, playback won't start without the viewer. Treat that as expected, not as an error: | ||
|
|
||
| - **Keep the poster visible.** The player isn't loading, it's waiting. Don't show a buffering spinner. | ||
| - **Expose a clear play affordance.** A play button over the poster is the conventional pattern. The player's own controls already do this, so make sure they render in the blocked state. |
There was a problem hiding this comment.
play affordance
suggest: less fancy wordses. If we had a reading-level target/checker on these docs I'd expect this to fail.
A play button over the poster is the conventional pattern.
suggest: Also frame this as what the default player skin should do in this case rather than giving them a chore to figure out
note: We should have more to say about background videos specifically, probably in the bg video doc, and understand clearly how the background video media works in each of the problem scenarios.
There was a problem hiding this comment.
Love me some affordances. Hate me some jargon. Not sure how that slipped in, thanks.
In the case of background videos... by default the media is autoplay muted. Which should work in every scenario... but... what if it doesn't? Hmm...
|
|
||
| - **Keep the poster visible.** The player isn't loading, it's waiting. Don't show a buffering spinner. | ||
| - **Expose a clear play affordance.** A play button over the poster is the conventional pattern. The player's own controls already do this, so make sure they render in the blocked state. | ||
| - **Don't log a blocked autoplay as a fatal error.** It's the browser doing its job. If your analytics counts every `play()` rejection as a playback failure, your error rates will spike on first visits where the policy hasn't been earned yet. Distinguish a `NotAllowedError` on the initial attempt from real playback errors. |
There was a problem hiding this comment.
I'm not quite sure where this would happen. The error doesn't bubble automatically? But I feel like we should give them a full example code that does this for them.
Distinguish a
NotAllowedErroron the initial attempt
I don't think the "initial attempt" is special compared to later attempts firing this error?
There was a problem hiding this comment.
iirc the intention here was to discourage people from logging an error if it's just NotAllowedError. Instead, they should try again muted or offer a button. Made that more clear.
| - **Expose a clear play affordance.** A play button over the poster is the conventional pattern. The player's own controls already do this, so make sure they render in the blocked state. | ||
| - **Don't log a blocked autoplay as a fatal error.** It's the browser doing its job. If your analytics counts every `play()` rejection as a playback failure, your error rates will spike on first visits where the policy hasn't been earned yet. Distinguish a `NotAllowedError` on the initial attempt from real playback errors. | ||
|
|
||
| Wire your fallback UI to the status the handler exposes, and show the overlay only once it reaches `'blocked'`. |
There was a problem hiding this comment.
reaches
'blocked'
Is this a formal state somewhere or a made up one?
Wire your fallback UI
This part makes it feel like a half solution. I feel like we could either show them an example UI for this purpose, or assume/verify that the existing player UI handles it fine already when autoplay is blocked, i.e. shows the controls.
There was a problem hiding this comment.
Good call. Pared this way back and made clear that our player ui components show the controls for them
Adds an autoplay how-to guide (and rounds out some problems Rahim and I saw with
usePlayeralong the way)Closes #1854
Note
Low Risk
Documentation-only changes with no runtime or API behavior impact.
Overview
Adds a new How to → Autoplay guide that explains browser autoplay constraints, recommends
muted/playsInline, and documents callingplay()manually (instead of relying on theautoplayattribute) so apps can handle promise rejections— including React viausePlayerand HTML via the player store—with a table of commonDOMExceptionnames and notes on default component behavior when playback is blocked.The
usePlayerreference page now opens with a small example contrasting direct store access vs selector subscriptions.The guide is registered in
docs.config.tsunder the How-to section.Reviewed by Cursor Bugbot for commit e98b91d. Bugbot is set up for automated code reviews on this repo. Configure here.