{
+ asChild?: boolean;
+ children?: React.ReactNode;
+ ref?: React.Ref
;
+}
+
+/**
+ * A button for requesting Google Cast.
+ *
+ * @see {@link https://developers.google.com/cast/docs/overview}
+ * @docs {@link https://www.vidstack.io/docs/player/components/buttons/google-cast-button}
+ * @example
+ * ```tsx
+ *
+ *
+ *
+ * ```
+ */
+const GoogleCastButton = React.forwardRef(
+ ({ children, ...props }, forwardRef) => {
+ return (
+ )}>
+ {(props) => (
+
+ {children}
+
+ )}
+
+ );
+ },
+);
+
+GoogleCastButton.displayName = 'GoogleCastButton';
+export { GoogleCastButton };
diff --git a/packages/react/src/globals.d.ts b/packages/react/src/globals.d.ts
index 19d91a1ac..b773fc1db 100644
--- a/packages/react/src/globals.d.ts
+++ b/packages/react/src/globals.d.ts
@@ -1,6 +1,3 @@
-declare global {
- const __DEV__: boolean;
- const __SERVER__: boolean;
-}
+///
export {};
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index abb030859..3ccb268ba 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -26,6 +26,10 @@ export type {
// Buttons
export { type ToggleButtonProps, ToggleButton } from './components/ui/buttons/toggle-button';
export { type AirPlayButtonProps, AirPlayButton } from './components/ui/buttons/airplay-button';
+export {
+ type GoogleCastButtonProps,
+ GoogleCastButton,
+} from './components/ui/buttons/google-cast-button';
export { type PlayButtonProps, PlayButton } from './components/ui/buttons/play-button';
export { type CaptionButtonProps, CaptionButton } from './components/ui/buttons/caption-button';
export {
diff --git a/packages/react/src/providers/remotion/loader.ts b/packages/react/src/providers/remotion/loader.ts
index 5d031d7b7..c9c9a24a8 100644
--- a/packages/react/src/providers/remotion/loader.ts
+++ b/packages/react/src/providers/remotion/loader.ts
@@ -1,10 +1,18 @@
import * as React from 'react';
-import type { MediaProviderAdapter, MediaProviderLoader, MediaSrc, MediaType } from 'vidstack';
+import type {
+ MediaContext,
+ MediaProviderAdapter,
+ MediaProviderLoader,
+ MediaSrc,
+ MediaType,
+} from 'vidstack';
import * as UI from '../../components/layouts/remotion-ui';
export class RemotionProviderLoader implements MediaProviderLoader {
+ readonly name = 'remotion';
+
target!: HTMLElement;
constructor() {
@@ -20,7 +28,7 @@ export class RemotionProviderLoader implements MediaProviderLoader {
return 'video';
}
- async load(): Promise {
- return new (await import('./provider')).RemotionProvider(this.target);
+ async load(ctx: MediaContext): Promise {
+ return new (await import('./provider')).RemotionProvider(this.target, ctx);
}
}
diff --git a/packages/react/src/providers/remotion/provider.tsx b/packages/react/src/providers/remotion/provider.tsx
index b3a9f5d33..75cc23c63 100644
--- a/packages/react/src/providers/remotion/provider.tsx
+++ b/packages/react/src/providers/remotion/provider.tsx
@@ -11,12 +11,7 @@ import {
type SetTimelineContextValue,
type TimelineContextValue,
} from 'remotion';
-import {
- TimeRange,
- type MediaProviderAdapter,
- type MediaSetupContext,
- type MediaSrc,
-} from 'vidstack';
+import { TimeRange, type MediaContext, type MediaProviderAdapter, type MediaSrc } from 'vidstack';
import { RemotionLayoutEngine } from './layout-engine';
import { RemotionPlaybackEngine } from './playback-engine';
@@ -31,7 +26,6 @@ export class RemotionProvider implements MediaProviderAdapter {
readonly scope = createScope();
- protected _ctx!: MediaSetupContext;
protected _src = signal(null);
protected _setup = false;
protected _played = 0;
@@ -74,12 +68,14 @@ export class RemotionProvider implements MediaProviderAdapter {
return this._frame();
}
- constructor(readonly container: HTMLElement) {
+ constructor(
+ readonly container: HTMLElement,
+ protected readonly _ctx: MediaContext,
+ ) {
this._layoutEngine.setContainer(container);
}
- setup(ctx: MediaSetupContext) {
- this._ctx = ctx;
+ setup() {
effect(this._watchWaiting.bind(this));
effect(this._watchMediaTags.bind(this));
effect(this._watchMediaElements.bind(this));
@@ -229,10 +225,10 @@ export class RemotionProvider implements MediaProviderAdapter {
this._notify('rate-change', rate);
}
- protected _getPlayedRange(currentTime: number) {
- return this._played >= currentTime
+ protected _getPlayedRange(time: number) {
+ return this._played >= time
? this._playedRange
- : (this._playedRange = new TimeRange(0, (this._played = currentTime)));
+ : (this._playedRange = new TimeRange(0, (this._played = time)));
}
async loadSource(src: MediaSrc) {
@@ -252,7 +248,7 @@ export class RemotionProvider implements MediaProviderAdapter {
onError: (error) => {
if (__DEV__) {
this._ctx.logger
- ?.errorGroup(error.message)
+ ?.errorGroup(`[vidstack] ${error.message}`)
.labelledLog('Source', peek(this._src))
.labelledLog('Error', error)
.dispatch();
@@ -315,8 +311,8 @@ export class RemotionProvider implements MediaProviderAdapter {
if (!$src) {
throw Error(
__DEV__
- ? '[vidstack]: attempting to render remotion provider without src'
- : '[vidstack]: no src',
+ ? '[vidstack] attempting to render remotion provider without src'
+ : '[vidstack] no src',
);
}
diff --git a/packages/react/src/providers/remotion/validate.ts b/packages/react/src/providers/remotion/validate.ts
index 97f9061db..77f7a1b28 100644
--- a/packages/react/src/providers/remotion/validate.ts
+++ b/packages/react/src/providers/remotion/validate.ts
@@ -47,7 +47,7 @@ export function validateInitialFrame(initialFrame: number | undefined, frames: n
if (!isNumber(frames)) {
throw new Error(
- `[vidstack]: \`durationInFrames\` must be a number, but is ${JSON.stringify(frames)}`,
+ `[vidstack] \`durationInFrames\` must be a number, but is ${JSON.stringify(frames)}`,
);
}
@@ -57,27 +57,27 @@ export function validateInitialFrame(initialFrame: number | undefined, frames: n
if (!isNumber(initialFrame)) {
throw new Error(
- `[vidstack]: \`initialFrame\` must be a number, but is ${JSON.stringify(initialFrame)}`,
+ `[vidstack] \`initialFrame\` must be a number, but is ${JSON.stringify(initialFrame)}`,
);
}
if (Number.isNaN(initialFrame)) {
- throw new Error(`[vidstack]: \`initialFrame\` must be a number, but is NaN`);
+ throw new Error(`[vidstack] \`initialFrame\` must be a number, but is NaN`);
}
if (!Number.isFinite(initialFrame)) {
- throw new Error(`[vidstack]: \`initialFrame\` must be a number, but is Infinity`);
+ throw new Error(`[vidstack] \`initialFrame\` must be a number, but is Infinity`);
}
if (initialFrame % 1 !== 0) {
throw new Error(
- `[vidstack]: \`initialFrame\` must be an integer, but is ${JSON.stringify(initialFrame)}`,
+ `[vidstack] \`initialFrame\` must be an integer, but is ${JSON.stringify(initialFrame)}`,
);
}
if (initialFrame > frames - 1) {
throw new Error(
- `[vidstack]: \`initialFrame\` must be less or equal than \`durationInFrames - 1\`, but is ${JSON.stringify(
+ `[vidstack] \`initialFrame\` must be less or equal than \`durationInFrames - 1\`, but is ${JSON.stringify(
initialFrame,
)}`,
);
@@ -93,25 +93,25 @@ export function validateSingleFrame(frame: unknown, variableName: string): numbe
if (!isNumber(frame)) {
throw new TypeError(
- `[vidstack]: \`${variableName}\` must be a number, but is ${JSON.stringify(frame)}`,
+ `[vidstack] \`${variableName}\` must be a number, but is ${JSON.stringify(frame)}`,
);
}
if (Number.isNaN(frame)) {
throw new TypeError(
- `[vidstack]: \`${variableName}\` must not be NaN, but is ${JSON.stringify(frame)}`,
+ `[vidstack] \`${variableName}\` must not be NaN, but is ${JSON.stringify(frame)}`,
);
}
if (!Number.isFinite(frame)) {
throw new TypeError(
- `[vidstack]: \`${variableName}\` must be finite, but is ${JSON.stringify(frame)}`,
+ `[vidstack] \`${variableName}\` must be finite, but is ${JSON.stringify(frame)}`,
);
}
if (frame % 1 !== 0) {
throw new TypeError(
- `[vidstack]: \`${variableName}\` must be an integer, but is ${JSON.stringify(frame)}`,
+ `[vidstack] \`${variableName}\` must be an integer, but is ${JSON.stringify(frame)}`,
);
}
@@ -135,26 +135,26 @@ export function validateInOutFrames(
// Must not be over the duration
if (!isNull(validatedInFrame) && validatedInFrame > frames - 1) {
throw new Error(
- `[vidstack]: \`inFrame\` must be less than (durationInFrames - 1), but is \`${validatedInFrame}\``,
+ `[vidstack] \`inFrame\` must be less than (durationInFrames - 1), but is \`${validatedInFrame}\``,
);
}
if (!isNull(validatedOutFrame) && validatedOutFrame > frames) {
throw new Error(
- `[vidstack]: \`outFrame\` must be less than (durationInFrames), but is \`${validatedOutFrame}\``,
+ `[vidstack] \`outFrame\` must be less than (durationInFrames), but is \`${validatedOutFrame}\``,
);
}
// Must not be under 0
if (!isNull(validatedInFrame) && validatedInFrame < 0) {
throw new Error(
- `[vidstack]: \`inFrame\` must be greater than 0, but is \`${validatedInFrame}\``,
+ `[vidstack] \`inFrame\` must be greater than 0, but is \`${validatedInFrame}\``,
);
}
if (!isNull(validatedOutFrame) && validatedOutFrame <= 0) {
throw new Error(
- `[vidstack]: \`outFrame\` must be greater than 0, but is \`${validatedOutFrame}\`. If you want to render a single frame, use \`\` instead.`,
+ `[vidstack] \`outFrame\` must be greater than 0, but is \`${validatedOutFrame}\`. If you want to render a single frame, use \`\` instead.`,
);
}
@@ -164,7 +164,7 @@ export function validateInOutFrames(
validatedOutFrame <= validatedInFrame
) {
throw new Error(
- '[vidstack]: `outFrame` must be greater than `inFrame`, but is ' +
+ '[vidstack] `outFrame` must be greater than `inFrame`, but is ' +
validatedOutFrame +
' <= ' +
validatedInFrame,
@@ -177,7 +177,7 @@ export function validateSharedNumberOfAudioTags(tags: number | undefined) {
if (tags % 1 !== 0 || !Number.isFinite(tags) || Number.isNaN(tags) || tags < 0) {
throw new TypeError(
- `[vidstack]: \`numberOfSharedAudioTags\` must be an integer but got \`${tags}\` instead`,
+ `[vidstack] \`numberOfSharedAudioTags\` must be an integer but got \`${tags}\` instead`,
);
}
}
@@ -187,18 +187,18 @@ export function validatePlaybackRate(playbackRate: number) {
if (playbackRate > 4) {
throw new Error(
- `[vidstack]: The highest possible playback rate with Remotion is 4. You passed: ${playbackRate}`,
+ `[vidstack] The highest possible playback rate with Remotion is 4. You passed: ${playbackRate}`,
);
}
if (playbackRate < -4) {
throw new Error(
- `[vidstack]: The lowest possible playback rate with Remotion is -4. You passed: ${playbackRate}`,
+ `[vidstack] The lowest possible playback rate with Remotion is -4. You passed: ${playbackRate}`,
);
}
if (playbackRate === 0) {
- throw new Error(`[vidstack]: A playback rate of 0 is not supported.`);
+ throw new Error(`[vidstack] A playback rate of 0 is not supported.`);
}
}
@@ -208,13 +208,13 @@ export function validateComponent(src: RemotionMediaResource['src']) {
// @ts-expect-error
if (src.type === Composition) {
throw new TypeError(
- `[vidstack]: \`src\` should not be an instance of \`\`. Pass the React component directly, and set the duration, fps and dimensions as source props.`,
+ `[vidstack] \`src\` should not be an instance of \`\`. Pass the React component directly, and set the duration, fps and dimensions as source props.`,
);
}
if (src === Composition) {
throw new TypeError(
- `[vidstack]: \`src\` must not be the \`Composition\` component. Pass your own React component directly, and set the duration, fps and dimensions as source props.`,
+ `[vidstack] \`src\` must not be the \`Composition\` component. Pass your own React component directly, and set the duration, fps and dimensions as source props.`,
);
}
}
diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json
index fa092f4e3..0218f8da2 100644
--- a/packages/react/tsconfig.json
+++ b/packages/react/tsconfig.json
@@ -6,7 +6,7 @@
"vidstack": ["../vidstack/src"],
"@vidstack/react": ["./src/index.ts"]
},
- "types": ["@types/node", "../vidstack/dom.d.ts"]
+ "types": ["@types/node"]
},
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
diff --git a/packages/vidstack/.templates/sandbox/main.ts b/packages/vidstack/.templates/sandbox/main.ts
index b6904e032..2563606d8 100644
--- a/packages/vidstack/.templates/sandbox/main.ts
+++ b/packages/vidstack/.templates/sandbox/main.ts
@@ -8,6 +8,12 @@ import { isHLSProvider, type TextTrackInit } from '../src';
const player = document.querySelector('media-player')!;
+Object.defineProperty(window, 'player', {
+ get() {
+ return player;
+ },
+});
+
player.addEventListener('provider-change', (event) => {
const provider = event.detail;
// We can configure provider's here.
diff --git a/packages/vidstack/google-cast.d.ts b/packages/vidstack/google-cast.d.ts
new file mode 100644
index 000000000..9b2d03876
--- /dev/null
+++ b/packages/vidstack/google-cast.d.ts
@@ -0,0 +1,1422 @@
+interface Window {
+ cast?: typeof cast;
+ __onGCastApiAvailable?(available: boolean, reason?: string): void;
+}
+
+declare namespace chrome.cast {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.AutoJoinPolicy
+ */
+ export enum AutoJoinPolicy {
+ CUSTOM_CONTROLLER_SCOPED = 'custom_controller_scoped',
+ TAB_AND_ORIGIN_SCOPED = 'tab_and_origin_scoped',
+ ORIGIN_SCOPED = 'origin_scoped',
+ PAGE_SCOPED = 'page_scoped',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.DefaultActionPolicy
+ */
+ export enum DefaultActionPolicy {
+ CREATE_SESSION = 'create_session',
+ CAST_THIS_TAB = 'cast_this_tab',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.Capability
+ */
+ export enum Capability {
+ VIDEO_OUT = 'video_out',
+ AUDIO_OUT = 'audio_out',
+ VIDEO_IN = 'video_in',
+ AUDIO_IN = 'audio_in',
+ MULTIZONE_GROUP = 'multizone_group',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.ErrorCode
+ */
+ export enum ErrorCode {
+ CANCEL = 'cancel',
+ TIMEOUT = 'timeout',
+ API_NOT_INITIALIZED = 'api_not_initialized',
+ INVALID_PARAMETER = 'invalid_parameter',
+ EXTENSION_NOT_COMPATIBLE = 'extension_not_compatible',
+ EXTENSION_MISSING = 'extension_missing',
+ RECEIVER_UNAVAILABLE = 'receiver_unavailable',
+ SESSION_ERROR = 'session_error',
+ CHANNEL_ERROR = 'channel_error',
+ LOAD_MEDIA_FAILED = 'load_media_failed',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.ReceiverAvailability
+ */
+ export enum ReceiverAvailability {
+ AVAILABLE = 'available',
+ UNAVAILABLE = 'unavailable',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.SenderPlatform
+ */
+ export enum SenderPlatform {
+ CHROME = 'chrome',
+ IOS = 'ios',
+ ANDROID = 'android',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.ReceiverType
+ */
+ export enum ReceiverType {
+ CAST = 'cast',
+ DIAL = 'dial',
+ HANGOUT = 'hangout',
+ CUSTOM = 'custom',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.ReceiverAction
+ */
+ export enum ReceiverAction {
+ CAST = 'cast',
+ STOP = 'stop',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.SessionStatus
+ */
+ export enum SessionStatus {
+ CONNECTED = 'connected',
+ DISCONNECTED = 'disconnected',
+ STOPPED = 'stopped',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.VERSION
+ */
+ export var VERSION: number[];
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.isAvailable
+ */
+ export var isAvailable: boolean;
+
+ /**
+ * @param apiConfig
+ * @param successCallback
+ * @param errorCallback
+ */
+ export function initialize(
+ apiConfig: chrome.cast.ApiConfig,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param successCallback
+ * @param errorCallback
+ * @param opt_sessionRequest
+ * @param opt_label
+ */
+ export function requestSession(
+ successCallback: (session: chrome.cast.Session) => void,
+ errorCallback: (error: chrome.cast.Error) => void,
+ sessionRequest?: chrome.cast.SessionRequest,
+ label?: string,
+ ): void;
+
+ /**
+ * @param sessionId The id of the session to join.
+ */
+ export function requestSessionById(sessionId: string): void;
+
+ /**
+ * @param listener
+ */
+ export function addReceiverActionListener(
+ listener: (receiver: chrome.cast.Receiver, receiverAction: chrome.cast.ReceiverAction) => void,
+ ): void;
+
+ /**
+ * @param listener
+ */
+ export function removeReceiverActionListener(
+ listener: (receiver: chrome.cast.Receiver, receiverAction: chrome.cast.ReceiverAction) => void,
+ ): void;
+
+ /**
+ * @param message The message to log.
+ */
+ export function logMessage(message: string): void;
+
+ /**
+ * @param receivers
+ * @param successCallback
+ * @param errorCallback
+ */
+ export function setCustomReceivers(
+ receivers: chrome.cast.Receiver[],
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param receiver
+ * @param successCallback
+ * @param errorCallback
+ */
+ export function setReceiverDisplayStatus(
+ receiver: chrome.cast.Receiver,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param escaped A string to unescape.
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast#.unescape
+ */
+ export function unescape(escaped: string): string;
+
+ export class ApiConfig {
+ /**
+ * @param sessionRequest
+ * @param sessionListener
+ * @param receiverListener
+ * @param opt_autoJoinPolicy
+ * @param opt_defaultActionPolicy
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ApiConfig
+ */
+ constructor(
+ sessionRequest: chrome.cast.SessionRequest,
+ sessionListener: (session: chrome.cast.Session) => void,
+ receiverListener: (receiverAvailability: chrome.cast.ReceiverAvailability) => void,
+ autoJoinPolicy?: chrome.cast.AutoJoinPolicy,
+ defaultActionPolicy?: chrome.cast.DefaultActionPolicy,
+ );
+
+ sessionRequest: chrome.cast.SessionRequest;
+ sessionListener: (session: chrome.cast.Session) => void;
+ receiverListener: (receiverAvailability: chrome.cast.ReceiverAvailability) => void;
+ autoJoinPolicy: chrome.cast.AutoJoinPolicy;
+ defaultActionPolicy: chrome.cast.DefaultActionPolicy;
+ }
+
+ export class Error {
+ /**
+ * @param code
+ * @param opt_description
+ * @param opt_details
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Error
+ */
+ constructor(code: chrome.cast.ErrorCode, description?: string, details?: Object);
+
+ code: chrome.cast.ErrorCode;
+ description: string | null;
+ details: object;
+ }
+
+ export class Image {
+ /**
+ * @param url
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Image
+ */
+ constructor(url: string);
+
+ url: string;
+ height: number | null;
+ width: number | null;
+ }
+
+ export class SenderApplication {
+ /**
+ * @param platform
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SenderApplication
+ */
+ constructor(platform: chrome.cast.SenderPlatform);
+
+ platform: chrome.cast.SenderPlatform;
+ url: string | null;
+ packageId: string | null;
+ }
+
+ export class SessionRequest {
+ /**
+ * @param appId
+ * @param opt_capabilities
+ * @param opt_timeout
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.SessionRequest
+ */
+ constructor(appId: string, capabilities?: chrome.cast.Capability[], timeout?: number);
+
+ appId: string;
+ capabilities: chrome.cast.Capability[];
+ requestSessionTimeout: number;
+ language: string | null;
+ }
+
+ export class Session {
+ /**
+ * @param sessionId
+ * @param appId
+ * @param displayName
+ * @param appImages
+ * @param receiver
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Session
+ */
+ constructor(
+ sessionId: string,
+ appId: string,
+ displayName: string,
+ appImages: chrome.cast.Image[],
+ receiver: chrome.cast.Receiver,
+ );
+
+ sessionId: string;
+ appId: string;
+ displayName: string;
+ appImages: chrome.cast.Image[];
+ receiver: chrome.cast.Receiver;
+ senderApps: chrome.cast.SenderApplication[];
+ namespaces: Array<{ name: string }>;
+ media: chrome.cast.media.Media[];
+ status: chrome.cast.SessionStatus;
+ statusText: string | null;
+ transportId: string;
+
+ /**
+ * @param newLevel
+ * @param successCallback
+ * @param errorCallback
+ */
+ setReceiverVolumeLevel(
+ newLevel: number,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param muted
+ * @param successCallback
+ * @param errorCallback
+ */
+ setReceiverMuted(
+ muted: boolean,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param successCallback
+ * @param errorCallback
+ */
+ leave(successCallback: Function, errorCallback: (error: chrome.cast.Error) => void): void;
+
+ /**
+ * @param successCallback
+ * @param errorCallback
+ */
+ stop(successCallback: Function, errorCallback: (error: chrome.cast.Error) => void): void;
+
+ /**
+ * @param namespace
+ * @param message
+ * @param successCallback
+ * @param errorCallback
+ */
+ sendMessage(
+ namespace: string,
+ message: string | object,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param listener
+ */
+ addUpdateListener(listener: (isAlive: boolean) => void): void;
+
+ /**
+ * @param listener
+ */
+ removeUpdateListener(listener: (isAlive: boolean) => void): void;
+
+ /**
+ * @param namespace
+ * @param listener
+ */
+ addMessageListener(
+ namespace: string,
+ listener: (namespace: string, message: string) => void,
+ ): void;
+
+ /**
+ * @param namespace
+ * @param listener
+ */
+ removeMessageListener(
+ namespace: string,
+ listener: (namespace: string, message: string) => void,
+ ): void;
+
+ /**
+ * @param listener
+ */
+ addMediaListener(listener: (media: chrome.cast.media.Media) => void): void;
+
+ /**
+ * @param listener
+ */
+ removeMediaListener(listener: (media: chrome.cast.media.Media) => void): void;
+
+ /**
+ * @param loadRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ loadMedia(
+ loadRequest: chrome.cast.media.LoadRequest,
+ successCallback: (media: chrome.cast.media.Media) => void,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param queueLoadRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueLoad(
+ queueLoadRequest: chrome.cast.media.QueueLoadRequest,
+ successCallback: (media: chrome.cast.media.Media) => void,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+ }
+
+ export class Receiver {
+ /**
+ * @param label
+ * @param friendlyName
+ * @param opt_capabilities
+ * @param opt_volume
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Receiver
+ */
+ constructor(
+ label: string,
+ friendlyName: string,
+ capabilities?: chrome.cast.Capability[],
+ volume?: chrome.cast.Volume,
+ );
+
+ label: string;
+ friendlyName: string;
+ capabilities: chrome.cast.Capability[];
+ volume: chrome.cast.Volume;
+ receiverType: chrome.cast.ReceiverType;
+ displayStatus: chrome.cast.ReceiverDisplayStatus;
+ }
+
+ export class ReceiverDisplayStatus {
+ /**
+ * @param statusText
+ * @param appImages
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.ReceiverDisplayStatus
+ */
+ constructor(statusText: string, appImages: chrome.cast.Image[]);
+
+ statusText: string;
+ appImages: chrome.cast.Image[];
+ }
+
+ export class Volume {
+ /**
+ * @param opt_level
+ * @param opt_muted
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.Volume
+ */
+ constructor(level?: number, muted?: boolean);
+
+ level: number | null;
+ muted: boolean | null;
+ }
+}
+
+declare namespace chrome.cast.media {
+ export var DEFAULT_MEDIA_RECEIVER_APP_ID: string;
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.MediaCommand
+ */
+ export enum MediaCommand {
+ PAUSE = 'pause',
+ SEEK = 'seek',
+ STREAM_VOLUME = 'stream_volume',
+ STREAM_MUTE = 'stream_mute',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.MetadataType
+ */
+ export enum MetadataType {
+ GENERIC,
+ TV_SHOW,
+ MOVIE,
+ MUSIC_TRACK,
+ PHOTO,
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.PlayerState
+ */
+ export enum PlayerState {
+ IDLE = 'IDLE',
+ PLAYING = 'PLAYING',
+ PAUSED = 'PAUSED',
+ BUFFERING = 'BUFFERING',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.ResumeState
+ */
+ export enum ResumeState {
+ PLAYBACK_START = 'PLAYBACK_START',
+ PLAYBACK_PAUSE = 'PLAYBACK_PAUSE',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.StreamType
+ */
+ export enum StreamType {
+ BUFFERED = 'BUFFERED',
+ LIVE = 'LIVE',
+ OTHER = 'OTHER',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.IdleReason
+ */
+ export enum IdleReason {
+ CANCELLED = 'CANCELLED',
+ INTERRUPTED = 'INTERRUPTED',
+ FINISHED = 'FINISHED',
+ ERROR = 'ERROR',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.RepeatMode
+ */
+ export enum RepeatMode {
+ OFF = 'REPEAT_OFF',
+ ALL = 'REPEAT_ALL',
+ SINGLE = 'REPEAT_SINGLE',
+ ALL_AND_SHUFFLE = 'REPEAT_ALL_AND_SHUFFLE',
+ }
+
+ export class QueueItem {
+ /**
+ * @param mediaInfo
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueItem
+ */
+ constructor(mediaInfo: chrome.cast.media.MediaInfo);
+
+ activeTrackIds: Number[];
+ autoplay: boolean;
+ customData: Object;
+ itemId: number;
+ media: chrome.cast.media.MediaInfo;
+ preloadTime: number;
+ startTime: number;
+ }
+
+ export class QueueLoadRequest {
+ /**
+ * @param items
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueLoadRequest
+ */
+ constructor(items: chrome.cast.media.QueueItem[]);
+
+ customData: Object;
+ items: chrome.cast.media.QueueItem[];
+ repeatMode: chrome.cast.media.RepeatMode;
+ startIndex: number;
+ }
+
+ export class QueueInsertItemsRequest {
+ /**
+ * @param itemsToInsert
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueInsertItemsRequest
+ */
+ constructor(itemsToInsert: chrome.cast.media.QueueItem[]);
+
+ customData: Object;
+ insertBefore: number;
+ items: chrome.cast.media.QueueItem[];
+ }
+
+ export class QueueRemoveItemsRequest {
+ /**
+ * @param itemIdsToRemove
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueRemoveItemsRequest
+ */
+ constructor(itemIdsToRemove: number[]);
+
+ customData: Object;
+ itemIds: number[];
+ }
+
+ export class QueueReorderItemsRequest {
+ /**
+ * @param itemIdsToReorder
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueReorderItemsRequest
+ */
+ constructor(itemIdsToReorder: number[]);
+
+ customData: Object;
+ insertBefore: number;
+ itemIds: number[];
+ }
+
+ export class QueueUpdateItemsRequest {
+ /**
+ * @param itemsToUpdate
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.QueueUpdateItemsRequest
+ */
+ constructor(itemsToUpdate: chrome.cast.media.QueueItem[]);
+
+ customData: Object;
+ item: chrome.cast.media.QueueItem[];
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TrackType
+ */
+ export enum TrackType {
+ TEXT = 'TEXT',
+ AUDIO = 'AUDIO',
+ VIDEO = 'VIDEO',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TextTrackType
+ */
+ export enum TextTrackType {
+ SUBTITLES = 'SUBTITLES',
+ CAPTIONS = 'CAPTIONS',
+ DESCRIPTIONS = 'DESCRIPTIONS',
+ CHAPTERS = 'CHAPTERS',
+ METADATA = 'METADATA',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TextTrackEdgeType
+ */
+ export enum TextTrackEdgeType {
+ NONE = 'NONE',
+ OUTLINE = 'OUTLINE',
+ DROP_SHADOW = 'DROP_SHADOW',
+ RAISED = 'RAISED',
+ DEPRESSED = 'DEPRESSED',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TextTrackWindowType
+ */
+ export enum TextTrackWindowType {
+ NONE = 'NONE',
+ NORMAL = 'NORMAL',
+ ROUNDED_CORNERS = 'ROUNDED_CORNERS',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TextTrackFontGenericFamily
+ */
+ export enum TextTrackFontGenericFamily {
+ SANS_SERIF = 'SANS_SERIF',
+ MONOSPACED_SANS_SERIF = 'MONOSPACED_SANS_SERIF',
+ SERIF = 'SERIF',
+ MONOSPACED_SERIF = 'MONOSPACED_SERIF',
+ CASUAL = 'CASUAL',
+ CURSIVE = 'CURSIVE',
+ SMALL_CAPITALS = 'SMALL_CAPITALS',
+ }
+
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media#.TextTrackFontStyle
+ */
+ export enum TextTrackFontStyle {
+ NORMAL = 'NORMAL',
+ BOLD = 'BOLD',
+ BOLD_ITALIC = 'BOLD_ITALIC',
+ ITALIC = 'ITALIC',
+ }
+
+ export class GetStatusRequest {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.GetStatusRequest
+ */
+ constructor();
+
+ customData: Object;
+ }
+
+ export class PauseRequest {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.PauseRequest
+ */
+ constructor();
+
+ customData: Object;
+ }
+
+ export class PlayRequest {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.PlayRequest
+ */
+ constructor();
+
+ customData: Object;
+ }
+
+ export class SeekRequest {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.SeekRequest
+ */
+ constructor();
+
+ currentTime: number;
+ resumeState: chrome.cast.media.ResumeState;
+ customData: Object;
+ }
+
+ export class StopRequest {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.StopRequest
+ */
+ constructor();
+
+ customData: Object;
+ }
+
+ export class VolumeRequest {
+ /**
+ * @param volume
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.VolumeRequest
+ */
+ constructor(volume: chrome.cast.Volume);
+
+ volume: chrome.cast.Volume;
+ customData: Object;
+ }
+
+ export class LoadRequest {
+ /**
+ * @param mediaInfo
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.LoadRequest
+ */
+ constructor(mediaInfo: chrome.cast.media.MediaInfo);
+
+ activeTrackIds: number[];
+ autoplay: boolean;
+ currentTime: number;
+ customData: Object;
+ media: chrome.cast.media.MediaInfo;
+ playbackRate?: number | undefined;
+ }
+
+ export class EditTracksInfoRequest {
+ /**
+ * @param opt_activeTrackIds
+ * @param opt_textTrackStyle
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.EditTracksInfoRequest
+ */
+ constructor(activeTrackIds?: number[], textTrackStyle?: chrome.cast.media.TextTrackStyle);
+
+ activeTrackIds: number[];
+ textTrackStyle: chrome.cast.media.TextTrackStyle;
+ }
+
+ export class GenericMediaMetadata {
+ images: chrome.cast.Image[];
+ metadataType: chrome.cast.media.MetadataType;
+ releaseDate: string;
+ /** @deprecated Use releaseDate instead. */
+ releaseYear: number;
+ subtitle: string;
+ title: string;
+ /** @deprecated Use metadataType instead. */
+ type: chrome.cast.media.MetadataType;
+ }
+
+ export class MovieMediaMetadata {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.MovieMediaMetadata
+ */
+ constructor();
+
+ images: chrome.cast.Image[];
+ metadataType: chrome.cast.media.MetadataType;
+ releaseDate: string;
+ /** @deprecated Use releaseDate instead. */
+ releaseYear: number;
+ subtitle: string;
+ title: string;
+ studio: string;
+ /** @deprecated Use metadataType instead. */
+ type: chrome.cast.media.MetadataType;
+ }
+
+ export class TvShowMediaMetadata {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.TvShowMediaMetadata
+ */
+ constructor();
+
+ metadataType: chrome.cast.media.MetadataType;
+ seriesTitle: string;
+ title: string;
+ season: number;
+ episode: number;
+ images: chrome.cast.Image[];
+ originalAirdate: string;
+
+ /** @deprecated Use metadataType instead. */
+ type: chrome.cast.media.MetadataType;
+ /** @deprecated Use title instead. */
+ episodeTitle: string;
+ /** @deprecated Use season instead. */
+ seasonNumber: number;
+ /** @deprecated Use episode instead. */
+ episodeNumber: number;
+ /** @deprecated Use originalAirdate instead. */
+ releaseYear: number;
+ }
+
+ export class MusicTrackMediaMetadata {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.MusicTrackMediaMetadata
+ */
+ constructor();
+
+ metadataType: chrome.cast.media.MetadataType;
+ albumName: string;
+ title: string;
+ albumArtist: string;
+ artist: string;
+ composer: string;
+ songName: string;
+ trackNumber: number;
+ discNumber: number;
+ images: chrome.cast.Image[];
+ releaseDate: string;
+
+ /** @deprecated Use metadataType instead. */
+ type: chrome.cast.media.MetadataType;
+ /** @deprecated Use artist instead. */
+ artistName: string;
+ /** @deprecated Use releaseDate instead. */
+ releaseYear: number;
+ }
+
+ export class PhotoMediaMetadata {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.PhotoMediaMetadata
+ */
+ constructor();
+
+ metadataType: chrome.cast.media.MetadataType;
+ title: string;
+ artist: string;
+ location: string;
+ images: chrome.cast.Image[];
+ latitude: number;
+ longitude: number;
+ width: number;
+ height: number;
+ creationDateTime: string;
+
+ /** @deprecated Use metadataType instead. */
+ type: chrome.cast.media.MetadataType;
+ }
+
+ export class MediaInfo {
+ /**
+ * @param contentId
+ * @param contentType
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.MediaInfo
+ */
+ constructor(contentId: string, contentType: string);
+
+ contentId: string;
+ streamType: chrome.cast.media.StreamType;
+ contentType: string;
+ metadata: any;
+ duration: number;
+ tracks: chrome.cast.media.Track[];
+ textTrackStyle: chrome.cast.media.TextTrackStyle;
+ customData: Object;
+ }
+
+ export class Media {
+ /**
+ * @param sessionId
+ * @param mediaSessionId
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Media
+ */
+ constructor(sessionId: string, mediaSessionId: number);
+
+ activeTrackIds: number[];
+ currentItemId: number;
+ customData: Object;
+ idleReason: chrome.cast.media.IdleReason | null;
+ items: chrome.cast.media.QueueItem[];
+ liveSeekableRange?: chrome.cast.media.LiveSeekableRange | undefined;
+ loadingItemId: number;
+ media: chrome.cast.media.MediaInfo;
+ mediaSessionId: number;
+ playbackRate: number;
+ playerState: chrome.cast.media.PlayerState;
+ preloadedItemId: number;
+ repeatMode: chrome.cast.media.RepeatMode;
+ sessionId: string;
+ supportedMediaCommands: chrome.cast.media.MediaCommand[];
+ volume: chrome.cast.Volume;
+
+ /** @deprecated Use getEstimatedTime instead */
+ currentTime: number;
+
+ /**
+ * @param getStatusRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ getStatus(
+ getStatusRequest: chrome.cast.media.GetStatusRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param playRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ play(
+ playRequest: chrome.cast.media.PlayRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param pauseRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ pause(
+ pauseRequest: chrome.cast.media.PauseRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param seekRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ seek(
+ seekRequest: chrome.cast.media.SeekRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param stopRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ stop(
+ stopRequest: chrome.cast.media.StopRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param volumeRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ setVolume(
+ volumeRequest: chrome.cast.media.VolumeRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param editTracksInfoRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ editTracksInfo(
+ editTracksInfoRequest: chrome.cast.media.EditTracksInfoRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param command
+ * @return whether or not the receiver supports the given chrome.cast.media.MediaCommand
+ */
+ supportsCommand(command: chrome.cast.media.MediaCommand): boolean;
+
+ /**
+ * @param listener
+ */
+ addUpdateListener(listener: (isAlive: boolean) => void): void;
+
+ /**
+ * @param listener
+ */
+ removeUpdateListener(listener: (isAlive: boolean) => void): void;
+
+ /**
+ * @return
+ * @suppress {deprecated} Uses currentTime member to compute estimated time.
+ */
+ getEstimatedTime(): number;
+
+ /**
+ * @param item
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueAppendItem(
+ item: chrome.cast.media.QueueItem,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param queueInsertItemsRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueInsertItems(
+ queueInsertItemsRequest: chrome.cast.media.QueueInsertItemsRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param itemId
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueJumpToItem(
+ itemId: number,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param itemId
+ * @param newIndex
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueMoveItemToNewIndex(
+ itemId: number,
+ newIndex: number,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueNext(successCallback: Function, errorCallback: (error: chrome.cast.Error) => void): void;
+
+ /**
+ * @param successCallback
+ * @param errorCallback
+ */
+ queuePrev(successCallback: Function, errorCallback: (error: chrome.cast.Error) => void): void;
+
+ /**
+ * @param itemId
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueRemoveItem(
+ itemId: number,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param queueReorderItemsRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueReorderItems(
+ queueReorderItemsRequest: chrome.cast.media.QueueReorderItemsRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param repeatMode
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueSetRepeatMode(
+ repeatMode: chrome.cast.media.RepeatMode,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+
+ /**
+ * @param queueUpdateItemsRequest
+ * @param successCallback
+ * @param errorCallback
+ */
+ queueUpdateItems(
+ queueUpdateItemsRequest: chrome.cast.media.QueueUpdateItemsRequest,
+ successCallback: Function,
+ errorCallback: (error: chrome.cast.Error) => void,
+ ): void;
+ }
+
+ export class Track {
+ /**
+ * @param trackId
+ * @param trackType
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.Track
+ */
+ constructor(trackId: number, trackType: chrome.cast.media.TrackType);
+
+ trackId: number;
+ trackContentId: string;
+ trackContentType: string;
+ type: chrome.cast.media.TrackType;
+ name: string;
+ language: string;
+ subtype: chrome.cast.media.TextTrackType;
+ customData: Object;
+ }
+
+ export class TextTrackStyle {
+ /**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.TextTrackStyle
+ */
+ constructor();
+
+ foregroundColor: string;
+ backgroundColor: string;
+ edgeType: chrome.cast.media.TextTrackEdgeType;
+ edgeColor: string;
+ windowType: chrome.cast.media.TextTrackWindowType;
+ windowColor: string;
+ windowRoundedCornerRadius: number;
+ fontScale: number;
+ fontFamily: string;
+ fontGenericFamily: chrome.cast.media.TextTrackFontGenericFamily;
+ fontStyle: chrome.cast.media.TextTrackFontStyle;
+ customData: Object;
+ }
+
+ export class LiveSeekableRange {
+ /**
+ * @param start
+ * @param end
+ * @param isMovingWindow
+ * @param isLiveDone
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.LiveSeekableRange
+ */
+ constructor(start?: number, end?: number, isMovingWindow?: boolean, isLiveDone?: boolean);
+
+ start?: number | undefined;
+ end?: number | undefined;
+ isMovingWindow?: boolean | undefined;
+ isLiveDone?: boolean | undefined;
+ }
+}
+
+/**
+ * @see https://developers.google.com/cast/docs/reference/chrome/chrome.cast.media.timeout
+ */
+declare namespace chrome.cast.media.timeout {
+ export var load: number;
+ export var getStatus: number;
+ export var play: number;
+ export var pause: number;
+ export var seek: number;
+ export var stop: number;
+ export var setVolume: number;
+ export var editTracksInfo: number;
+ export var queueInsert: number;
+ export var queueLoad: number;
+ export var queueRemove: number;
+ export var queueReorder: number;
+ export var queueUpdate: number;
+}
+
+/**
+ * Cast Application Framework
+ *
+ * @see https://developers.google.com/cast/docs/reference/chrome/cast.framework
+ */
+declare namespace cast.framework {
+ export enum LoggerLevel {
+ DEBUG,
+ INFO,
+ WARNING,
+ ERROR,
+ NONE,
+ }
+
+ export enum CastState {
+ NO_DEVICES_AVAILABLE = 'NO_DEVICES_AVAILABLE',
+ NOT_CONNECTED = 'NOT_CONNECTED',
+ CONNECTING = 'CONNECTING',
+ CONNECTED = 'CONNECTED',
+ }
+
+ export enum SessionState {
+ NO_SESSION = 'NO_SESSION',
+ SESSION_STARTING = 'SESSION_STARTING',
+ SESSION_STARTED = 'SESSION_STARTED',
+ SESSION_START_FAILED = 'SESSION_START_FAILED',
+ SESSION_ENDING = 'SESSION_ENDING',
+ SESSION_ENDED = 'SESSION_ENDED',
+ SESSION_RESUMED = 'SESSION_RESUMED',
+ }
+
+ export enum CastContextEventType {
+ CAST_STATE_CHANGED = 'caststatechanged',
+ SESSION_STATE_CHANGED = 'sessionstatechanged',
+ }
+
+ export enum SessionEventType {
+ APPLICATION_STATUS_CHANGED = 'applicationstatuschanged',
+ APPLICATION_METADATA_CHANGED = 'applicationmetadatachanged',
+ ACTIVE_INPUT_STATE_CHANGED = 'activeinputstatechanged',
+ VOLUME_CHANGED = 'volumechanged',
+ MEDIA_SESSION = 'mediasession',
+ }
+
+ export enum RemotePlayerEventType {
+ ANY_CHANGE = 'anyChanged',
+ IS_CONNECTED_CHANGED = 'isConnectedChanged',
+ IS_MEDIA_LOADED_CHANGED = 'isMediaLoadedChanged',
+ DURATION_CHANGED = 'durationChanged',
+ CURRENT_TIME_CHANGED = 'currentTimeChanged',
+ IS_PAUSED_CHANGED = 'isPausedChanged',
+ VOLUME_LEVEL_CHANGED = 'volumeLevelChanged',
+ CAN_CONTROL_VOLUME_CHANGED = 'canControlVolumeChanged',
+ IS_MUTED_CHANGED = 'isMutedChanged',
+ CAN_PAUSE_CHANGED = 'canPauseChanged',
+ CAN_SEEK_CHANGED = 'canSeekChanged',
+ DISPLAY_NAME_CHANGED = 'displayNameChanged',
+ STATUS_TEXT_CHANGED = 'statusTextChanged',
+ TITLE_CHANGED = 'titleChanged',
+ DISPLAY_STATUS_CHANGED = 'displayStatusChanged',
+ MEDIA_INFO_CHANGED = 'mediaInfoChanged',
+ IMAGE_URL_CHANGED = 'imageUrlChanged',
+ PLAYER_STATE_CHANGED = 'playerStateChanged',
+ LIVE_SEEKABLE_RANGE_CHANGED = 'liveSeekableRange',
+ }
+
+ export enum ActiveInputState {
+ ACTIVE_INPUT_STATE_UNKNOWN = -1,
+ ACTIVE_INPUT_STATE_NO = 0,
+ ACTIVE_INPUT_STATE_YES = 1,
+ }
+
+ export interface CastOptions {
+ autoJoinPolicy: chrome.cast.AutoJoinPolicy;
+ language?: string | undefined;
+ receiverApplicationId?: string | undefined;
+ resumeSavedSession?: boolean | undefined;
+ /** The following flag enables Cast Connect(requires Chrome 87 or higher) */
+ androidReceiverCompatible?: boolean | undefined;
+ }
+
+ export const VERSION: string;
+ export function setLoggerLevel(level: LoggerLevel): void;
+
+ export class CastContext {
+ static getInstance(): CastContext;
+ setOptions(options: CastOptions): void;
+ getCastState(): CastState;
+ getSessionState(): SessionState;
+ requestSession(): Promise;
+ getCurrentSession(): CastSession | null;
+ endCurrentSession(stopCasting: boolean): void;
+ addEventListener(
+ type: T,
+ handler: (event: CastContextEvents[T]) => void,
+ ): void;
+ removeEventListener(
+ type: T,
+ handler: (event: CastContextEvents[T]) => void,
+ ): void;
+ }
+
+ export interface CastContextEvents {
+ [CastContextEventType.CAST_STATE_CHANGED]: CastStateEventData;
+ [CastContextEventType.SESSION_STATE_CHANGED]: SessionStateEventData;
+ }
+
+ export class CastSession {
+ constructor(sessionObj: chrome.cast.Session, state: SessionState);
+ getSessionObj(): chrome.cast.Session;
+ getSessionId(): string;
+ getSessionState(): SessionState;
+ getCastDevice(): chrome.cast.Receiver;
+ getApplicationMetadata(): ApplicationMetadata;
+ getApplicationStatus(): string;
+ getActiveInputState(): ActiveInputState;
+ endSession(stopCasting: boolean): void;
+ setVolume(volume: number): Promise;
+ getVolume(): number;
+ setMute(mute: boolean): Promise;
+ isMute(): boolean;
+ sendMessage(namespace: string, data: any): Promise;
+ addMessageListener(
+ namespace: string,
+ listener: (namespace: string, message: string) => void,
+ ): void;
+ removeMessageListener(
+ namespace: string,
+ listener: (namespace: string, message: string) => void,
+ ): void;
+ loadMedia(request: chrome.cast.media.LoadRequest): Promise;
+ getMediaSession(): chrome.cast.media.Media | null;
+ addEventListener(
+ type: T,
+ handler: (event: CastSessionEvents[T]) => void,
+ ): void;
+ removeEventListener(
+ type: T,
+ handler: (event: CastSessionEvents[T]) => void,
+ ): void;
+ }
+
+ export interface CastSessionEvents {
+ [SessionEventType.ACTIVE_INPUT_STATE_CHANGED]: ActiveInputStateEventData;
+ [SessionEventType.APPLICATION_METADATA_CHANGED]: ApplicationMetadataEventData;
+ [SessionEventType.APPLICATION_STATUS_CHANGED]: ApplicationStatusEventData;
+ [SessionEventType.MEDIA_SESSION]: MediaSessionEventData;
+ [SessionEventType.VOLUME_CHANGED]: VolumeEventData;
+ }
+
+ export class RemotePlayerController {
+ constructor(player: RemotePlayer);
+ playOrPause(): void;
+ stop(): void;
+ seek(): void;
+ muteOrUnmute(): void;
+ setVolumeLevel(): void;
+ getFormattedTime(timeInSec: number): string;
+ getSeekPosition(currentTime: number, duration: number): number;
+ getSeekTime(currentPosition: number, duration: number): number;
+ addEventListener(
+ type: RemotePlayerEventType,
+ handler: (event: RemotePlayerChangedEvent) => void,
+ ): void;
+ removeEventListener(
+ type: RemotePlayerEventType,
+ handler: (event: RemotePlayerChangedEvent) => void,
+ ): void;
+ }
+
+ export interface SavedPlayerState {
+ mediaInfo: chrome.cast.media.PlayerState | null;
+ currentTime: number;
+ isPaused: boolean;
+ }
+
+ export class RemotePlayer {
+ isConnected: boolean;
+ isMediaLoaded: boolean;
+ duration: number;
+ currentTime: number;
+ volumeLevel: number;
+ canControlVolume: boolean;
+ isPaused: boolean;
+ isMuted: boolean;
+ canPause: boolean;
+ canSeek: boolean;
+ displayName: string;
+ statusText: string;
+ title: string;
+ displayStatus: string;
+ liveSeekableRange?: chrome.cast.media.LiveSeekableRange | undefined;
+ mediaInfo?: chrome.cast.media.MediaInfo | undefined;
+ imageUrl: string | null;
+ playerState: chrome.cast.media.PlayerState | null;
+ savedPlayerState: SavedPlayerState | null;
+ controller: RemotePlayerController | null;
+ }
+
+ export class ApplicationMetadata {
+ constructor(sessionObj: chrome.cast.Session);
+ applicationId: string;
+ images: chrome.cast.Image[];
+ name: string;
+ namespaces: string[];
+ }
+
+ export abstract class EventData {
+ constructor(type: string);
+ type: string;
+ }
+
+ export class ActiveInputStateEventData extends EventData {
+ constructor(activeInputState: ActiveInputState);
+ activeInputState: ActiveInputState;
+ }
+
+ export class ApplicationMetadataEventData extends EventData {
+ constructor(metadata: ApplicationMetadata);
+ metadata: ApplicationMetadata;
+ }
+
+ export class ApplicationStatusEventData extends EventData {
+ constructor(status: string);
+ status: string;
+ }
+
+ export class CastStateEventData extends EventData {
+ constructor(castState: CastState);
+ castState: CastState;
+ }
+
+ export class MediaSessionEventData extends EventData {
+ constructor(mediaSession: chrome.cast.media.Media);
+ mediaSession: chrome.cast.media.Media;
+ }
+
+ export class RemotePlayerChangedEvent extends EventData {
+ constructor(type: RemotePlayerEventType, field: string, value: T);
+ field: string;
+ value: T;
+ }
+
+ export class SessionStateEventData extends EventData {
+ constructor(
+ session: CastSession,
+ sessionState: SessionState,
+ opt_errorCode: chrome.cast.ErrorCode,
+ );
+ errorCode: chrome.cast.ErrorCode;
+ session: CastSession;
+ sessionState: SessionState;
+ }
+
+ export class VolumeEventData extends EventData {
+ constructor(volume: number, isMute: boolean);
+ isMute: boolean;
+ volume: number;
+ }
+}
diff --git a/packages/vidstack/mangle.json b/packages/vidstack/mangle.json
index ede5b511b..4a49d757a 100644
--- a/packages/vidstack/mangle.json
+++ b/packages/vidstack/mangle.json
@@ -660,5 +660,56 @@
"_peek": "kl",
"_clear": "pl",
"_requestAirPlay": "ol",
- "_logError": "ql"
+ "_logError": "ql",
+ "_canPrompt": "sl",
+ "_connect": "vl",
+ "_connectDisposal": "rl",
+ "_getRemotePlaybackState": "xl",
+ "_onCastStateChanged": "tl",
+ "_onSessionResumed": "yl",
+ "_onSessionStateChanged": "wl",
+ "_requestGoogleCast": "ul",
+ "_addMediaMetadata": "Hl",
+ "_addMediaStreamType": "Fl",
+ "_addMediaTracks": "Gl",
+ "_buildCastTrack": "Il",
+ "_buildMediaInfo": "Dl",
+ "_getActiveCastTrackIds": "El",
+ "_getDefaultLabel": "zl",
+ "_getValidTracks": "Bl",
+ "_onGoogleCastSupportChange": "Ll",
+ "_onMediaLoadedChange": "Jl",
+ "_syncWithCastState": "Kl",
+ "_throwIfPlayerNotReady": "Al",
+ "_watchLoad": "Cl",
+ "_connectToReceiver": "Ol",
+ "_disconnectFromReceiver": "Ml",
+ "_loadCastFramework": "Pl",
+ "_mediaInfoBuilder": "Nl",
+ "_setCastOptions": "Ql",
+ "_showPrompt": "Rl",
+ "_throwNotAvailable": "Sl",
+ "_setOptions": "Tl",
+ "_attachCastEvents": "_l",
+ "_attachPlayerEvents": "$l",
+ "_buildLoadRequest": "am",
+ "_createEvent": "Vl",
+ "_getActiveRemoteTrackIds": "bm",
+ "_getRemoteTracks": "Wl",
+ "_info": "Ul",
+ "_setMetadata": "Zl",
+ "_setStreamType": "Xl",
+ "_setTracks": "Yl",
+ "_attachCastPlayerEvents": "hm",
+ "_getPlayedRange": "cm",
+ "_getSeekableRange": "gm",
+ "_getStreamType": "mm",
+ "_onCanControlVolumeChange": "em",
+ "_onCanSeekChange": "fm",
+ "_onCurrentTimeChange": "im",
+ "_onPausedChange": "jm",
+ "_onPlayerStateChange": "km",
+ "_onRemotePlayerEvent": "lm",
+ "_playerEventHandlers": "dm",
+ "_watchCanSetVolume": "nm"
}
diff --git a/packages/vidstack/package.json b/packages/vidstack/package.json
index 140c789ac..7bfc8a59d 100644
--- a/packages/vidstack/package.json
+++ b/packages/vidstack/package.json
@@ -153,6 +153,8 @@
"types": "./solid.d.ts",
"default": "./solid.d.ts"
},
+ "./dom.d.ts": "./dom.d.ts",
+ "./google-cast.d.ts": "./google-cast.d.ts",
"./vscode.html-data.json": "./vscode.html-data.json"
},
"publishConfig": {
diff --git a/packages/vidstack/player/index.d.ts b/packages/vidstack/player/index.d.ts
index 395930b49..7bfe56848 100644
--- a/packages/vidstack/player/index.d.ts
+++ b/packages/vidstack/player/index.d.ts
@@ -1,4 +1,3 @@
-///
///
export {};
diff --git a/packages/vidstack/player/layouts.d.ts b/packages/vidstack/player/layouts.d.ts
index 395930b49..7bfe56848 100644
--- a/packages/vidstack/player/layouts.d.ts
+++ b/packages/vidstack/player/layouts.d.ts
@@ -1,4 +1,3 @@
-///
///
export {};
diff --git a/packages/vidstack/player/ui.d.ts b/packages/vidstack/player/ui.d.ts
index 395930b49..7bfe56848 100644
--- a/packages/vidstack/player/ui.d.ts
+++ b/packages/vidstack/player/ui.d.ts
@@ -1,4 +1,3 @@
-///
///
export {};
diff --git a/packages/vidstack/rollup.config.js b/packages/vidstack/rollup.config.js
index bcf877a62..97838508b 100644
--- a/packages/vidstack/rollup.config.js
+++ b/packages/vidstack/rollup.config.js
@@ -20,7 +20,7 @@ const NPM_EXTERNAL_PACKAGES = ['hls.js', 'media-captions', 'media-icons'],
CDN_EXTERNAL_PACKAGES = ['media-captions', 'media-icons'],
NPM_BUNDLES = [define({ type: 'server' }), define({ type: 'dev' }), define({ type: 'prod' })],
CDN_BUNDLES = [defineCDN({ dev: true }), defineCDN(), defineCDN({ layouts: true })],
- TYPES_BUNDLES = [defineTypes()];
+ TYPES_BUNDLES = defineTypes();
// Styles
if (!MODE_TYPES) {
@@ -46,34 +46,51 @@ export default defineConfig(
);
/**
- * @returns {import('rollup').RollupOptions}
+ * @returns {import('rollup').RollupOptions[]}
* */
function defineTypes() {
/** @type {Record} */
- let input = {
- ['index']: 'src/index.ts',
- elements: 'src/elements/index.ts',
- icons: 'src/elements/bundles/icons.ts',
+ const input = {
+ index: 'types/index.d.ts',
+ elements: 'types/elements/index.d.ts',
+ icons: 'types/elements/bundles/icons.d.ts',
};
- for (const key of Object.keys(input)) {
- input[key] = input[key].replace(/^src/, 'types').replace(/\.ts$/, '.d.ts');
- }
-
- return {
- input,
- output: {
- dir: '.',
- chunkFileNames: 'dist/types/vidstack-[hash].d.ts',
- manualChunks(id) {
- if (id.includes('maverick') || id.includes('lit-html') || id.includes('@floating-ui')) {
- return 'framework.d';
- }
+ return [
+ {
+ input,
+ output: {
+ dir: '.',
+ chunkFileNames: 'dist/types/vidstack-[hash].d.ts',
+ manualChunks(id) {
+ if (id.includes('maverick') || id.includes('lit-html') || id.includes('@floating-ui')) {
+ return 'framework.d';
+ }
+ },
},
+ external: NPM_EXTERNAL_PACKAGES,
+ plugins: [
+ dts({
+ respectExternal: true,
+ }),
+ {
+ name: 'globals',
+ generateBundle(_, bundle) {
+ const files = new Set(['index.d.ts', 'elements.d.ts']),
+ references = ['dom.d.ts', 'google-cast.d.ts']
+ .map((path) => `/// `)
+ .join('\n');
+
+ for (const file of Object.values(bundle)) {
+ if (files.has(file.fileName) && file.type === 'chunk' && file.isEntry) {
+ file.code = references + `\n\n${file.code}`;
+ }
+ }
+ },
+ },
+ ],
},
- external: NPM_EXTERNAL_PACKAGES,
- plugins: [dts({ respectExternal: true })],
- };
+ ];
}
/**
@@ -105,12 +122,7 @@ function define({ target, type, minify }) {
if (!isServer) {
input = {
...input,
- [`providers/vidstack-html`]: 'src/providers/html/provider.ts',
- [`providers/vidstack-audio`]: 'src/providers/audio/provider.ts',
- [`providers/vidstack-video`]: 'src/providers/video/provider.ts',
- [`providers/vidstack-hls`]: 'src/providers/hls/provider.ts',
- [`providers/vidstack-youtube`]: 'src/providers/youtube/provider.ts',
- [`providers/vidstack-vimeo`]: 'src/providers/vimeo/provider.ts',
+ ...getProviderInputs(),
};
input['define/vidstack-audio-layout'] =
@@ -191,12 +203,7 @@ function defineCDN({ dev = false, layouts = false } = {}) {
}),
input: {
[output]: input,
- [`providers/vidstack-html`]: 'src/providers/html/provider.ts',
- [`providers/vidstack-audio`]: 'src/providers/audio/provider.ts',
- [`providers/vidstack-video`]: 'src/providers/video/provider.ts',
- [`providers/vidstack-hls`]: 'src/providers/hls/provider.ts',
- [`providers/vidstack-youtube`]: 'src/providers/youtube/provider.ts',
- [`providers/vidstack-vimeo`]: 'src/providers/vimeo/provider.ts',
+ ...getProviderInputs(),
},
output: {
format: 'esm',
@@ -264,3 +271,15 @@ export async function buildMangleCache() {
return mangleCache;
}
+
+function getProviderInputs() {
+ return {
+ [`providers/vidstack-html`]: 'src/providers/html/provider.ts',
+ [`providers/vidstack-audio`]: 'src/providers/audio/provider.ts',
+ [`providers/vidstack-video`]: 'src/providers/video/provider.ts',
+ [`providers/vidstack-hls`]: 'src/providers/hls/provider.ts',
+ [`providers/vidstack-youtube`]: 'src/providers/youtube/provider.ts',
+ [`providers/vidstack-vimeo`]: 'src/providers/vimeo/provider.ts',
+ [`providers/vidstack-google-cast`]: 'src/providers/google-cast/provider.ts',
+ };
+}
diff --git a/packages/vidstack/src/components/index.ts b/packages/vidstack/src/components/index.ts
index 7db5740a5..e5f8fb3c5 100644
--- a/packages/vidstack/src/components/index.ts
+++ b/packages/vidstack/src/components/index.ts
@@ -11,6 +11,7 @@ export * from './ui/tooltip/tooltip-trigger';
export * from './ui/tooltip/tooltip-content';
export * from './ui/buttons/toggle-button';
export * from './ui/buttons/airplay-button';
+export * from './ui/buttons/google-cast-button';
export * from './ui/buttons/play-button';
export * from './ui/buttons/caption-button';
export * from './ui/buttons/fullscreen-button';
diff --git a/packages/vidstack/src/components/layouts/default-layout.ts b/packages/vidstack/src/components/layouts/default-layout.ts
index 94cd38ae7..83abb6bf6 100644
--- a/packages/vidstack/src/components/layouts/default-layout.ts
+++ b/packages/vidstack/src/components/layouts/default-layout.ts
@@ -203,6 +203,7 @@ export interface DefaultLayoutTranslations {
Connecting: string;
Disconnected: string;
Default: string;
+ 'Google Cast': string;
LIVE: string;
Mute: string;
Normal: string;
diff --git a/packages/vidstack/src/components/player.ts b/packages/vidstack/src/components/player.ts
index 846aee612..b8e5e958a 100644
--- a/packages/vidstack/src/components/player.ts
+++ b/packages/vidstack/src/components/player.ts
@@ -63,7 +63,7 @@ import { RequestQueue } from '../foundation/queue/request-queue';
import type { AnyMediaProvider, MediaProviderAdapter } from '../providers';
import { setAttributeIfEmpty } from '../utils/dom';
import { clampNumber } from '../utils/number';
-import { canChangeVolume, IS_IPHONE } from '../utils/support';
+import { IS_IPHONE } from '../utils/support';
declare global {
interface HTMLElementEventMap {
@@ -148,7 +148,6 @@ export class MediaPlayer
const context = {
player: this,
- scope: getScope(),
qualities: new VideoQualityList(),
audioTracks: new AudioTrackList(),
storage,
@@ -224,8 +223,6 @@ export class MediaPlayer
protected override onConnect(el: HTMLElement) {
if (IS_IPHONE) setAttribute(el, 'data-iphone', '');
- canChangeVolume().then(this.$state.canSetVolume.set);
-
const pointerQuery = window.matchMedia('(pointer: coarse)');
this._onPointerChange(pointerQuery);
pointerQuery.onchange = this._onPointerChange.bind(this);
@@ -706,6 +703,15 @@ export class MediaPlayer
return this._requestMgr._requestAirPlay(trigger);
}
+ /**
+ * Request Google Cast device picker to open. The Google Cast framework will be loaded if it
+ * hasn't yet.
+ */
+ @method
+ requestGoogleCast(trigger?: Event) {
+ return this._requestMgr._requestGoogleCast(trigger);
+ }
+
/**
* Returns a new `PlayerQueryList` object that can then be used to determine if the
* player and document matches the query string, as well as to monitor any changes to detect
diff --git a/packages/vidstack/src/components/provider/source-select.ts b/packages/vidstack/src/components/provider/source-select.ts
index 50d1d59d1..e7d0bc04d 100644
--- a/packages/vidstack/src/components/provider/source-select.ts
+++ b/packages/vidstack/src/components/provider/source-select.ts
@@ -49,9 +49,13 @@ export class SourceSelection {
EMBED_LOADERS = [YOUTUBE_LOADER, VIMEO_LOADER];
this._loaders = computed(() => {
- return _media.$props.preferNativeHLS()
+ const remoteLoader = _media.$state.remotePlaybackLoader();
+
+ const loaders = _media.$props.preferNativeHLS()
? [VIDEO_LOADER, AUDIO_LOADER, HLS_LOADER, ...EMBED_LOADERS, ...customLoaders]
: [HLS_LOADER, VIDEO_LOADER, AUDIO_LOADER, ...EMBED_LOADERS, ...customLoaders];
+
+ return remoteLoader ? [remoteLoader, ...loaders] : loaders;
});
const { $state } = _media;
@@ -149,10 +153,11 @@ export class SourceSelection {
protected _findNewSource(currentSource: MediaSrc, sources: MediaSrc[]) {
let newSource: MediaSrc = { src: '', type: '' },
- newLoader: MediaProviderLoader | null = null;
+ newLoader: MediaProviderLoader | null = null,
+ loaders = this._loaders();
for (const src of sources) {
- const loader = peek(this._loaders).find((loader) => loader.canPlay(src));
+ const loader = loaders.find((loader) => loader.canPlay(src));
if (loader) {
newSource = src;
newLoader = loader;
@@ -185,15 +190,16 @@ export class SourceSelection {
private _onSetup() {
const provider = this._media.$provider();
+
if (!provider || peek(this._media.$providerSetup)) return;
if (this._media.$state.canLoad()) {
- scoped(() => provider.setup(this._media), provider.scope);
+ scoped(() => provider.setup(), provider.scope);
this._media.$providerSetup.set(true);
return;
}
- peek(() => provider.preconnect?.(this._media));
+ peek(() => provider.preconnect?.());
}
private _onLoadSource() {
@@ -226,13 +232,26 @@ export class SourceSelection {
this._notify('stream-type-change', 'on-demand');
}
- peek(() => provider?.loadSource(source, peek(this._media.$state.preload)));
+ peek(() => {
+ const preload = peek(this._media.$state.preload);
+ return provider?.loadSource(source, preload).catch((error) => {
+ if (__DEV__) {
+ this._media.logger
+ ?.errorGroup('[vidstack] failed to load source')
+ .labelledLog('Error', error)
+ .labelledLog('Source', source)
+ .labelledLog('Provider', provider)
+ .labelledLog('Media Context', { ...this._media })
+ .dispatch();
+ }
+ });
+ });
return () => abort.abort();
}
try {
- isString(source.src) && preconnect(new URL(source.src).origin, 'preconnect');
+ isString(source.src) && preconnect(new URL(source.src).origin);
} catch (error) {
if (__DEV__) {
this._media.logger
diff --git a/packages/vidstack/src/components/ui/buttons/airplay-button.ts b/packages/vidstack/src/components/ui/buttons/airplay-button.ts
index 31716d74f..e921dd1b9 100644
--- a/packages/vidstack/src/components/ui/buttons/airplay-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/airplay-button.ts
@@ -46,7 +46,7 @@ export class AirPlayButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-tooltip', 'airplay');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -64,7 +64,7 @@ export class AirPlayButton extends Component {
return remotePlaybackType() === 'airplay' && remotePlaybackState();
}
- private _getLabel() {
+ private _getDefaultLabel() {
const { remotePlaybackState } = this._media.$state;
return `AirPlay ${remotePlaybackState()}`;
}
diff --git a/packages/vidstack/src/components/ui/buttons/caption-button.ts b/packages/vidstack/src/components/ui/buttons/caption-button.ts
index b173e7c5a..abb534aa5 100644
--- a/packages/vidstack/src/components/ui/buttons/caption-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/caption-button.ts
@@ -44,7 +44,7 @@ export class CaptionButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-tooltip', 'caption');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -62,7 +62,7 @@ export class CaptionButton extends Component {
return !hasCaptions();
}
- private _getLabel() {
+ private _getDefaultLabel() {
const { textTrack } = this._media.$state;
return textTrack() ? 'Closed-Captions Off' : 'Closed-Captions On';
}
diff --git a/packages/vidstack/src/components/ui/buttons/fullscreen-button.ts b/packages/vidstack/src/components/ui/buttons/fullscreen-button.ts
index 4f72b0ab9..a945a5274 100644
--- a/packages/vidstack/src/components/ui/buttons/fullscreen-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/fullscreen-button.ts
@@ -58,7 +58,7 @@ export class FullscreenButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-tooltip', 'fullscreen');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -79,7 +79,7 @@ export class FullscreenButton extends Component {
return canFullscreen();
}
- private _getLabel() {
+ private _getDefaultLabel() {
const { fullscreen } = this._media.$state;
return fullscreen() ? 'Exit Fullscreen' : 'Enter Fullscreen';
}
diff --git a/packages/vidstack/src/components/ui/buttons/google-cast-button.ts b/packages/vidstack/src/components/ui/buttons/google-cast-button.ts
new file mode 100644
index 000000000..69c59dd36
--- /dev/null
+++ b/packages/vidstack/src/components/ui/buttons/google-cast-button.ts
@@ -0,0 +1,71 @@
+import { Component } from 'maverick.js';
+
+import { useMediaContext, type MediaContext } from '../../../core/api/media-context';
+import { $ariaBool } from '../../../utils/aria';
+import { setARIALabel } from '../../../utils/dom';
+import {
+ ToggleButtonController,
+ type ToggleButtonControllerProps,
+} from './toggle-button-controller';
+
+export interface GoogleCastButtonProps extends ToggleButtonControllerProps {}
+
+/**
+ * A button for requesting Google Cast.
+ *
+ * @attr data-active - Whether Google Cast is connected.
+ * @attr data-supported - Whether Google Cast is available.
+ * @attr data-state - Current connection state.
+ * @see {@link https://developers.google.com/cast/docs/overview}
+ * @docs {@link https://www.vidstack.io/docs/player/components/buttons/google-cast-button}
+ */
+export class GoogleCastButton extends Component {
+ static props: GoogleCastButtonProps = ToggleButtonController.props;
+
+ private _media!: MediaContext;
+
+ constructor() {
+ super();
+ new ToggleButtonController({
+ _isPressed: this._isPressed.bind(this),
+ _onPress: this._onPress.bind(this),
+ });
+ }
+
+ protected override onSetup(): void {
+ this._media = useMediaContext();
+
+ const { canGoogleCast, isGoogleCastConnected } = this._media.$state;
+ this.setAttributes({
+ 'data-active': isGoogleCastConnected,
+ 'data-supported': canGoogleCast,
+ 'data-state': this._getState.bind(this),
+ 'aria-hidden': $ariaBool(() => !canGoogleCast()),
+ });
+ }
+
+ protected override onAttach(el: HTMLElement): void {
+ el.setAttribute('data-media-tooltip', 'google-cast');
+ setARIALabel(el, this._getDefaultLabel.bind(this));
+ }
+
+ private _onPress(event: Event) {
+ const remote = this._media.remote;
+ remote.requestGoogleCast(event);
+ }
+
+ private _isPressed() {
+ const { remotePlaybackType, remotePlaybackState } = this._media.$state;
+ return remotePlaybackType() === 'google-cast' && remotePlaybackState() !== 'disconnected';
+ }
+
+ private _getState() {
+ const { remotePlaybackType, remotePlaybackState } = this._media.$state;
+ return remotePlaybackType() === 'google-cast' && remotePlaybackState();
+ }
+
+ private _getDefaultLabel() {
+ const { remotePlaybackState } = this._media.$state;
+ return `Google Cast ${remotePlaybackState()}`;
+ }
+}
diff --git a/packages/vidstack/src/components/ui/buttons/mute-button.ts b/packages/vidstack/src/components/ui/buttons/mute-button.ts
index d3235d992..aecd93b66 100644
--- a/packages/vidstack/src/components/ui/buttons/mute-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/mute-button.ts
@@ -42,7 +42,7 @@ export class MuteButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-mute-button', '');
el.setAttribute('data-media-tooltip', 'mute');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -55,7 +55,7 @@ export class MuteButton extends Component {
return muted() || volume() === 0;
}
- private _getLabel() {
+ private _getDefaultLabel() {
return this._isPressed() ? 'Unmute' : 'Mute';
}
diff --git a/packages/vidstack/src/components/ui/buttons/pip-button.ts b/packages/vidstack/src/components/ui/buttons/pip-button.ts
index da7f4fc38..ab6337f16 100644
--- a/packages/vidstack/src/components/ui/buttons/pip-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/pip-button.ts
@@ -47,7 +47,7 @@ export class PIPButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-tooltip', 'pip');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -65,7 +65,7 @@ export class PIPButton extends Component {
return canPictureInPicture();
}
- private _getLabel() {
+ private _getDefaultLabel() {
const { pictureInPicture } = this._media.$state;
return pictureInPicture() ? 'Exit Picture In Picture' : 'Enter Picture In Picture';
}
diff --git a/packages/vidstack/src/components/ui/buttons/play-button.ts b/packages/vidstack/src/components/ui/buttons/play-button.ts
index 9b5d61969..73feabe11 100644
--- a/packages/vidstack/src/components/ui/buttons/play-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/play-button.ts
@@ -43,7 +43,7 @@ export class PlayButton extends Component {
protected override onAttach(el: HTMLElement): void {
el.setAttribute('data-media-tooltip', 'play');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
private _onPress(event: Event) {
@@ -56,7 +56,7 @@ export class PlayButton extends Component {
return !paused();
}
- private _getLabel() {
+ private _getDefaultLabel() {
const { paused } = this._media.$state;
return paused() ? 'Play' : 'Pause';
}
diff --git a/packages/vidstack/src/components/ui/buttons/seek-button.ts b/packages/vidstack/src/components/ui/buttons/seek-button.ts
index e5b188621..1a03a44e7 100644
--- a/packages/vidstack/src/components/ui/buttons/seek-button.ts
+++ b/packages/vidstack/src/components/ui/buttons/seek-button.ts
@@ -59,7 +59,7 @@ export class SeekButton extends Component {
setAttributeIfEmpty(el, 'role', 'button');
setAttributeIfEmpty(el, 'type', 'button');
el.setAttribute('data-media-tooltip', 'seek');
- setARIALabel(el, this._getLabel.bind(this));
+ setARIALabel(el, this._getDefaultLabel.bind(this));
}
protected override onConnect(el: HTMLElement) {
@@ -71,7 +71,7 @@ export class SeekButton extends Component {
return canSeek();
}
- protected _getLabel() {
+ protected _getDefaultLabel() {
const { seconds } = this.$props;
return `Seek ${seconds() > 0 ? 'forward' : 'backward'} ${seconds()} seconds`;
}
diff --git a/packages/vidstack/src/components/ui/thumbnails/thumbnail-loader.ts b/packages/vidstack/src/components/ui/thumbnails/thumbnail-loader.ts
index cc85c901a..85a9b4793 100644
--- a/packages/vidstack/src/components/ui/thumbnails/thumbnail-loader.ts
+++ b/packages/vidstack/src/components/ui/thumbnails/thumbnail-loader.ts
@@ -265,7 +265,7 @@ export class ThumbnailsLoader {
if (!__DEV__ || warned?.has(src)) return;
this._media.logger
- ?.errorGroup('Failed to load thumbnails.')
+ ?.errorGroup('[vidstack] failed to load thumbnails')
.labelledLog('Src', src)
.labelledLog('Error', error)
.dispatch();
diff --git a/packages/vidstack/src/core/api/media-context.ts b/packages/vidstack/src/core/api/media-context.ts
index 207b02279..439222ad8 100644
--- a/packages/vidstack/src/core/api/media-context.ts
+++ b/packages/vidstack/src/core/api/media-context.ts
@@ -3,7 +3,6 @@ import {
useContext,
type ReadSignal,
type ReadSignalRecord,
- type Scope,
type WriteSignal,
} from 'maverick.js';
@@ -23,7 +22,6 @@ import type { PlayerStore } from './player-state';
export interface MediaContext {
player: MediaPlayer;
- scope: Scope;
storage: MediaStorage;
remote: MediaRemoteControl;
delegate: MediaPlayerDelegate;
diff --git a/packages/vidstack/src/core/api/player-events.ts b/packages/vidstack/src/core/api/player-events.ts
index eed6ad6a0..fd8e8a139 100644
--- a/packages/vidstack/src/core/api/player-events.ts
+++ b/packages/vidstack/src/core/api/player-events.ts
@@ -2,6 +2,7 @@ import type { DOMEvent } from 'maverick.js/std';
import type { MediaPlayer } from '../../components';
import type { LoggerEvents } from '../../foundation/logger/events';
+import type { GoogleCastEvents } from '../../providers/google-cast/events';
import type { HLSProviderEvents } from '../../providers/hls/events';
import type { VideoPresentationEvents } from '../../providers/video/presentation/events';
import type { MediaEvents } from './media-events';
@@ -13,7 +14,8 @@ export interface MediaPlayerEvents
MediaUserEvents,
LoggerEvents,
VideoPresentationEvents,
- HLSProviderEvents {
+ HLSProviderEvents,
+ GoogleCastEvents {
'media-player-connect': MediaPlayerConnectEvent;
/* @internal */
'find-media-player': FindMediaPlayerEvent;
diff --git a/packages/vidstack/src/core/api/player-props.ts b/packages/vidstack/src/core/api/player-props.ts
index 8d943b383..81b75598c 100644
--- a/packages/vidstack/src/core/api/player-props.ts
+++ b/packages/vidstack/src/core/api/player-props.ts
@@ -1,5 +1,6 @@
import type { LogLevel } from '../../foundation/logger/log-level';
import type { ScreenOrientationLockType } from '../../foundation/orientation/types';
+import type { GoogleCastOptions } from '../../providers/google-cast/types';
import { MEDIA_KEY_SHORTCUTS } from '../keyboard/controller';
import type { MediaKeyShortcuts, MediaKeyTarget } from '../keyboard/types';
import type { MediaState } from './player-state';
@@ -14,6 +15,7 @@ export const mediaPlayerProps: MediaPlayerProps = {
crossorigin: null,
crossOrigin: null,
fullscreenOrientation: 'landscape',
+ googleCast: {},
load: 'visible',
posterLoad: 'visible',
logLevel: __DEV__ ? 'warn' : 'silent',
@@ -134,6 +136,12 @@ export interface MediaPlayerProps
* the Screen Orientation API is available.
*/
fullscreenOrientation: ScreenOrientationLockType | undefined;
+ /**
+ * Google Cast options.
+ *
+ * @see {@link https://developers.google.com/cast/docs/reference/web_sender/cast.framework.CastOptions}
+ */
+ googleCast: GoogleCastOptions;
/**
* Whether native HLS support is preferred over using `hls.js`. We recommend setting this to
* `false` to ensure a consistent and configurable experience across browsers. In addition, our
diff --git a/packages/vidstack/src/core/api/player-state.ts b/packages/vidstack/src/core/api/player-state.ts
index 9f2b4eb6c..e30d11d2a 100644
--- a/packages/vidstack/src/core/api/player-state.ts
+++ b/packages/vidstack/src/core/api/player-state.ts
@@ -1,6 +1,7 @@
import { State, tick, type Store } from 'maverick.js';
import type { LogLevel } from '../../foundation/logger/log-level';
+import type { MediaProviderLoader } from '../../providers/types';
import { canOrientScreen } from '../../utils/support';
import type { VideoQuality } from '../quality/video-quality';
import { getTimeRangesEnd, getTimeRangesStart, TimeRange } from '../time-ranges';
@@ -13,6 +14,7 @@ import type {
MediaStreamType,
MediaType,
MediaViewType,
+ RemotePlaybackInfo,
RemotePlaybackType,
} from './types';
@@ -124,6 +126,8 @@ export const mediaState = new State({
canGoogleCast: false,
remotePlaybackState: 'disconnected',
remotePlaybackType: 'none',
+ remotePlaybackLoader: null,
+ remotePlaybackInfo: null,
get isAirPlayConnected() {
return this.remotePlaybackType === 'airplay' && this.remotePlaybackState === 'connected';
},
@@ -174,7 +178,7 @@ export const mediaState = new State({
},
// ~~ internal props ~~
- autoplaying: false,
+ autoPlaying: false,
providedTitle: '',
inferredTitle: '',
providedPoster: '',
@@ -186,54 +190,40 @@ export const mediaState = new State({
liveSyncPosition: null,
});
-const DO_NOT_RESET_ON_SRC_CHANGE = new Set([
- 'autoplay',
- 'canFullscreen',
- 'canLoad',
- 'canLoadPoster',
- 'canPictureInPicture',
- 'canAirPlay',
- 'canGoogleCast',
- 'remotePlaybackState',
- 'remotePlaybackType',
- 'canSetVolume',
- 'clipEndTime',
- 'clipStartTime',
- 'controls',
- 'crossorigin',
- 'crossOrigin',
- 'fullscreen',
- 'height',
- 'inferredViewType',
- 'lastKeyboardAction',
- 'logLevel',
- 'loop',
- 'mediaHeight',
- 'mediaType',
- 'mediaWidth',
- 'muted',
- 'orientation',
- 'pictureInPicture',
- 'playsinline',
- 'pointer',
- 'preload',
- 'providedPoster',
- 'providedStreamType',
- 'providedTitle',
- 'providedViewType',
- 'source',
- 'sources',
- 'textTrack',
- 'textTracks',
- 'volume',
- 'width',
+const RESET_ON_SRC_CHANGE = new Set([
+ 'audioTrack',
+ 'audioTracks',
+ 'autoplayError',
+ 'autoPlaying',
+ 'autoQuality',
+ 'buffered',
+ 'canPlay',
+ 'ended',
+ 'error',
+ 'inferredPoster',
+ 'inferredStreamType',
+ 'inferredTitle',
+ 'intrinsicDuration',
+ 'liveSyncPosition',
+ 'paused',
+ 'playbackRate',
+ 'played',
+ 'playing',
+ 'qualities',
+ 'quality',
+ 'realCurrentTime',
+ 'seekable',
+ 'seeking',
+ 'started',
+ 'userBehindLiveEdge',
+ 'waiting',
]);
/**
* Resets all media state and leaves general player state intact (i.e., `autoplay`, `volume`, etc.).
*/
export function softResetMediaState($media: MediaStore) {
- mediaState.reset($media, (prop) => !DO_NOT_RESET_ON_SRC_CHANGE.has(prop));
+ mediaState.reset($media, (prop) => RESET_ON_SRC_CHANGE.has(prop));
tick();
}
@@ -316,6 +306,14 @@ export interface MediaState {
* The type of remote playback that is currently connecting or connected.
*/
remotePlaybackType: RemotePlaybackType;
+ /**
+ * An active remote playback loader such as the `GoogleCastLoader`.
+ */
+ remotePlaybackLoader: MediaProviderLoader | null;
+ /**
+ * Information about the current remote playback.
+ */
+ remotePlaybackInfo: RemotePlaybackInfo | null;
/**
* Whether AirPlay is connected.
*/
@@ -792,7 +790,7 @@ export interface MediaState {
// !!! INTERNALS !!!
/* @internal */
- autoplaying: boolean;
+ autoPlaying: boolean;
/* @internal */
providedTitle: string;
/* @internal */
diff --git a/packages/vidstack/src/core/api/types.ts b/packages/vidstack/src/core/api/types.ts
index fe0192a1d..bc414a66d 100644
--- a/packages/vidstack/src/core/api/types.ts
+++ b/packages/vidstack/src/core/api/types.ts
@@ -18,6 +18,14 @@ export type MediaCrossOrigin = '' | 'anonymous' | 'use-credentials';
export type RemotePlaybackType = 'airplay' | 'google-cast' | 'none';
+export interface RemotePlaybackInfo {
+ deviceName?: string;
+ savedState?: {
+ paused?: boolean;
+ currentTime?: number;
+ };
+}
+
/**
* Indicates the current view type which determines how the media will be presented.
*/
@@ -59,7 +67,7 @@ export type MediaErrorCode = 1 | 2 | 3 | 4;
export interface MediaErrorDetail {
message: string;
- code: MediaErrorCode;
+ code?: MediaErrorCode;
error?: Error;
mediaError?: MediaError;
}
diff --git a/packages/vidstack/src/core/state/media-player-delegate.ts b/packages/vidstack/src/core/state/media-player-delegate.ts
index 7a3a34749..3b4130e4a 100644
--- a/packages/vidstack/src/core/state/media-player-delegate.ts
+++ b/packages/vidstack/src/core/state/media-player-delegate.ts
@@ -63,7 +63,11 @@ export class MediaPlayerDelegate {
const provider = peek(this._media.$provider),
{ storage } = this._media,
{ muted, volume, playsinline, clipStartTime } = this._media.$props,
- startTime = storage.data.time ?? clipStartTime();
+ { remotePlaybackInfo } = this._media.$state,
+ remotePlaybackTime = remotePlaybackInfo()?.savedState?.currentTime,
+ wasRemotePlaying = remotePlaybackInfo()?.savedState?.paused === false,
+ startTime = remotePlaybackTime ?? storage.data.time ?? clipStartTime(),
+ shouldAutoPlay = wasRemotePlaying || $state.autoplay();
if (provider) {
provider.setVolume(storage.data.volume ?? peek(volume));
@@ -72,15 +76,17 @@ export class MediaPlayerDelegate {
if (startTime > 0) provider.setCurrentTime(startTime);
}
- if ($state.canPlay() && $state.autoplay() && !$state.started()) {
+ if ($state.canPlay() && shouldAutoPlay && !$state.started()) {
await this._attemptAutoplay(trigger);
}
+
+ remotePlaybackInfo.set(null);
}
private async _attemptAutoplay(trigger?: Event) {
const { player, $state } = this._media;
- $state.autoplaying.set(true);
+ $state.autoPlaying.set(true);
const attemptEvent = new DOMEvent('autoplay-attempt', { trigger });
@@ -93,7 +99,7 @@ export class MediaPlayerDelegate {
: '';
this._media.logger
- ?.errorGroup('autoplay request failed')
+ ?.errorGroup('[vidstack] autoplay request failed')
.labelledLog(
'Message',
`Autoplay was requested but failed most likely due to browser autoplay policies.${muteMsg}`,
diff --git a/packages/vidstack/src/core/state/media-request-manager.ts b/packages/vidstack/src/core/state/media-request-manager.ts
index 76edf206f..1c99b02fe 100644
--- a/packages/vidstack/src/core/state/media-request-manager.ts
+++ b/packages/vidstack/src/core/state/media-request-manager.ts
@@ -8,8 +8,12 @@ import {
import { ScreenOrientationController } from '../../foundation/orientation/controller';
import { Queue } from '../../foundation/queue/queue';
import { RequestQueue } from '../../foundation/queue/request-queue';
+import type { GoogleCastLoader } from '../../providers/google-cast/loader';
import type { MediaProviderAdapter } from '../../providers/types';
import { coerceToError } from '../../utils/error';
+import { canGoogleCastSrc } from '../../utils/mime';
+import { preconnect } from '../../utils/network';
+import { IS_CHROME, IS_IOS } from '../../utils/support';
import type { MediaContext } from '../api/media-context';
import type { MediaFullscreenChangeEvent } from '../api/media-events';
import * as RE from '../api/media-request-events';
@@ -57,9 +61,11 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}
this._attachLoadPlayListener();
+
effect(this._watchProvider.bind(this));
effect(this._onControlsDelayChange.bind(this));
effect(this._onAirPlaySupportChange.bind(this));
+ effect(this._onGoogleCastSupportChange.bind(this));
effect(this._onFullscreenSupportChange.bind(this));
effect(this._onPiPSupportChange.bind(this));
}
@@ -117,7 +123,7 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
async _play(trigger?: Event) {
if (__SERVER__) return;
- const { canPlay, paused, autoplaying } = this.$state;
+ const { canPlay, paused, autoPlaying } = this.$state;
if (this._handleLoadPlayStrategy(trigger)) return;
@@ -137,7 +143,7 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
trigger,
});
- errorEvent.autoplay = autoplaying();
+ errorEvent.autoplay = autoPlaying();
this._stateMgr._handle(errorEvent);
throw error;
@@ -316,6 +322,12 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
canAirPlay.set(supported);
}
+ private _onGoogleCastSupportChange() {
+ const { canGoogleCast, source } = this.$state,
+ supported = IS_CHROME && !IS_IOS && canGoogleCastSrc(source());
+ canGoogleCast.set(supported);
+ }
+
private _onFullscreenSupportChange() {
const { canFullscreen } = this.$state,
supported = this._fullscreen.supported || !!this._$provider()?.fullscreen?.supported;
@@ -340,7 +352,7 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
try {
const adapter = this._$provider()?.airPlay;
- if (!adapter) {
+ if (!adapter?.supported) {
throw Error(__DEV__ ? 'AirPlay adapter not available on provider.' : 'No AirPlay adapter.');
}
@@ -348,7 +360,7 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
this._request._queue._enqueue('media-airplay-request', trigger);
}
- return await adapter.request();
+ return await adapter.prompt();
} catch (error) {
this._request._queue._delete('media-airplay-request');
@@ -361,7 +373,59 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
}
async ['media-google-cast-request'](event: RE.MediaGoogleCastRequestEvent) {
- // TODO: Implement google cast.
+ try {
+ await this._requestGoogleCast(event);
+ } catch (error) {
+ // no-op
+ }
+ }
+
+ protected _googleCastLoader?: GoogleCastLoader;
+ async _requestGoogleCast(trigger?: Event) {
+ try {
+ const { canGoogleCast } = this.$state;
+
+ if (!peek(canGoogleCast)) {
+ throw new Error(
+ __DEV__ ? 'Google Cast not available on this platform.' : 'Cast not available.',
+ );
+ }
+
+ preconnect('https://www.gstatic.com');
+
+ if (!this._googleCastLoader) {
+ const $module = await import('../../providers/google-cast/loader');
+ this._googleCastLoader = new $module.GoogleCastLoader();
+ }
+
+ await this._googleCastLoader.prompt(this._media);
+
+ if (trigger) {
+ this._request._queue._enqueue('media-google-cast-request', trigger);
+ }
+
+ const isConnecting = peek(this.$state.remotePlaybackState) !== 'disconnected';
+
+ if (isConnecting) {
+ this.$state.remotePlaybackInfo.set((info) => ({
+ ...info,
+ savedState: {
+ paused: peek(this.$state.paused),
+ currentTime: peek(this.$state.currentTime),
+ },
+ }));
+ }
+
+ this.$state.remotePlaybackLoader.set(isConnecting ? this._googleCastLoader : null);
+ } catch (error) {
+ this._request._queue._delete('media-google-cast-request');
+
+ if (__DEV__ && (error as Error).message !== '"cancel"') {
+ this._logError('google cast request failed', error, trigger);
+ }
+
+ throw error;
+ }
}
['media-audio-track-change-request'](event: RE.MediaAudioTrackChangeRequestEvent) {
@@ -703,7 +767,7 @@ export class MediaRequestManager extends MediaPlayerController implements MediaR
private _logError(title: string, error: unknown, request?: Event) {
if (!__DEV__) return;
this._media.logger
- ?.errorGroup(`[vidstack]: ${title}`)
+ ?.errorGroup(`[vidstack] ${title}`)
.labelledLog('Error', error)
.labelledLog('Media Context', { ...this._media })
.labelledLog('Trigger Event', request)
diff --git a/packages/vidstack/src/core/state/media-state-manager.ts b/packages/vidstack/src/core/state/media-state-manager.ts
index 49b26cb67..72c3a97b5 100644
--- a/packages/vidstack/src/core/state/media-state-manager.ts
+++ b/packages/vidstack/src/core/state/media-state-manager.ts
@@ -1,9 +1,10 @@
import debounce from 'just-debounce-it';
import throttle from 'just-throttle';
-import { onDispose, peek } from 'maverick.js';
+import { effect, onDispose, peek } from 'maverick.js';
import { DOMEvent, listenEvent } from 'maverick.js/std';
import { ListSymbol } from '../../foundation/list/symbols';
+import { canChangeVolume } from '../../utils/support';
import type { MediaContext } from '../api/media-context';
import * as ME from '../api/media-events';
import { MediaPlayerController } from '../api/player-controller';
@@ -53,6 +54,7 @@ export class MediaStateManager extends MediaPlayerController {
}
protected override onConnect(el: HTMLElement) {
+ effect(this._watchCanSetVolume.bind(this));
this._addTextTrackListeners();
this._addQualityListeners();
this._addAudioTrackListeners();
@@ -74,12 +76,10 @@ export class MediaStateManager extends MediaPlayerController {
private _resumePlaybackOnConnect() {
if (!this._isPlayingOnDisconnect) return;
- if (this._media.$provider()?.paused) {
- requestAnimationFrame(() => {
- if (!this.scope) return;
- this._media.remote.play(new DOMEvent('dom-connect'));
- });
- }
+ requestAnimationFrame(() => {
+ if (!this.scope) return;
+ this._media.remote.play(new DOMEvent('dom-connect'));
+ });
this._isPlayingOnDisconnect = false;
}
@@ -207,6 +207,18 @@ export class MediaStateManager extends MediaPlayerController {
this.$state.canSetQuality.set(!this._media.qualities.readonly);
}
+ protected _watchCanSetVolume() {
+ const { canSetVolume, isGoogleCastConnected } = this.$state;
+
+ if (isGoogleCastConnected()) {
+ // The provider will set this value accordingly.
+ canSetVolume.set(false);
+ return;
+ }
+
+ canChangeVolume().then(canSetVolume.set);
+ }
+
['provider-change'](event: ME.MediaProviderChangeEvent) {
const prevProvider = this._media.$provider(),
newProvider = event.detail;
@@ -422,7 +434,7 @@ export class MediaStateManager extends MediaPlayerController {
}
['play'](event: ME.MediaPlayEvent) {
- const { paused, autoplayError, ended, autoplaying, playsinline, pointer, muted, viewType } =
+ const { paused, autoplayError, ended, autoPlaying, playsinline, pointer, muted, viewType } =
this.$state;
this._resetPlaybackIfNeeded();
@@ -432,7 +444,7 @@ export class MediaStateManager extends MediaPlayerController {
return;
}
- event.autoplay = autoplaying();
+ event.autoplay = autoPlaying();
const waitingEvent = this._trackedEvents.get('waiting');
if (waitingEvent) event.triggers.add(waitingEvent);
@@ -451,7 +463,7 @@ export class MediaStateManager extends MediaPlayerController {
}),
);
- autoplaying.set(false);
+ autoPlaying.set(false);
}
if (ended() || this._request._replaying) {
@@ -493,7 +505,7 @@ export class MediaStateManager extends MediaPlayerController {
}
['play-fail'](event: ME.MediaPlayFailEvent) {
- const { muted, autoplaying } = this.$state;
+ const { muted, autoPlaying } = this.$state;
const playEvent = this._trackedEvents.get('play');
if (playEvent) event.triggers.add(playEvent);
@@ -518,7 +530,7 @@ export class MediaStateManager extends MediaPlayerController {
}),
);
- autoplaying.set(false);
+ autoPlaying.set(false);
}
}
diff --git a/packages/vidstack/src/core/state/remote-control.ts b/packages/vidstack/src/core/state/remote-control.ts
index 47531cf4e..6b688c7e0 100644
--- a/packages/vidstack/src/core/state/remote-control.ts
+++ b/packages/vidstack/src/core/state/remote-control.ts
@@ -96,6 +96,15 @@ export class MediaRemoteControl {
this._dispatchRequest('media-airplay-request', trigger);
}
+ /**
+ * Dispatch a request to connect Google Cast.
+ *
+ * @see {@link https://developers.google.com/cast/docs/overview}
+ */
+ requestGoogleCast(trigger?: Event) {
+ this._dispatchRequest('media-google-cast-request', trigger);
+ }
+
/**
* Dispatch a request to begin/resume media playback.
*/
diff --git a/packages/vidstack/src/core/tracks/text/text-track.ts b/packages/vidstack/src/core/tracks/text/text-track.ts
index 6c7cf78c5..56767668e 100644
--- a/packages/vidstack/src/core/tracks/text/text-track.ts
+++ b/packages/vidstack/src/core/tracks/text/text-track.ts
@@ -130,7 +130,7 @@ export class TextTrack extends EventsTarget {
} else if (!init.src) this[TextTrackSymbol._readyState] = 2;
if (__DEV__ && isTrackCaptionKind(this) && !this.label) {
- throw Error(`[vidstack]: captions text track created without label: \`${this.src}\``);
+ throw Error(`[vidstack] captions text track created without label: \`${this.src}\``);
}
}
diff --git a/packages/vidstack/src/elements/bundles/player-ui.ts b/packages/vidstack/src/elements/bundles/player-ui.ts
index e6e535231..3e521f1f5 100644
--- a/packages/vidstack/src/elements/bundles/player-ui.ts
+++ b/packages/vidstack/src/elements/bundles/player-ui.ts
@@ -3,6 +3,7 @@ import { defineCustomElement } from 'maverick.js/element';
import { MediaAirPlayButtonElement } from '../define/buttons/airplay-button-element';
import { MediaCaptionButtonElement } from '../define/buttons/caption-button-element';
import { MediaFullscreenButtonElement } from '../define/buttons/fullscreen-button-element';
+import { MediaGoogleCastButtonElement } from '../define/buttons/google-cast-button-element';
import { MediaLiveButtonElement } from '../define/buttons/live-button-element';
import { MediaMuteButtonElement } from '../define/buttons/mute-button-element';
import { MediaPIPButtonElement } from '../define/buttons/pip-button-element';
@@ -56,6 +57,7 @@ defineCustomElement(MediaPlayButtonElement);
defineCustomElement(MediaSeekButtonElement);
defineCustomElement(MediaToggleButtonElement);
defineCustomElement(MediaAirPlayButtonElement);
+defineCustomElement(MediaGoogleCastButtonElement);
// Sliders
defineCustomElement(MediaSliderElement);
defineCustomElement(MediaVolumeSliderElement);
diff --git a/packages/vidstack/src/elements/define/buttons/google-cast-button-element.ts b/packages/vidstack/src/elements/define/buttons/google-cast-button-element.ts
new file mode 100644
index 000000000..afb5b8bbe
--- /dev/null
+++ b/packages/vidstack/src/elements/define/buttons/google-cast-button-element.ts
@@ -0,0 +1,21 @@
+import { Host } from 'maverick.js/element';
+
+import { GoogleCastButton } from '../../../components';
+
+/**
+ * @example
+ * ```html
+ *
+ *
+ *
+ * ```
+ */
+export class MediaGoogleCastButtonElement extends Host(HTMLElement, GoogleCastButton) {
+ static tagName = 'media-google-cast-button';
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'media-google-cast-button': MediaGoogleCastButtonElement;
+ }
+}
diff --git a/packages/vidstack/src/elements/define/layouts/default/icons.ts b/packages/vidstack/src/elements/define/layouts/default/icons.ts
index 30ce5ca83..fc2ebeb29 100644
--- a/packages/vidstack/src/elements/define/layouts/default/icons.ts
+++ b/packages/vidstack/src/elements/define/layouts/default/icons.ts
@@ -2,6 +2,7 @@ import airplay from 'media-icons/dist/icons/airplay.js';
import menuArrowLeft from 'media-icons/dist/icons/arrow-left.js';
import chapters from 'media-icons/dist/icons/chapters.js';
import menuArrowRight from 'media-icons/dist/icons/chevron-right.js';
+import googleCast from 'media-icons/dist/icons/chromecast.js';
import ccOn from 'media-icons/dist/icons/closed-captions-on.js';
import ccOff from 'media-icons/dist/icons/closed-captions.js';
import menuCaptions from 'media-icons/dist/icons/closed-captions.js';
@@ -28,6 +29,7 @@ export const icons = {
pause,
replay,
mute,
+ 'google-cast': googleCast,
'volume-low': volumeLow,
'volume-high': volumeHigh,
'cc-on': ccOn,
diff --git a/packages/vidstack/src/elements/define/layouts/default/shared-layout.ts b/packages/vidstack/src/elements/define/layouts/default/shared-layout.ts
index f16c0a763..f5c2a9726 100644
--- a/packages/vidstack/src/elements/define/layouts/default/shared-layout.ts
+++ b/packages/vidstack/src/elements/define/layouts/default/shared-layout.ts
@@ -42,6 +42,28 @@ export function DefaultAirPlayButton({ tooltip }: { tooltip: TooltipPlacement })
`;
}
+export function DefaultGoogleCastButton({ tooltip }: { tooltip: TooltipPlacement }) {
+ const { translations } = useDefaultLayoutContext(),
+ { remotePlaybackState } = useMediaContext().$state,
+ $label = $computed(() => {
+ const googleCastText = getDefaultLayoutLang(translations, 'Google Cast'),
+ stateText = uppercaseFirstChar(remotePlaybackState()) as Capitalize;
+ return `${googleCastText} ${stateText}`;
+ });
+ return html`
+
+
+
+
+
+
+
+ ${$i18n(translations, 'Google Cast')}
+
+
+ `;
+}
+
export function DefaultPlayButton({ tooltip }: { tooltip: TooltipPlacement }) {
const { translations } = useDefaultLayoutContext(),
{ paused } = useMediaContext().$state,
diff --git a/packages/vidstack/src/elements/define/layouts/default/video-layout.ts b/packages/vidstack/src/elements/define/layouts/default/video-layout.ts
index 9d61ed4c9..d7a0aa309 100644
--- a/packages/vidstack/src/elements/define/layouts/default/video-layout.ts
+++ b/packages/vidstack/src/elements/define/layouts/default/video-layout.ts
@@ -10,6 +10,7 @@ import {
DefaultCaptionButton,
DefaultChaptersMenu,
DefaultFullscreenButton,
+ DefaultGoogleCastButton,
DefaultMuteButton,
DefaultPIPButton,
DefaultPlayButton,
@@ -40,7 +41,7 @@ export function DefaultVideoLayoutLarge() {
${$computed(DefaultTimeInfo)}
${DefaultCaptionButton({ tooltip: 'top' })}${$computed(DefaultBottomMenuGroup)}
- ${DefaultAirPlayButton({ tooltip: 'top' })}
+ ${DefaultAirPlayButton({ tooltip: 'top' })} ${DefaultGoogleCastButton({ tooltip: 'top' })}
${DefaultPIPButton()}${DefaultFullscreenButton({ tooltip: 'top end' })}
@@ -77,6 +78,7 @@ export function DefaultVideoLayoutSmall() {
${DefaultAirPlayButton({ tooltip: 'top start' })}
+ ${DefaultGoogleCastButton({ tooltip: 'top start' })}
${DefaultCaptionButton({ tooltip: 'bottom' })}
${DefaultVideoMenus()}${DefaultMuteButton({ tooltip: 'bottom end' })}
diff --git a/packages/vidstack/src/elements/define/provider-element.ts b/packages/vidstack/src/elements/define/provider-element.ts
index 8a473244c..b122e9328 100644
--- a/packages/vidstack/src/elements/define/provider-element.ts
+++ b/packages/vidstack/src/elements/define/provider-element.ts
@@ -37,16 +37,19 @@ export class MediaProviderElement extends Host(HTMLElement, MediaProvider) {
protected onConnect(): void {
effect(() => {
const loader = this.$state.loader(),
- isYouTubeEmbed = loader?.canPlay({ src: '', type: 'video/youtube' }),
- isVimeoEmbed = loader?.canPlay({ src: '', type: 'video/vimeo' }),
- isEmbed = isYouTubeEmbed || isVimeoEmbed;
+ isYouTubeEmbed = loader?.name === 'youtube',
+ isVimeoEmbed = loader?.name === 'vimeo',
+ isEmbed = isYouTubeEmbed || isVimeoEmbed,
+ isGoogleCast = loader?.name === 'google-cast';
const target = loader
- ? isEmbed
- ? this._createIFrame()
- : loader.mediaType() === 'audio'
- ? this._createAudio()
- : this._createVideo()
+ ? isGoogleCast
+ ? document.createElement('div')
+ : isEmbed
+ ? this._createIFrame()
+ : loader.mediaType() === 'audio'
+ ? this._createAudio()
+ : this._createVideo()
: null;
if (this._target !== target) {
@@ -78,6 +81,7 @@ export class MediaProviderElement extends Host(HTMLElement, MediaProvider) {
if (isYouTubeEmbed) target?.classList.add('vds-youtube');
else if (isVimeoEmbed) target?.classList.add('vds-vimeo');
+ else if (isGoogleCast) target?.classList.add('vds-google-cast');
if (!isEmbed) {
this._blocker?.remove();
diff --git a/packages/vidstack/src/elements/index.ts b/packages/vidstack/src/elements/index.ts
index 7df6b0122..e1c810930 100644
--- a/packages/vidstack/src/elements/index.ts
+++ b/packages/vidstack/src/elements/index.ts
@@ -21,14 +21,15 @@ export { MediaVideoLayoutElement } from './define/layouts/default/video-layout-e
// Buttons
export { MediaAirPlayButtonElement } from './define/buttons/airplay-button-element';
-export { MediaPlayButtonElement } from './define/buttons/play-button-element';
-export { MediaMuteButtonElement } from './define/buttons/mute-button-element';
export { MediaCaptionButtonElement } from './define/buttons/caption-button-element';
export { MediaFullscreenButtonElement } from './define/buttons/fullscreen-button-element';
+export { MediaGoogleCastButtonElement } from './define/buttons/google-cast-button-element';
+export { MediaLiveButtonElement } from './define/buttons/live-button-element';
+export { MediaMuteButtonElement } from './define/buttons/mute-button-element';
export { MediaPIPButtonElement } from './define/buttons/pip-button-element';
+export { MediaPlayButtonElement } from './define/buttons/play-button-element';
export { MediaSeekButtonElement } from './define/buttons/seek-button-element';
export { MediaToggleButtonElement } from './define/buttons/toggle-button-element';
-export { MediaLiveButtonElement } from './define/buttons/live-button-element';
// Tooltips
export { MediaTooltipElement } from './define/tooltips/tooltip-element';
diff --git a/packages/vidstack/src/elements/lit/directives/signal.ts b/packages/vidstack/src/elements/lit/directives/signal.ts
index af434452e..f3f7c3efa 100644
--- a/packages/vidstack/src/elements/lit/directives/signal.ts
+++ b/packages/vidstack/src/elements/lit/directives/signal.ts
@@ -54,7 +54,7 @@ class SignalDirective extends AsyncDirective {
].join('\n');
console.warn(
- `[vidstack]: Failed to render most likely due to a hydration issue with your framework.` +
+ `[vidstack] Failed to render most likely due to a hydration issue with your framework.` +
` Dynamically importing the player should resolve the issue.` +
`\n\nSvelte Example:\n\n${svelteDynamicImportExample}`,
);
diff --git a/packages/vidstack/src/globals.d.ts b/packages/vidstack/src/globals.d.ts
index 033435821..313cdf013 100644
--- a/packages/vidstack/src/globals.d.ts
+++ b/packages/vidstack/src/globals.d.ts
@@ -1,4 +1,5 @@
///
+///
declare global {
const __DEV__: boolean;
diff --git a/packages/vidstack/src/index.ts b/packages/vidstack/src/index.ts
index 46f46bba7..132d0aed1 100644
--- a/packages/vidstack/src/index.ts
+++ b/packages/vidstack/src/index.ts
@@ -1,5 +1,5 @@
if (__DEV__) {
- console.warn('[vidstack]: dev mode!');
+ console.warn('[vidstack] dev mode!');
}
// Foundation
diff --git a/packages/vidstack/src/providers/audio/loader.ts b/packages/vidstack/src/providers/audio/loader.ts
index a78b4283e..5432c3ff2 100644
--- a/packages/vidstack/src/providers/audio/loader.ts
+++ b/packages/vidstack/src/providers/audio/loader.ts
@@ -1,19 +1,15 @@
-import { isString } from 'maverick.js/std';
-
import type { MediaSrc, MediaType } from '../../core';
-import { AUDIO_EXTENSIONS, AUDIO_TYPES } from '../../utils/mime';
+import { isAudioSrc } from '../../utils/mime';
import type { MediaProviderLoader } from '../types';
import type { AudioProvider } from './provider';
export class AudioProviderLoader implements MediaProviderLoader {
+ readonly name = 'audio';
+
target!: HTMLAudioElement;
- canPlay({ src, type }: MediaSrc) {
- return isString(src)
- ? AUDIO_EXTENSIONS.test(src) ||
- AUDIO_TYPES.has(type) ||
- (src.startsWith('blob:') && type === 'audio/object')
- : type === 'audio/object';
+ canPlay(src: MediaSrc) {
+ return isAudioSrc(src);
}
mediaType(): MediaType {
diff --git a/packages/vidstack/src/providers/audio/provider.ts b/packages/vidstack/src/providers/audio/provider.ts
index 8d746d1b2..eef68a400 100644
--- a/packages/vidstack/src/providers/audio/provider.ts
+++ b/packages/vidstack/src/providers/audio/provider.ts
@@ -1,5 +1,5 @@
import { HTMLMediaProvider } from '../html/provider';
-import type { MediaProviderAdapter, MediaSetupContext } from '../types';
+import type { MediaProviderAdapter } from '../types';
/**
* The audio provider adapts the `