feat(packages): add chromecast support via remote playback API#1348
Conversation
|
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
Presets (7)
Media (8)
Players (3)
Skins (17)
UI Components (25)
Sizes are marginal over the root entry point. ⚛️ @videojs/react
Presets (7)
Media (7)
Skins (14)
UI Components (20)
Sizes are marginal over the root entry point. 🧩 @videojs/core
Entries (9)
🏷️ @videojs/element — no changesEntries (2)
📦 @videojs/store — no changesEntries (3)
🔧 @videojs/utils — no changesEntries (10)
📦 @videojs/spf — no changesEntries (3)
ℹ️ How to interpretAll sizes are standalone totals (minified + brotli).
Run |
| // Recompute visibility when cast state changes. | ||
| if (isMediaRemotePlaybackCapable(media)) { | ||
| const onCastChange = () => { | ||
| const { userActive } = get(); | ||
| set({ controlsVisible: computeVisible(userActive) }); | ||
| }; | ||
|
|
||
| listen(media.remote, 'connect', onCastChange, { signal }); | ||
| listen(media.remote, 'disconnect', onCastChange, { signal }); | ||
| } | ||
|
|
There was a problem hiding this comment.
maybe we should override the controlsVisible state in the cast feature? wasn't 100% sure.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed:
exitCastcallsrequestCastinstead of disconnecting session- Added exitCast function that calls castContext().endCurrentSession(true) and updated exitCast and toggleCast methods to use it instead of requestCast.
- ✅ Fixed: HLS Content-Type check fails with header parameters
- Updated isHls function to normalize content-type by splitting on semicolon, trimming, and comparing case-insensitively to handle parameters and case variations.
Or push these changes by commenting:
@cursor push 66cfdf3f5d
Preview (66cfdf3f5d)
diff --git a/packages/core/src/dom/media/castable/utils.ts b/packages/core/src/dom/media/castable/utils.ts
--- a/packages/core/src/dom/media/castable/utils.ts
+++ b/packages/core/src/dom/media/castable/utils.ts
@@ -126,7 +126,10 @@
const response = await fetch(url, { method: 'HEAD' });
const contentType = response.headers.get('Content-Type');
- return HLS_RESPONSE_HEADERS.some((header) => contentType === header);
+ if (!contentType) return false;
+
+ const normalizedContentType = contentType.toLowerCase().split(';')[0]!.trim();
+ return HLS_RESPONSE_HEADERS.some((header) => normalizedContentType === header.toLowerCase());
} catch (err) {
console.error('Error while trying to get the Content-Type of the manifest', err);
return false;
diff --git a/packages/core/src/dom/presentation/cast.ts b/packages/core/src/dom/presentation/cast.ts
--- a/packages/core/src/dom/presentation/cast.ts
+++ b/packages/core/src/dom/presentation/cast.ts
@@ -25,3 +25,17 @@
}
return remote.prompt();
}
+
+export async function exitCast(media: EventTarget) {
+ const remote = resolveRemote(media);
+ if (!remote) {
+ throw new DOMException('Remote playback not supported', 'NotSupportedError');
+ }
+
+ const castCtx = (globalThis as any).cast?.framework?.CastContext?.getInstance();
+ if (!castCtx) {
+ throw new DOMException('Cast context not available', 'NotSupportedError');
+ }
+
+ return castCtx.endCurrentSession(true);
+}
diff --git a/packages/core/src/dom/store/features/cast.ts b/packages/core/src/dom/store/features/cast.ts
--- a/packages/core/src/dom/store/features/cast.ts
+++ b/packages/core/src/dom/store/features/cast.ts
@@ -3,7 +3,7 @@
import type { CastState, MediaCastState } from '../../../core/media/state';
import { definePlayerFeature } from '../../feature';
import { isMediaRemotePlaybackCapable } from '../../media/predicate';
-import { isCastConnected, requestCast } from '../../presentation/cast';
+import { exitCast, isCastConnected, requestCast } from '../../presentation/cast';
import { exitFullscreen, isFullscreenElement } from '../../presentation/fullscreen';
export const castFeature = definePlayerFeature({
@@ -24,14 +24,14 @@
async exitCast() {
const { media } = target();
- return requestCast(media);
+ return exitCast(media);
},
async toggleCast() {
const { media, container } = target();
if (isCastConnected(media)) {
- return requestCast(media);
+ return exitCast(media);
}
if (isFullscreenElement(container, media)) {This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
4b4342b to
01c75f7
Compare
Refs #1043 Made-with: Cursor
The `remote` getter treated `null` the same as "never initialized", so accessing `.remote` after `destroy()` would lazily create a new GoogleCastProvider (leaking listeners on providerInstances). Track a destroyed flag so the getter returns undefined post-destroy. Made-with: Cursor
c21e856 to
346467b
Compare
CAF's RemotePlayerController does not deduplicate handlers. Attaching listeners before requestSession and returning early on cancel (or calling requestCastSession again to stop casting) left stale listeners in place, causing duplicate timeupdate/volumechange/play/pause events. Make attach/detach idempotent via a #listenersAttached flag and route every call site through the private helpers. Inline the listener helpers into the provider, then compact surrounding code: simplify onCastStateChanged, PLAYER_STATE_CHANGED, load()'s HLS/subtitle blocks, and #updateRemoteTextTrack; dedupe the disableRemotePlayback guard in RemotePlayback; drop the brittle globalThis casts in utils. Made-with: Cursor
The recent change typed CastableMediaBase.remote as our concrete RemotePlayback class, which broke the mixin's superclass constraint: HTMLMediaElementHost.remote is the DOM RemotePlayback (structurally different due to private fields on our class) and is optional because target may be null. The constraint failure cascaded into MuxDataMedia, MediaHost, and React/html wrappers. Use the existing structural RemotePlaybackLike interface in the constraint and widen the mixin output to accept either our class or a RemotePlaybackLike fallback returned via super.remote. Made-with: Cursor
Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3999ef2. Configure here.


Refs #1043
Closes #1355
Closes #1356
Closes #1357
Closes #1358
Closes #1359
Closes #1360
Test url: https://v10-sandbox-git-feat-1043-cast-support-mux.vercel.app/?platform=html&styling=css&preset=mux-video&skin=default&source=hls-1
Summary
Add Chromecast support using a
RemotePlaybackpolyfill backed by the Cast Application Framework (CAF) sender SDK. The implementation follows the same feature pattern as fullscreen and picture-in-picture — aCastableMediaMixin, acastFeaturestore slice, and aCastButtonUI component across HTML and React packages.Sub-issues
CastButtoncomponent across core, HTML, and ReactChanges
CastableMediaMixinwraps any media host with aRemotePlayback-shaped API that delegates to CAF sender sessionscastFeatureexposescastState,castAvailability,requestCast,exitCast, andtoggleCastas reactive stateCastButtoncomponent (core, HTML custom element, React) with enter/exit icon toggling viadata-cast-stateImplementation details
RemotePlaybackpolyfill mirrors the browser API surface (state,prompt(),watchAvailability(),cancelWatchAvailability()) so the feature layer doesn't need to know whether it's using native remote playback or the CAF polyfillCastableMediaMixinloads the CAF sender SDK on-demand and manages theCastContext/CastSessionlifecyclecontrolsFeaturelistens forconnect/disconnectevents on the remote object to recompute visibility@types/chromeand@types/chromecast-caf-senderadded as dev dependencies for type-safe CAF SDK usageTesting
CastButtonCorestate derivationMade with Cursor
Note
High Risk
High risk because it introduces a new remote playback/casting layer that intercepts core media operations (play/pause/seek/volume/rate/load) and adds network fetches for HLS probing, affecting playback behavior across video presets.
Overview
Adds Chromecast/remote playback support by introducing a
RemotePlayback-shaped polyfill backed by Google Cast CAF, plus aCastableMediaMixinthat routes media operations to a remote session while casting.Exposes casting as a first-class player capability via new
MediaCastState/castFeature+selectCast, updatescontrolsFeatureto keep controls visible while connecting/connected, and wires a newCastButtonacross core (CastButtonCore), HTML (media-cast-button), and React, including icon/skin integration.Also updates media host plumbing (e.g.,
HTMLMediaElementHostattach/detach/query helpers,HTMLVideoElementHost.poster), makes Mux presets cast-capable, adds CAF/chrome type deps and TS config, and includes unit tests + docs for the new cast feature.Reviewed by Cursor Bugbot for commit dbba87f. Bugbot is set up for automated code reviews on this repo. Configure here.