@scarlett-player/chromecast 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +319 -0
- package/dist/index.d.cts +234 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +289 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
chromecastPlugin: () => chromecastPlugin,
|
|
24
|
+
default: () => index_default,
|
|
25
|
+
isCastSDKLoaded: () => isCastSDKLoaded,
|
|
26
|
+
isCastSupported: () => isCastSupported,
|
|
27
|
+
loadCastSDK: () => loadCastSDK
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/cast-loader.ts
|
|
32
|
+
var CAST_SDK_URL = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
|
33
|
+
var loadPromise = null;
|
|
34
|
+
function loadCastSDK() {
|
|
35
|
+
if (loadPromise) {
|
|
36
|
+
return loadPromise;
|
|
37
|
+
}
|
|
38
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
39
|
+
if (isCastSDKLoaded()) {
|
|
40
|
+
resolve();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
44
|
+
reject(new Error("Cast SDK requires browser environment"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
window.__onGCastApiAvailable = (isAvailable) => {
|
|
48
|
+
if (isAvailable && isCastSDKLoaded()) {
|
|
49
|
+
resolve();
|
|
50
|
+
} else {
|
|
51
|
+
reject(new Error("Cast SDK reported not available"));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const script = document.createElement("script");
|
|
55
|
+
script.src = CAST_SDK_URL;
|
|
56
|
+
script.async = true;
|
|
57
|
+
script.onerror = () => {
|
|
58
|
+
loadPromise = null;
|
|
59
|
+
reject(new Error("Failed to load Cast SDK script"));
|
|
60
|
+
};
|
|
61
|
+
document.head.appendChild(script);
|
|
62
|
+
});
|
|
63
|
+
return loadPromise;
|
|
64
|
+
}
|
|
65
|
+
function isCastSDKLoaded() {
|
|
66
|
+
return !!(typeof window !== "undefined" && window.cast?.framework?.CastContext);
|
|
67
|
+
}
|
|
68
|
+
function isCastSupported() {
|
|
69
|
+
if (typeof window === "undefined") {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const ua = navigator.userAgent;
|
|
73
|
+
const isChrome = /Chrome/.test(ua) && !/Edge|Edg/.test(ua);
|
|
74
|
+
const isChromium = /Chromium/.test(ua);
|
|
75
|
+
return isChrome || isChromium;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/index.ts
|
|
79
|
+
function chromecastPlugin() {
|
|
80
|
+
let api;
|
|
81
|
+
let castContext = null;
|
|
82
|
+
let currentSession = null;
|
|
83
|
+
let remotePlayer = null;
|
|
84
|
+
let remotePlayerController = null;
|
|
85
|
+
let localTimeBeforeCast = 0;
|
|
86
|
+
let localSrcBeforeCast = "";
|
|
87
|
+
let castStateHandler = null;
|
|
88
|
+
let sessionStateHandler = null;
|
|
89
|
+
let remotePlayerHandler = null;
|
|
90
|
+
const initCastApi = () => {
|
|
91
|
+
if (!window.cast?.framework) {
|
|
92
|
+
api.logger.error("Cast framework not available");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
castContext = window.cast.framework.CastContext.getInstance();
|
|
96
|
+
castContext.setOptions({
|
|
97
|
+
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
|
98
|
+
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
|
99
|
+
});
|
|
100
|
+
remotePlayer = new window.cast.framework.RemotePlayer();
|
|
101
|
+
remotePlayerController = new window.cast.framework.RemotePlayerController(remotePlayer);
|
|
102
|
+
castStateHandler = handleCastStateChange;
|
|
103
|
+
sessionStateHandler = handleSessionStateChange;
|
|
104
|
+
remotePlayerHandler = handleRemotePlayerChange;
|
|
105
|
+
castContext.addEventListener(
|
|
106
|
+
window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
107
|
+
castStateHandler
|
|
108
|
+
);
|
|
109
|
+
castContext.addEventListener(
|
|
110
|
+
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
|
111
|
+
sessionStateHandler
|
|
112
|
+
);
|
|
113
|
+
remotePlayerController.addEventListener(
|
|
114
|
+
window.cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
115
|
+
remotePlayerHandler
|
|
116
|
+
);
|
|
117
|
+
const initialState = castContext.getCastState();
|
|
118
|
+
const available = initialState !== window.cast.framework.CastState.NO_DEVICES_AVAILABLE;
|
|
119
|
+
api.setState("chromecastAvailable", available);
|
|
120
|
+
api.logger.debug("Cast API initialized", { available });
|
|
121
|
+
};
|
|
122
|
+
const handleCastStateChange = (event) => {
|
|
123
|
+
const available = event.castState !== window.cast.framework.CastState.NO_DEVICES_AVAILABLE;
|
|
124
|
+
api.setState("chromecastAvailable", available);
|
|
125
|
+
api.emit(available ? "chromecast:available" : "chromecast:unavailable", void 0);
|
|
126
|
+
api.logger.debug("Cast state changed", { castState: event.castState, available });
|
|
127
|
+
};
|
|
128
|
+
const handleSessionStateChange = (event) => {
|
|
129
|
+
const SessionState = window.cast.framework.SessionState;
|
|
130
|
+
switch (event.sessionState) {
|
|
131
|
+
case SessionState.SESSION_STARTED:
|
|
132
|
+
case SessionState.SESSION_RESUMED:
|
|
133
|
+
currentSession = castContext.getCurrentSession();
|
|
134
|
+
onSessionConnected();
|
|
135
|
+
break;
|
|
136
|
+
case SessionState.SESSION_ENDED:
|
|
137
|
+
onSessionDisconnected();
|
|
138
|
+
currentSession = null;
|
|
139
|
+
break;
|
|
140
|
+
case SessionState.SESSION_START_FAILED:
|
|
141
|
+
api.emit("chromecast:error", {
|
|
142
|
+
error: new Error("Failed to start cast session")
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const onSessionConnected = () => {
|
|
148
|
+
if (!currentSession) return;
|
|
149
|
+
const deviceName = currentSession.getCastDevice()?.friendlyName || "Chromecast";
|
|
150
|
+
api.setState("chromecastActive", true);
|
|
151
|
+
api.emit("chromecast:connected", { deviceName });
|
|
152
|
+
api.logger.info("Chromecast connected", { deviceName });
|
|
153
|
+
localTimeBeforeCast = api.getState("currentTime") || 0;
|
|
154
|
+
const source = api.getState("source");
|
|
155
|
+
localSrcBeforeCast = source?.src || "";
|
|
156
|
+
const video = api.container.querySelector("video");
|
|
157
|
+
if (video) {
|
|
158
|
+
video.pause();
|
|
159
|
+
}
|
|
160
|
+
if (localSrcBeforeCast) {
|
|
161
|
+
loadMediaOnCast(localSrcBeforeCast, localTimeBeforeCast);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const onSessionDisconnected = () => {
|
|
165
|
+
const castTime = remotePlayer?.currentTime || localTimeBeforeCast;
|
|
166
|
+
api.setState("chromecastActive", false);
|
|
167
|
+
api.emit("chromecast:disconnected", void 0);
|
|
168
|
+
api.logger.info("Chromecast disconnected", { resumeTime: castTime });
|
|
169
|
+
const video = api.container.querySelector("video");
|
|
170
|
+
if (video && castTime > 0) {
|
|
171
|
+
video.currentTime = castTime;
|
|
172
|
+
video.play().catch(() => {
|
|
173
|
+
api.logger.debug("Autoplay blocked on cast disconnect");
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
const loadMediaOnCast = async (src, startTime) => {
|
|
178
|
+
if (!currentSession || !window.chrome?.cast) return;
|
|
179
|
+
const contentType = src.includes(".m3u8") ? "application/x-mpegurl" : src.includes(".mpd") ? "application/dash+xml" : "video/mp4";
|
|
180
|
+
const mediaInfo = new window.chrome.cast.media.MediaInfo(src, contentType);
|
|
181
|
+
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
|
|
182
|
+
request.currentTime = startTime;
|
|
183
|
+
request.autoplay = true;
|
|
184
|
+
try {
|
|
185
|
+
await currentSession.loadMedia(request);
|
|
186
|
+
api.logger.debug("Media loaded on Chromecast", { src, startTime });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
api.logger.error("Failed to load media on Chromecast", { error });
|
|
189
|
+
api.emit("chromecast:error", { error });
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const handleRemotePlayerChange = () => {
|
|
193
|
+
if (!remotePlayer) return;
|
|
194
|
+
if (!api.getState("chromecastActive")) return;
|
|
195
|
+
api.setState("currentTime", remotePlayer.currentTime);
|
|
196
|
+
api.setState("duration", remotePlayer.duration);
|
|
197
|
+
api.setState("playing", !remotePlayer.isPaused);
|
|
198
|
+
api.setState("paused", remotePlayer.isPaused);
|
|
199
|
+
api.setState("volume", remotePlayer.volumeLevel);
|
|
200
|
+
api.setState("muted", remotePlayer.isMuted);
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
id: "chromecast",
|
|
204
|
+
name: "Chromecast",
|
|
205
|
+
type: "feature",
|
|
206
|
+
version: "1.0.0",
|
|
207
|
+
async init(pluginApi) {
|
|
208
|
+
api = pluginApi;
|
|
209
|
+
api.setState("chromecastAvailable", false);
|
|
210
|
+
api.setState("chromecastActive", false);
|
|
211
|
+
if (!isCastSupported()) {
|
|
212
|
+
api.logger.debug("Chromecast not supported in this browser");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await loadCastSDK();
|
|
217
|
+
initCastApi();
|
|
218
|
+
api.logger.debug("Chromecast plugin initialized");
|
|
219
|
+
} catch (error) {
|
|
220
|
+
api.logger.warn("Failed to load Cast SDK", { error });
|
|
221
|
+
api.emit("chromecast:error", { error });
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
async destroy() {
|
|
225
|
+
if (currentSession) {
|
|
226
|
+
try {
|
|
227
|
+
currentSession.endSession(true);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (castContext && castStateHandler) {
|
|
232
|
+
castContext.removeEventListener(
|
|
233
|
+
window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
234
|
+
castStateHandler
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (castContext && sessionStateHandler) {
|
|
238
|
+
castContext.removeEventListener(
|
|
239
|
+
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
|
240
|
+
sessionStateHandler
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
if (remotePlayerController && remotePlayerHandler) {
|
|
244
|
+
remotePlayerController.removeEventListener(
|
|
245
|
+
window.cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
246
|
+
remotePlayerHandler
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
castContext = null;
|
|
250
|
+
currentSession = null;
|
|
251
|
+
remotePlayer = null;
|
|
252
|
+
remotePlayerController = null;
|
|
253
|
+
castStateHandler = null;
|
|
254
|
+
sessionStateHandler = null;
|
|
255
|
+
remotePlayerHandler = null;
|
|
256
|
+
api.logger.debug("Chromecast plugin destroyed");
|
|
257
|
+
},
|
|
258
|
+
// Public methods
|
|
259
|
+
async requestSession() {
|
|
260
|
+
if (!castContext) {
|
|
261
|
+
api?.logger.warn("Cast not available");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
await castContext.requestSession();
|
|
265
|
+
},
|
|
266
|
+
endSession() {
|
|
267
|
+
if (currentSession) {
|
|
268
|
+
currentSession.endSession(true);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
isAvailable() {
|
|
272
|
+
return api?.getState("chromecastAvailable") === true;
|
|
273
|
+
},
|
|
274
|
+
isConnected() {
|
|
275
|
+
return api?.getState("chromecastActive") === true;
|
|
276
|
+
},
|
|
277
|
+
getDeviceName() {
|
|
278
|
+
if (!currentSession) return null;
|
|
279
|
+
return currentSession.getCastDevice()?.friendlyName || null;
|
|
280
|
+
},
|
|
281
|
+
play() {
|
|
282
|
+
if (remotePlayer?.isPaused && remotePlayerController) {
|
|
283
|
+
remotePlayerController.playOrPause();
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
pause() {
|
|
287
|
+
if (remotePlayer && !remotePlayer.isPaused && remotePlayerController) {
|
|
288
|
+
remotePlayerController.playOrPause();
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
seek(time) {
|
|
292
|
+
if (remotePlayer && remotePlayerController) {
|
|
293
|
+
remotePlayer.currentTime = time;
|
|
294
|
+
remotePlayerController.seek();
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
setVolume(level) {
|
|
298
|
+
if (remotePlayer && remotePlayerController) {
|
|
299
|
+
remotePlayer.volumeLevel = Math.max(0, Math.min(1, level));
|
|
300
|
+
remotePlayerController.setVolumeLevel();
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
setMuted(muted) {
|
|
304
|
+
if (remotePlayer && remotePlayerController) {
|
|
305
|
+
if (remotePlayer.isMuted !== muted) {
|
|
306
|
+
remotePlayerController.muteOrUnmute();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
var index_default = chromecastPlugin;
|
|
313
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
314
|
+
0 && (module.exports = {
|
|
315
|
+
chromecastPlugin,
|
|
316
|
+
isCastSDKLoaded,
|
|
317
|
+
isCastSupported,
|
|
318
|
+
loadCastSDK
|
|
319
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Plugin } from '@scarlett-player/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chromecast Plugin Types
|
|
5
|
+
*
|
|
6
|
+
* Minimal type definitions for Google Cast SDK.
|
|
7
|
+
* These cover the subset of the API we actually use.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Chrome cast media types */
|
|
11
|
+
declare namespace ChromeCastMedia {
|
|
12
|
+
interface MediaInfo {
|
|
13
|
+
contentId: string;
|
|
14
|
+
contentType: string;
|
|
15
|
+
streamType?: string;
|
|
16
|
+
metadata?: object;
|
|
17
|
+
}
|
|
18
|
+
interface LoadRequest {
|
|
19
|
+
mediaInfo: MediaInfo;
|
|
20
|
+
autoplay: boolean;
|
|
21
|
+
currentTime: number;
|
|
22
|
+
}
|
|
23
|
+
interface Media {
|
|
24
|
+
getEstimatedTime(): number;
|
|
25
|
+
playerState: string;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Cast framework namespace */
|
|
29
|
+
declare namespace CastFramework {
|
|
30
|
+
interface CastContext {
|
|
31
|
+
getInstance(): CastContext;
|
|
32
|
+
setOptions(options: CastOptions): void;
|
|
33
|
+
addEventListener(type: CastContextEventType, handler: (event: CastStateEventData | SessionStateEventData) => void): void;
|
|
34
|
+
removeEventListener(type: CastContextEventType, handler: (event: CastStateEventData | SessionStateEventData) => void): void;
|
|
35
|
+
requestSession(): Promise<void>;
|
|
36
|
+
getCurrentSession(): CastSession | null;
|
|
37
|
+
endCurrentSession(stopCasting: boolean): void;
|
|
38
|
+
getCastState(): CastState;
|
|
39
|
+
}
|
|
40
|
+
interface CastOptions {
|
|
41
|
+
receiverApplicationId: string;
|
|
42
|
+
autoJoinPolicy: string;
|
|
43
|
+
}
|
|
44
|
+
interface CastSession {
|
|
45
|
+
getSessionId(): string;
|
|
46
|
+
getCastDevice(): CastDevice;
|
|
47
|
+
loadMedia(request: ChromeCastMedia.LoadRequest): Promise<void>;
|
|
48
|
+
endSession(stopCasting: boolean): void;
|
|
49
|
+
getMediaSession(): ChromeCastMedia.Media | null;
|
|
50
|
+
}
|
|
51
|
+
interface CastDevice {
|
|
52
|
+
friendlyName: string;
|
|
53
|
+
deviceId: string;
|
|
54
|
+
}
|
|
55
|
+
interface RemotePlayer {
|
|
56
|
+
currentTime: number;
|
|
57
|
+
duration: number;
|
|
58
|
+
isPaused: boolean;
|
|
59
|
+
isMediaLoaded: boolean;
|
|
60
|
+
volumeLevel: number;
|
|
61
|
+
isMuted: boolean;
|
|
62
|
+
playerState: string;
|
|
63
|
+
}
|
|
64
|
+
interface RemotePlayerController {
|
|
65
|
+
addEventListener(type: RemotePlayerEventType, handler: (event: RemotePlayerChangedEvent) => void): void;
|
|
66
|
+
removeEventListener(type: RemotePlayerEventType, handler: (event: RemotePlayerChangedEvent) => void): void;
|
|
67
|
+
playOrPause(): void;
|
|
68
|
+
stop(): void;
|
|
69
|
+
seek(): void;
|
|
70
|
+
setVolumeLevel(): void;
|
|
71
|
+
muteOrUnmute(): void;
|
|
72
|
+
}
|
|
73
|
+
interface CastStateEventData {
|
|
74
|
+
castState: CastState;
|
|
75
|
+
}
|
|
76
|
+
interface SessionStateEventData {
|
|
77
|
+
sessionState: SessionState;
|
|
78
|
+
session?: CastSession;
|
|
79
|
+
}
|
|
80
|
+
interface RemotePlayerChangedEvent {
|
|
81
|
+
field: string;
|
|
82
|
+
value: unknown;
|
|
83
|
+
}
|
|
84
|
+
enum CastContextEventType {
|
|
85
|
+
CAST_STATE_CHANGED = "caststatechanged",
|
|
86
|
+
SESSION_STATE_CHANGED = "sessionstatechanged"
|
|
87
|
+
}
|
|
88
|
+
enum RemotePlayerEventType {
|
|
89
|
+
ANY_CHANGE = "anyChanged",
|
|
90
|
+
IS_CONNECTED_CHANGED = "isConnectedChanged",
|
|
91
|
+
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
|
|
92
|
+
CURRENT_TIME_CHANGED = "currentTimeChanged",
|
|
93
|
+
DURATION_CHANGED = "durationChanged",
|
|
94
|
+
IS_PAUSED_CHANGED = "isPausedChanged",
|
|
95
|
+
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
|
|
96
|
+
IS_MUTED_CHANGED = "isMutedChanged"
|
|
97
|
+
}
|
|
98
|
+
enum CastState {
|
|
99
|
+
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
|
|
100
|
+
NOT_CONNECTED = "NOT_CONNECTED",
|
|
101
|
+
CONNECTING = "CONNECTING",
|
|
102
|
+
CONNECTED = "CONNECTED"
|
|
103
|
+
}
|
|
104
|
+
enum SessionState {
|
|
105
|
+
NO_SESSION = "NO_SESSION",
|
|
106
|
+
SESSION_STARTING = "SESSION_STARTING",
|
|
107
|
+
SESSION_STARTED = "SESSION_STARTED",
|
|
108
|
+
SESSION_START_FAILED = "SESSION_START_FAILED",
|
|
109
|
+
SESSION_ENDING = "SESSION_ENDING",
|
|
110
|
+
SESSION_ENDED = "SESSION_ENDED",
|
|
111
|
+
SESSION_RESUMED = "SESSION_RESUMED"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
declare global {
|
|
115
|
+
interface Window {
|
|
116
|
+
__onGCastApiAvailable?: (isAvailable: boolean) => void;
|
|
117
|
+
cast?: {
|
|
118
|
+
framework: {
|
|
119
|
+
CastContext: {
|
|
120
|
+
getInstance(): CastFramework.CastContext;
|
|
121
|
+
};
|
|
122
|
+
RemotePlayer: new () => CastFramework.RemotePlayer;
|
|
123
|
+
RemotePlayerController: new (player: CastFramework.RemotePlayer) => CastFramework.RemotePlayerController;
|
|
124
|
+
CastContextEventType: typeof CastFramework.CastContextEventType;
|
|
125
|
+
RemotePlayerEventType: typeof CastFramework.RemotePlayerEventType;
|
|
126
|
+
CastState: typeof CastFramework.CastState;
|
|
127
|
+
SessionState: typeof CastFramework.SessionState;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
chrome?: {
|
|
131
|
+
cast: {
|
|
132
|
+
media: {
|
|
133
|
+
DEFAULT_MEDIA_RECEIVER_APP_ID: string;
|
|
134
|
+
MediaInfo: new (contentId: string, contentType: string) => ChromeCastMedia.MediaInfo;
|
|
135
|
+
LoadRequest: new (mediaInfo: ChromeCastMedia.MediaInfo) => ChromeCastMedia.LoadRequest;
|
|
136
|
+
};
|
|
137
|
+
AutoJoinPolicy: {
|
|
138
|
+
ORIGIN_SCOPED: string;
|
|
139
|
+
TAB_AND_ORIGIN_SCOPED: string;
|
|
140
|
+
PAGE_SCOPED: string;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Chromecast plugin interface */
|
|
147
|
+
interface IChromecastPlugin extends Plugin {
|
|
148
|
+
readonly id: 'chromecast';
|
|
149
|
+
/** Request a cast session (opens device picker) */
|
|
150
|
+
requestSession(): Promise<void>;
|
|
151
|
+
/** End the current cast session */
|
|
152
|
+
endSession(): void;
|
|
153
|
+
/** Check if Chromecast devices are available */
|
|
154
|
+
isAvailable(): boolean;
|
|
155
|
+
/** Check if currently connected to a Chromecast */
|
|
156
|
+
isConnected(): boolean;
|
|
157
|
+
/** Get the connected device name */
|
|
158
|
+
getDeviceName(): string | null;
|
|
159
|
+
/** Play on cast device */
|
|
160
|
+
play(): void;
|
|
161
|
+
/** Pause on cast device */
|
|
162
|
+
pause(): void;
|
|
163
|
+
/** Seek on cast device */
|
|
164
|
+
seek(time: number): void;
|
|
165
|
+
/** Set volume on cast device (0-1) */
|
|
166
|
+
setVolume(level: number): void;
|
|
167
|
+
/** Mute/unmute cast device */
|
|
168
|
+
setMuted(muted: boolean): void;
|
|
169
|
+
}
|
|
170
|
+
/** Chromecast error event payload */
|
|
171
|
+
interface ChromecastErrorEvent {
|
|
172
|
+
error: Error;
|
|
173
|
+
code?: string;
|
|
174
|
+
}
|
|
175
|
+
/** Chromecast connected event payload */
|
|
176
|
+
interface ChromecastConnectedEvent {
|
|
177
|
+
deviceName: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Cast SDK Loader
|
|
182
|
+
*
|
|
183
|
+
* Dynamically loads the Google Cast SDK.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* Load the Google Cast SDK.
|
|
187
|
+
*
|
|
188
|
+
* Returns a promise that resolves when the SDK is ready.
|
|
189
|
+
* Safe to call multiple times - will return the same promise.
|
|
190
|
+
*
|
|
191
|
+
* @returns Promise that resolves when SDK is loaded
|
|
192
|
+
*/
|
|
193
|
+
declare function loadCastSDK(): Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* Check if Cast SDK is already loaded and available.
|
|
196
|
+
*/
|
|
197
|
+
declare function isCastSDKLoaded(): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Check if Cast is supported in current environment.
|
|
200
|
+
* Chrome and Chromium-based browsers support Cast.
|
|
201
|
+
*/
|
|
202
|
+
declare function isCastSupported(): boolean;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Chromecast Plugin for Scarlett Player
|
|
206
|
+
*
|
|
207
|
+
* Enables casting video to Chromecast devices using Google Cast SDK.
|
|
208
|
+
* SDK is loaded dynamically when plugin initializes.
|
|
209
|
+
*
|
|
210
|
+
* @packageDocumentation
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a Chromecast plugin instance.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* import { chromecastPlugin } from '@scarlett-player/chromecast';
|
|
219
|
+
*
|
|
220
|
+
* const player = new ScarlettPlayer({
|
|
221
|
+
* container: '#player',
|
|
222
|
+
* plugins: [chromecastPlugin()],
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* // Request cast session
|
|
226
|
+
* const chromecast = player.getPlugin<IChromecastPlugin>('chromecast');
|
|
227
|
+
* if (chromecast?.isAvailable()) {
|
|
228
|
+
* await chromecast.requestSession();
|
|
229
|
+
* }
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function chromecastPlugin(): IChromecastPlugin;
|
|
233
|
+
|
|
234
|
+
export { type ChromecastConnectedEvent, type ChromecastErrorEvent, type IChromecastPlugin, chromecastPlugin, chromecastPlugin as default, isCastSDKLoaded, isCastSupported, loadCastSDK };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Plugin } from '@scarlett-player/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chromecast Plugin Types
|
|
5
|
+
*
|
|
6
|
+
* Minimal type definitions for Google Cast SDK.
|
|
7
|
+
* These cover the subset of the API we actually use.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Chrome cast media types */
|
|
11
|
+
declare namespace ChromeCastMedia {
|
|
12
|
+
interface MediaInfo {
|
|
13
|
+
contentId: string;
|
|
14
|
+
contentType: string;
|
|
15
|
+
streamType?: string;
|
|
16
|
+
metadata?: object;
|
|
17
|
+
}
|
|
18
|
+
interface LoadRequest {
|
|
19
|
+
mediaInfo: MediaInfo;
|
|
20
|
+
autoplay: boolean;
|
|
21
|
+
currentTime: number;
|
|
22
|
+
}
|
|
23
|
+
interface Media {
|
|
24
|
+
getEstimatedTime(): number;
|
|
25
|
+
playerState: string;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Cast framework namespace */
|
|
29
|
+
declare namespace CastFramework {
|
|
30
|
+
interface CastContext {
|
|
31
|
+
getInstance(): CastContext;
|
|
32
|
+
setOptions(options: CastOptions): void;
|
|
33
|
+
addEventListener(type: CastContextEventType, handler: (event: CastStateEventData | SessionStateEventData) => void): void;
|
|
34
|
+
removeEventListener(type: CastContextEventType, handler: (event: CastStateEventData | SessionStateEventData) => void): void;
|
|
35
|
+
requestSession(): Promise<void>;
|
|
36
|
+
getCurrentSession(): CastSession | null;
|
|
37
|
+
endCurrentSession(stopCasting: boolean): void;
|
|
38
|
+
getCastState(): CastState;
|
|
39
|
+
}
|
|
40
|
+
interface CastOptions {
|
|
41
|
+
receiverApplicationId: string;
|
|
42
|
+
autoJoinPolicy: string;
|
|
43
|
+
}
|
|
44
|
+
interface CastSession {
|
|
45
|
+
getSessionId(): string;
|
|
46
|
+
getCastDevice(): CastDevice;
|
|
47
|
+
loadMedia(request: ChromeCastMedia.LoadRequest): Promise<void>;
|
|
48
|
+
endSession(stopCasting: boolean): void;
|
|
49
|
+
getMediaSession(): ChromeCastMedia.Media | null;
|
|
50
|
+
}
|
|
51
|
+
interface CastDevice {
|
|
52
|
+
friendlyName: string;
|
|
53
|
+
deviceId: string;
|
|
54
|
+
}
|
|
55
|
+
interface RemotePlayer {
|
|
56
|
+
currentTime: number;
|
|
57
|
+
duration: number;
|
|
58
|
+
isPaused: boolean;
|
|
59
|
+
isMediaLoaded: boolean;
|
|
60
|
+
volumeLevel: number;
|
|
61
|
+
isMuted: boolean;
|
|
62
|
+
playerState: string;
|
|
63
|
+
}
|
|
64
|
+
interface RemotePlayerController {
|
|
65
|
+
addEventListener(type: RemotePlayerEventType, handler: (event: RemotePlayerChangedEvent) => void): void;
|
|
66
|
+
removeEventListener(type: RemotePlayerEventType, handler: (event: RemotePlayerChangedEvent) => void): void;
|
|
67
|
+
playOrPause(): void;
|
|
68
|
+
stop(): void;
|
|
69
|
+
seek(): void;
|
|
70
|
+
setVolumeLevel(): void;
|
|
71
|
+
muteOrUnmute(): void;
|
|
72
|
+
}
|
|
73
|
+
interface CastStateEventData {
|
|
74
|
+
castState: CastState;
|
|
75
|
+
}
|
|
76
|
+
interface SessionStateEventData {
|
|
77
|
+
sessionState: SessionState;
|
|
78
|
+
session?: CastSession;
|
|
79
|
+
}
|
|
80
|
+
interface RemotePlayerChangedEvent {
|
|
81
|
+
field: string;
|
|
82
|
+
value: unknown;
|
|
83
|
+
}
|
|
84
|
+
enum CastContextEventType {
|
|
85
|
+
CAST_STATE_CHANGED = "caststatechanged",
|
|
86
|
+
SESSION_STATE_CHANGED = "sessionstatechanged"
|
|
87
|
+
}
|
|
88
|
+
enum RemotePlayerEventType {
|
|
89
|
+
ANY_CHANGE = "anyChanged",
|
|
90
|
+
IS_CONNECTED_CHANGED = "isConnectedChanged",
|
|
91
|
+
IS_MEDIA_LOADED_CHANGED = "isMediaLoadedChanged",
|
|
92
|
+
CURRENT_TIME_CHANGED = "currentTimeChanged",
|
|
93
|
+
DURATION_CHANGED = "durationChanged",
|
|
94
|
+
IS_PAUSED_CHANGED = "isPausedChanged",
|
|
95
|
+
VOLUME_LEVEL_CHANGED = "volumeLevelChanged",
|
|
96
|
+
IS_MUTED_CHANGED = "isMutedChanged"
|
|
97
|
+
}
|
|
98
|
+
enum CastState {
|
|
99
|
+
NO_DEVICES_AVAILABLE = "NO_DEVICES_AVAILABLE",
|
|
100
|
+
NOT_CONNECTED = "NOT_CONNECTED",
|
|
101
|
+
CONNECTING = "CONNECTING",
|
|
102
|
+
CONNECTED = "CONNECTED"
|
|
103
|
+
}
|
|
104
|
+
enum SessionState {
|
|
105
|
+
NO_SESSION = "NO_SESSION",
|
|
106
|
+
SESSION_STARTING = "SESSION_STARTING",
|
|
107
|
+
SESSION_STARTED = "SESSION_STARTED",
|
|
108
|
+
SESSION_START_FAILED = "SESSION_START_FAILED",
|
|
109
|
+
SESSION_ENDING = "SESSION_ENDING",
|
|
110
|
+
SESSION_ENDED = "SESSION_ENDED",
|
|
111
|
+
SESSION_RESUMED = "SESSION_RESUMED"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
declare global {
|
|
115
|
+
interface Window {
|
|
116
|
+
__onGCastApiAvailable?: (isAvailable: boolean) => void;
|
|
117
|
+
cast?: {
|
|
118
|
+
framework: {
|
|
119
|
+
CastContext: {
|
|
120
|
+
getInstance(): CastFramework.CastContext;
|
|
121
|
+
};
|
|
122
|
+
RemotePlayer: new () => CastFramework.RemotePlayer;
|
|
123
|
+
RemotePlayerController: new (player: CastFramework.RemotePlayer) => CastFramework.RemotePlayerController;
|
|
124
|
+
CastContextEventType: typeof CastFramework.CastContextEventType;
|
|
125
|
+
RemotePlayerEventType: typeof CastFramework.RemotePlayerEventType;
|
|
126
|
+
CastState: typeof CastFramework.CastState;
|
|
127
|
+
SessionState: typeof CastFramework.SessionState;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
chrome?: {
|
|
131
|
+
cast: {
|
|
132
|
+
media: {
|
|
133
|
+
DEFAULT_MEDIA_RECEIVER_APP_ID: string;
|
|
134
|
+
MediaInfo: new (contentId: string, contentType: string) => ChromeCastMedia.MediaInfo;
|
|
135
|
+
LoadRequest: new (mediaInfo: ChromeCastMedia.MediaInfo) => ChromeCastMedia.LoadRequest;
|
|
136
|
+
};
|
|
137
|
+
AutoJoinPolicy: {
|
|
138
|
+
ORIGIN_SCOPED: string;
|
|
139
|
+
TAB_AND_ORIGIN_SCOPED: string;
|
|
140
|
+
PAGE_SCOPED: string;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Chromecast plugin interface */
|
|
147
|
+
interface IChromecastPlugin extends Plugin {
|
|
148
|
+
readonly id: 'chromecast';
|
|
149
|
+
/** Request a cast session (opens device picker) */
|
|
150
|
+
requestSession(): Promise<void>;
|
|
151
|
+
/** End the current cast session */
|
|
152
|
+
endSession(): void;
|
|
153
|
+
/** Check if Chromecast devices are available */
|
|
154
|
+
isAvailable(): boolean;
|
|
155
|
+
/** Check if currently connected to a Chromecast */
|
|
156
|
+
isConnected(): boolean;
|
|
157
|
+
/** Get the connected device name */
|
|
158
|
+
getDeviceName(): string | null;
|
|
159
|
+
/** Play on cast device */
|
|
160
|
+
play(): void;
|
|
161
|
+
/** Pause on cast device */
|
|
162
|
+
pause(): void;
|
|
163
|
+
/** Seek on cast device */
|
|
164
|
+
seek(time: number): void;
|
|
165
|
+
/** Set volume on cast device (0-1) */
|
|
166
|
+
setVolume(level: number): void;
|
|
167
|
+
/** Mute/unmute cast device */
|
|
168
|
+
setMuted(muted: boolean): void;
|
|
169
|
+
}
|
|
170
|
+
/** Chromecast error event payload */
|
|
171
|
+
interface ChromecastErrorEvent {
|
|
172
|
+
error: Error;
|
|
173
|
+
code?: string;
|
|
174
|
+
}
|
|
175
|
+
/** Chromecast connected event payload */
|
|
176
|
+
interface ChromecastConnectedEvent {
|
|
177
|
+
deviceName: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Cast SDK Loader
|
|
182
|
+
*
|
|
183
|
+
* Dynamically loads the Google Cast SDK.
|
|
184
|
+
*/
|
|
185
|
+
/**
|
|
186
|
+
* Load the Google Cast SDK.
|
|
187
|
+
*
|
|
188
|
+
* Returns a promise that resolves when the SDK is ready.
|
|
189
|
+
* Safe to call multiple times - will return the same promise.
|
|
190
|
+
*
|
|
191
|
+
* @returns Promise that resolves when SDK is loaded
|
|
192
|
+
*/
|
|
193
|
+
declare function loadCastSDK(): Promise<void>;
|
|
194
|
+
/**
|
|
195
|
+
* Check if Cast SDK is already loaded and available.
|
|
196
|
+
*/
|
|
197
|
+
declare function isCastSDKLoaded(): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Check if Cast is supported in current environment.
|
|
200
|
+
* Chrome and Chromium-based browsers support Cast.
|
|
201
|
+
*/
|
|
202
|
+
declare function isCastSupported(): boolean;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Chromecast Plugin for Scarlett Player
|
|
206
|
+
*
|
|
207
|
+
* Enables casting video to Chromecast devices using Google Cast SDK.
|
|
208
|
+
* SDK is loaded dynamically when plugin initializes.
|
|
209
|
+
*
|
|
210
|
+
* @packageDocumentation
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a Chromecast plugin instance.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* import { chromecastPlugin } from '@scarlett-player/chromecast';
|
|
219
|
+
*
|
|
220
|
+
* const player = new ScarlettPlayer({
|
|
221
|
+
* container: '#player',
|
|
222
|
+
* plugins: [chromecastPlugin()],
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* // Request cast session
|
|
226
|
+
* const chromecast = player.getPlugin<IChromecastPlugin>('chromecast');
|
|
227
|
+
* if (chromecast?.isAvailable()) {
|
|
228
|
+
* await chromecast.requestSession();
|
|
229
|
+
* }
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function chromecastPlugin(): IChromecastPlugin;
|
|
233
|
+
|
|
234
|
+
export { type ChromecastConnectedEvent, type ChromecastErrorEvent, type IChromecastPlugin, chromecastPlugin, chromecastPlugin as default, isCastSDKLoaded, isCastSupported, loadCastSDK };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// src/cast-loader.ts
|
|
2
|
+
var CAST_SDK_URL = "https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1";
|
|
3
|
+
var loadPromise = null;
|
|
4
|
+
function loadCastSDK() {
|
|
5
|
+
if (loadPromise) {
|
|
6
|
+
return loadPromise;
|
|
7
|
+
}
|
|
8
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
9
|
+
if (isCastSDKLoaded()) {
|
|
10
|
+
resolve();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
14
|
+
reject(new Error("Cast SDK requires browser environment"));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
window.__onGCastApiAvailable = (isAvailable) => {
|
|
18
|
+
if (isAvailable && isCastSDKLoaded()) {
|
|
19
|
+
resolve();
|
|
20
|
+
} else {
|
|
21
|
+
reject(new Error("Cast SDK reported not available"));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const script = document.createElement("script");
|
|
25
|
+
script.src = CAST_SDK_URL;
|
|
26
|
+
script.async = true;
|
|
27
|
+
script.onerror = () => {
|
|
28
|
+
loadPromise = null;
|
|
29
|
+
reject(new Error("Failed to load Cast SDK script"));
|
|
30
|
+
};
|
|
31
|
+
document.head.appendChild(script);
|
|
32
|
+
});
|
|
33
|
+
return loadPromise;
|
|
34
|
+
}
|
|
35
|
+
function isCastSDKLoaded() {
|
|
36
|
+
return !!(typeof window !== "undefined" && window.cast?.framework?.CastContext);
|
|
37
|
+
}
|
|
38
|
+
function isCastSupported() {
|
|
39
|
+
if (typeof window === "undefined") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
const ua = navigator.userAgent;
|
|
43
|
+
const isChrome = /Chrome/.test(ua) && !/Edge|Edg/.test(ua);
|
|
44
|
+
const isChromium = /Chromium/.test(ua);
|
|
45
|
+
return isChrome || isChromium;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/index.ts
|
|
49
|
+
function chromecastPlugin() {
|
|
50
|
+
let api;
|
|
51
|
+
let castContext = null;
|
|
52
|
+
let currentSession = null;
|
|
53
|
+
let remotePlayer = null;
|
|
54
|
+
let remotePlayerController = null;
|
|
55
|
+
let localTimeBeforeCast = 0;
|
|
56
|
+
let localSrcBeforeCast = "";
|
|
57
|
+
let castStateHandler = null;
|
|
58
|
+
let sessionStateHandler = null;
|
|
59
|
+
let remotePlayerHandler = null;
|
|
60
|
+
const initCastApi = () => {
|
|
61
|
+
if (!window.cast?.framework) {
|
|
62
|
+
api.logger.error("Cast framework not available");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
castContext = window.cast.framework.CastContext.getInstance();
|
|
66
|
+
castContext.setOptions({
|
|
67
|
+
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
|
68
|
+
autoJoinPolicy: window.chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED
|
|
69
|
+
});
|
|
70
|
+
remotePlayer = new window.cast.framework.RemotePlayer();
|
|
71
|
+
remotePlayerController = new window.cast.framework.RemotePlayerController(remotePlayer);
|
|
72
|
+
castStateHandler = handleCastStateChange;
|
|
73
|
+
sessionStateHandler = handleSessionStateChange;
|
|
74
|
+
remotePlayerHandler = handleRemotePlayerChange;
|
|
75
|
+
castContext.addEventListener(
|
|
76
|
+
window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
77
|
+
castStateHandler
|
|
78
|
+
);
|
|
79
|
+
castContext.addEventListener(
|
|
80
|
+
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
|
81
|
+
sessionStateHandler
|
|
82
|
+
);
|
|
83
|
+
remotePlayerController.addEventListener(
|
|
84
|
+
window.cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
85
|
+
remotePlayerHandler
|
|
86
|
+
);
|
|
87
|
+
const initialState = castContext.getCastState();
|
|
88
|
+
const available = initialState !== window.cast.framework.CastState.NO_DEVICES_AVAILABLE;
|
|
89
|
+
api.setState("chromecastAvailable", available);
|
|
90
|
+
api.logger.debug("Cast API initialized", { available });
|
|
91
|
+
};
|
|
92
|
+
const handleCastStateChange = (event) => {
|
|
93
|
+
const available = event.castState !== window.cast.framework.CastState.NO_DEVICES_AVAILABLE;
|
|
94
|
+
api.setState("chromecastAvailable", available);
|
|
95
|
+
api.emit(available ? "chromecast:available" : "chromecast:unavailable", void 0);
|
|
96
|
+
api.logger.debug("Cast state changed", { castState: event.castState, available });
|
|
97
|
+
};
|
|
98
|
+
const handleSessionStateChange = (event) => {
|
|
99
|
+
const SessionState = window.cast.framework.SessionState;
|
|
100
|
+
switch (event.sessionState) {
|
|
101
|
+
case SessionState.SESSION_STARTED:
|
|
102
|
+
case SessionState.SESSION_RESUMED:
|
|
103
|
+
currentSession = castContext.getCurrentSession();
|
|
104
|
+
onSessionConnected();
|
|
105
|
+
break;
|
|
106
|
+
case SessionState.SESSION_ENDED:
|
|
107
|
+
onSessionDisconnected();
|
|
108
|
+
currentSession = null;
|
|
109
|
+
break;
|
|
110
|
+
case SessionState.SESSION_START_FAILED:
|
|
111
|
+
api.emit("chromecast:error", {
|
|
112
|
+
error: new Error("Failed to start cast session")
|
|
113
|
+
});
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const onSessionConnected = () => {
|
|
118
|
+
if (!currentSession) return;
|
|
119
|
+
const deviceName = currentSession.getCastDevice()?.friendlyName || "Chromecast";
|
|
120
|
+
api.setState("chromecastActive", true);
|
|
121
|
+
api.emit("chromecast:connected", { deviceName });
|
|
122
|
+
api.logger.info("Chromecast connected", { deviceName });
|
|
123
|
+
localTimeBeforeCast = api.getState("currentTime") || 0;
|
|
124
|
+
const source = api.getState("source");
|
|
125
|
+
localSrcBeforeCast = source?.src || "";
|
|
126
|
+
const video = api.container.querySelector("video");
|
|
127
|
+
if (video) {
|
|
128
|
+
video.pause();
|
|
129
|
+
}
|
|
130
|
+
if (localSrcBeforeCast) {
|
|
131
|
+
loadMediaOnCast(localSrcBeforeCast, localTimeBeforeCast);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
const onSessionDisconnected = () => {
|
|
135
|
+
const castTime = remotePlayer?.currentTime || localTimeBeforeCast;
|
|
136
|
+
api.setState("chromecastActive", false);
|
|
137
|
+
api.emit("chromecast:disconnected", void 0);
|
|
138
|
+
api.logger.info("Chromecast disconnected", { resumeTime: castTime });
|
|
139
|
+
const video = api.container.querySelector("video");
|
|
140
|
+
if (video && castTime > 0) {
|
|
141
|
+
video.currentTime = castTime;
|
|
142
|
+
video.play().catch(() => {
|
|
143
|
+
api.logger.debug("Autoplay blocked on cast disconnect");
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const loadMediaOnCast = async (src, startTime) => {
|
|
148
|
+
if (!currentSession || !window.chrome?.cast) return;
|
|
149
|
+
const contentType = src.includes(".m3u8") ? "application/x-mpegurl" : src.includes(".mpd") ? "application/dash+xml" : "video/mp4";
|
|
150
|
+
const mediaInfo = new window.chrome.cast.media.MediaInfo(src, contentType);
|
|
151
|
+
const request = new window.chrome.cast.media.LoadRequest(mediaInfo);
|
|
152
|
+
request.currentTime = startTime;
|
|
153
|
+
request.autoplay = true;
|
|
154
|
+
try {
|
|
155
|
+
await currentSession.loadMedia(request);
|
|
156
|
+
api.logger.debug("Media loaded on Chromecast", { src, startTime });
|
|
157
|
+
} catch (error) {
|
|
158
|
+
api.logger.error("Failed to load media on Chromecast", { error });
|
|
159
|
+
api.emit("chromecast:error", { error });
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const handleRemotePlayerChange = () => {
|
|
163
|
+
if (!remotePlayer) return;
|
|
164
|
+
if (!api.getState("chromecastActive")) return;
|
|
165
|
+
api.setState("currentTime", remotePlayer.currentTime);
|
|
166
|
+
api.setState("duration", remotePlayer.duration);
|
|
167
|
+
api.setState("playing", !remotePlayer.isPaused);
|
|
168
|
+
api.setState("paused", remotePlayer.isPaused);
|
|
169
|
+
api.setState("volume", remotePlayer.volumeLevel);
|
|
170
|
+
api.setState("muted", remotePlayer.isMuted);
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
id: "chromecast",
|
|
174
|
+
name: "Chromecast",
|
|
175
|
+
type: "feature",
|
|
176
|
+
version: "1.0.0",
|
|
177
|
+
async init(pluginApi) {
|
|
178
|
+
api = pluginApi;
|
|
179
|
+
api.setState("chromecastAvailable", false);
|
|
180
|
+
api.setState("chromecastActive", false);
|
|
181
|
+
if (!isCastSupported()) {
|
|
182
|
+
api.logger.debug("Chromecast not supported in this browser");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
await loadCastSDK();
|
|
187
|
+
initCastApi();
|
|
188
|
+
api.logger.debug("Chromecast plugin initialized");
|
|
189
|
+
} catch (error) {
|
|
190
|
+
api.logger.warn("Failed to load Cast SDK", { error });
|
|
191
|
+
api.emit("chromecast:error", { error });
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
async destroy() {
|
|
195
|
+
if (currentSession) {
|
|
196
|
+
try {
|
|
197
|
+
currentSession.endSession(true);
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (castContext && castStateHandler) {
|
|
202
|
+
castContext.removeEventListener(
|
|
203
|
+
window.cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
204
|
+
castStateHandler
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (castContext && sessionStateHandler) {
|
|
208
|
+
castContext.removeEventListener(
|
|
209
|
+
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
|
|
210
|
+
sessionStateHandler
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (remotePlayerController && remotePlayerHandler) {
|
|
214
|
+
remotePlayerController.removeEventListener(
|
|
215
|
+
window.cast.framework.RemotePlayerEventType.ANY_CHANGE,
|
|
216
|
+
remotePlayerHandler
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
castContext = null;
|
|
220
|
+
currentSession = null;
|
|
221
|
+
remotePlayer = null;
|
|
222
|
+
remotePlayerController = null;
|
|
223
|
+
castStateHandler = null;
|
|
224
|
+
sessionStateHandler = null;
|
|
225
|
+
remotePlayerHandler = null;
|
|
226
|
+
api.logger.debug("Chromecast plugin destroyed");
|
|
227
|
+
},
|
|
228
|
+
// Public methods
|
|
229
|
+
async requestSession() {
|
|
230
|
+
if (!castContext) {
|
|
231
|
+
api?.logger.warn("Cast not available");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
await castContext.requestSession();
|
|
235
|
+
},
|
|
236
|
+
endSession() {
|
|
237
|
+
if (currentSession) {
|
|
238
|
+
currentSession.endSession(true);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
isAvailable() {
|
|
242
|
+
return api?.getState("chromecastAvailable") === true;
|
|
243
|
+
},
|
|
244
|
+
isConnected() {
|
|
245
|
+
return api?.getState("chromecastActive") === true;
|
|
246
|
+
},
|
|
247
|
+
getDeviceName() {
|
|
248
|
+
if (!currentSession) return null;
|
|
249
|
+
return currentSession.getCastDevice()?.friendlyName || null;
|
|
250
|
+
},
|
|
251
|
+
play() {
|
|
252
|
+
if (remotePlayer?.isPaused && remotePlayerController) {
|
|
253
|
+
remotePlayerController.playOrPause();
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
pause() {
|
|
257
|
+
if (remotePlayer && !remotePlayer.isPaused && remotePlayerController) {
|
|
258
|
+
remotePlayerController.playOrPause();
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
seek(time) {
|
|
262
|
+
if (remotePlayer && remotePlayerController) {
|
|
263
|
+
remotePlayer.currentTime = time;
|
|
264
|
+
remotePlayerController.seek();
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
setVolume(level) {
|
|
268
|
+
if (remotePlayer && remotePlayerController) {
|
|
269
|
+
remotePlayer.volumeLevel = Math.max(0, Math.min(1, level));
|
|
270
|
+
remotePlayerController.setVolumeLevel();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
setMuted(muted) {
|
|
274
|
+
if (remotePlayer && remotePlayerController) {
|
|
275
|
+
if (remotePlayer.isMuted !== muted) {
|
|
276
|
+
remotePlayerController.muteOrUnmute();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
var index_default = chromecastPlugin;
|
|
283
|
+
export {
|
|
284
|
+
chromecastPlugin,
|
|
285
|
+
index_default as default,
|
|
286
|
+
isCastSDKLoaded,
|
|
287
|
+
isCastSupported,
|
|
288
|
+
loadCastSDK
|
|
289
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scarlett-player/chromecast",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chromecast Plugin for Scarlett Player",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
21
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
22
|
+
"test": "vitest --run",
|
|
23
|
+
"test:watch": "vitest",
|
|
24
|
+
"test:coverage": "vitest --coverage"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@scarlett-player/core": "^0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@scarlett-player/core": "file:../../core",
|
|
31
|
+
"typescript": "^5.3.0",
|
|
32
|
+
"tsup": "^8.0.0",
|
|
33
|
+
"vitest": "^1.6.0",
|
|
34
|
+
"@vitest/coverage-v8": "^1.6.0",
|
|
35
|
+
"jsdom": "^24.0.0"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"video",
|
|
39
|
+
"player",
|
|
40
|
+
"chromecast",
|
|
41
|
+
"casting",
|
|
42
|
+
"google-cast",
|
|
43
|
+
"scarlett"
|
|
44
|
+
],
|
|
45
|
+
"author": "The Stream Platform",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/Hackney-Enterprises-Inc/scarlett-player.git",
|
|
50
|
+
"directory": "packages/plugins/chromecast"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Hackney-Enterprises-Inc/scarlett-player/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/Hackney-Enterprises-Inc/scarlett-player#readme"
|
|
56
|
+
}
|