@clockworkdog/cogs-client 3.1.4 → 3.2.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.
@@ -0,0 +1,49 @@
1
+ import { MediaObjectFit } from '.';
2
+ import CogsConnection from './CogsConnection';
3
+ import MediaClipStateMessage from './types/MediaClipStateMessage';
4
+ import { VideoState } from './types/VideoState';
5
+ type EventTypes = {
6
+ state: VideoState;
7
+ videoClipState: MediaClipStateMessage;
8
+ };
9
+ export default class VideoPlayer {
10
+ private cogsConnection;
11
+ private eventTarget;
12
+ private globalVolume;
13
+ private videoClipPlayers;
14
+ private activeClip?;
15
+ private pendingClip?;
16
+ private parentElement;
17
+ private sinkId;
18
+ constructor(cogsConnection: CogsConnection<any>, parentElement?: HTMLElement);
19
+ setParentElement(parentElement: HTMLElement): void;
20
+ resetParentElement(): void;
21
+ setGlobalVolume(globalVolume: number): void;
22
+ playVideoClip(path: string, { playId, volume, loop, fit }: {
23
+ playId: string;
24
+ volume: number;
25
+ loop: boolean;
26
+ fit: MediaObjectFit;
27
+ }): void;
28
+ pauseVideoClip(): void;
29
+ stopVideoClip(): void;
30
+ setVideoClipVolume({ volume }: {
31
+ volume: number;
32
+ }): void;
33
+ setVideoClipFit({ fit }: {
34
+ fit: MediaObjectFit;
35
+ }): void;
36
+ private handleStoppedClip;
37
+ private updateVideoClipPlayer;
38
+ setAudioSink(sinkId: string): void;
39
+ private updateConfig;
40
+ private notifyStateListeners;
41
+ private notifyClipStateListeners;
42
+ addEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | AddEventListenerOptions): void;
43
+ removeEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | EventListenerOptions): void;
44
+ private dispatchEvent;
45
+ private createVideoElement;
46
+ private createClipPlayer;
47
+ private unloadClip;
48
+ }
49
+ export {};
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const VideoState_1 = require("./types/VideoState");
4
+ const DEFAULT_PARENT_ELEMENT = document.body;
5
+ class VideoPlayer {
6
+ constructor(
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ cogsConnection, parentElement = DEFAULT_PARENT_ELEMENT) {
9
+ this.cogsConnection = cogsConnection;
10
+ this.eventTarget = new EventTarget();
11
+ this.globalVolume = 1;
12
+ this.videoClipPlayers = {};
13
+ this.sinkId = '';
14
+ this.parentElement = parentElement;
15
+ // Send the current status of each clip to COGS
16
+ this.addEventListener('videoClipState', ({ detail }) => {
17
+ cogsConnection.sendMediaClipState(detail);
18
+ });
19
+ // Listen for video control messages
20
+ cogsConnection.addEventListener('message', ({ message }) => {
21
+ switch (message.type) {
22
+ case 'media_config_update':
23
+ this.setGlobalVolume(message.globalVolume);
24
+ if (message.audioOutput !== undefined) {
25
+ const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
26
+ this.setAudioSink(sinkId !== null && sinkId !== void 0 ? sinkId : '');
27
+ }
28
+ this.updateConfig(message.files);
29
+ break;
30
+ case 'video_play':
31
+ this.playVideoClip(message.file, {
32
+ playId: message.playId,
33
+ volume: message.volume,
34
+ loop: Boolean(message.loop),
35
+ fit: message.fit,
36
+ });
37
+ break;
38
+ case 'video_pause':
39
+ this.pauseVideoClip();
40
+ break;
41
+ case 'video_stop':
42
+ this.stopVideoClip();
43
+ break;
44
+ case 'video_set_volume':
45
+ this.setVideoClipVolume({ volume: message.volume });
46
+ break;
47
+ case 'video_set_fit':
48
+ this.setVideoClipFit({ fit: message.fit });
49
+ break;
50
+ }
51
+ });
52
+ // On connection, send the current playing state of all clips
53
+ // (Usually empty unless websocket is reconnecting)
54
+ const sendInitialClipStates = () => {
55
+ const files = Object.entries(this.videoClipPlayers).map(([file, player]) => {
56
+ const status = !player.videoElement.paused
57
+ ? 'playing'
58
+ : player.videoElement.currentTime === 0 || player.videoElement.currentTime === player.videoElement.duration
59
+ ? 'paused'
60
+ : 'stopped';
61
+ return [file, status];
62
+ });
63
+ cogsConnection.sendInitialMediaClipStates({ mediaType: 'video', files });
64
+ };
65
+ cogsConnection.addEventListener('open', sendInitialClipStates);
66
+ sendInitialClipStates();
67
+ }
68
+ setParentElement(parentElement) {
69
+ this.parentElement = parentElement;
70
+ Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
71
+ parentElement.appendChild(clipPlayer.videoElement);
72
+ });
73
+ }
74
+ resetParentElement() {
75
+ this.setParentElement(DEFAULT_PARENT_ELEMENT);
76
+ }
77
+ setGlobalVolume(globalVolume) {
78
+ Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
79
+ setVideoElementVolume(clipPlayer.videoElement, clipPlayer.volume * globalVolume);
80
+ });
81
+ this.globalVolume = globalVolume;
82
+ this.notifyStateListeners();
83
+ }
84
+ playVideoClip(path, { playId, volume, loop, fit }) {
85
+ var _a;
86
+ if (!this.videoClipPlayers[path]) {
87
+ this.videoClipPlayers[path] = this.createClipPlayer(path, { preload: 'none', ephemeral: true, fit });
88
+ }
89
+ // Check if there's already a pending clip, which has now been superseded and abort the play operation
90
+ if (this.pendingClip) {
91
+ this.updateVideoClipPlayer(this.pendingClip.path, (clipPlayer) => {
92
+ clipPlayer.videoElement.load(); // Resets the media element
93
+ return clipPlayer;
94
+ });
95
+ }
96
+ // New pending clip is video being requested
97
+ if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) !== path) {
98
+ this.pendingClip = { path, playId, actionOncePlaying: 'play' };
99
+ }
100
+ // Setup and play the pending clip's player
101
+ this.updateVideoClipPlayer(path, (clipPlayer) => {
102
+ clipPlayer.volume = volume;
103
+ setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
104
+ clipPlayer.videoElement.loop = loop;
105
+ clipPlayer.videoElement.style.objectFit = fit;
106
+ if (clipPlayer.videoElement.currentTime === clipPlayer.videoElement.duration) {
107
+ clipPlayer.videoElement.currentTime = 0;
108
+ }
109
+ clipPlayer.videoElement.play();
110
+ // Display right away if there's currently no active clip
111
+ if (!this.activeClip) {
112
+ clipPlayer.videoElement.style.display = 'block';
113
+ }
114
+ return clipPlayer;
115
+ });
116
+ }
117
+ pauseVideoClip() {
118
+ // Pending clip should be paused when it loads and becomes active
119
+ if (this.pendingClip) {
120
+ this.pendingClip.actionOncePlaying = 'pause';
121
+ }
122
+ // Pause the currently active clip
123
+ if (this.activeClip) {
124
+ const { playId, path } = this.activeClip;
125
+ this.updateVideoClipPlayer(path, (clipPlayer) => {
126
+ var _a;
127
+ (_a = clipPlayer.videoElement) === null || _a === void 0 ? void 0 : _a.pause();
128
+ return clipPlayer;
129
+ });
130
+ this.notifyClipStateListeners(playId, path, 'paused');
131
+ }
132
+ }
133
+ stopVideoClip() {
134
+ // Pending clip should be stopped when it loads and becomes active
135
+ if (this.pendingClip) {
136
+ this.pendingClip.actionOncePlaying = 'stop';
137
+ }
138
+ // Stop the currently active clip
139
+ if (this.activeClip) {
140
+ this.handleStoppedClip(this.activeClip.path);
141
+ }
142
+ }
143
+ setVideoClipVolume({ volume }) {
144
+ var _a, _b;
145
+ // If there is a pending clip, this is latest to have been played so update its volume
146
+ const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
147
+ if (!clipToUpdate) {
148
+ return;
149
+ }
150
+ if (!(volume >= 0 && volume <= 1)) {
151
+ console.warn('Invalid volume', volume);
152
+ return;
153
+ }
154
+ this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
155
+ if (clipPlayer.videoElement) {
156
+ clipPlayer.volume = volume;
157
+ setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
158
+ }
159
+ return clipPlayer;
160
+ });
161
+ }
162
+ setVideoClipFit({ fit }) {
163
+ var _a, _b;
164
+ // If there is a pending clip, this is latest to have been played so update its fit
165
+ const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
166
+ if (!clipToUpdate) {
167
+ return;
168
+ }
169
+ this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
170
+ if (clipPlayer.videoElement) {
171
+ clipPlayer.videoElement.style.objectFit = fit;
172
+ }
173
+ return clipPlayer;
174
+ });
175
+ }
176
+ handleStoppedClip(path) {
177
+ var _a;
178
+ if (!this.activeClip || this.activeClip.path !== path) {
179
+ return;
180
+ }
181
+ const playId = this.activeClip.playId;
182
+ // Once an ephemeral clip stops, cleanup and remove the player
183
+ if ((_a = this.videoClipPlayers[this.activeClip.path]) === null || _a === void 0 ? void 0 : _a.config.ephemeral) {
184
+ this.unloadClip(path);
185
+ }
186
+ this.activeClip = undefined;
187
+ this.updateVideoClipPlayer(path, (clipPlayer) => {
188
+ clipPlayer.videoElement.pause();
189
+ clipPlayer.videoElement.currentTime = 0;
190
+ clipPlayer.videoElement.style.display = 'none';
191
+ return clipPlayer;
192
+ });
193
+ this.notifyClipStateListeners(playId, path, 'stopped');
194
+ }
195
+ updateVideoClipPlayer(path, update) {
196
+ if (this.videoClipPlayers[path]) {
197
+ const newPlayer = update(this.videoClipPlayers[path]);
198
+ if (newPlayer) {
199
+ this.videoClipPlayers[path] = newPlayer;
200
+ }
201
+ else {
202
+ delete this.videoClipPlayers[path];
203
+ }
204
+ this.notifyStateListeners();
205
+ }
206
+ }
207
+ setAudioSink(sinkId) {
208
+ for (const clipPlayer of Object.values(this.videoClipPlayers)) {
209
+ setPlayerSinkId(clipPlayer, sinkId);
210
+ }
211
+ this.sinkId = sinkId;
212
+ }
213
+ updateConfig(newPaths) {
214
+ const newVideoPaths = Object.fromEntries(Object.entries(newPaths).filter(([, { type }]) => type === 'video' || !type));
215
+ const previousClipPlayers = this.videoClipPlayers;
216
+ this.videoClipPlayers = (() => {
217
+ const clipPlayers = { ...previousClipPlayers };
218
+ const removedClips = Object.keys(previousClipPlayers).filter((previousPath) => !(previousPath in newVideoPaths));
219
+ removedClips.forEach((path) => {
220
+ var _a, _b;
221
+ if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) === path && ((_b = previousClipPlayers[path]) === null || _b === void 0 ? void 0 : _b.config.ephemeral) === false) {
222
+ this.updateVideoClipPlayer(path, (player) => {
223
+ player.config = { ...player.config, ephemeral: true };
224
+ return player;
225
+ });
226
+ }
227
+ else {
228
+ this.unloadClip(path);
229
+ delete clipPlayers[path];
230
+ }
231
+ });
232
+ const addedClips = Object.entries(newVideoPaths).filter(([newFile]) => !previousClipPlayers[newFile]);
233
+ addedClips.forEach(([path, config]) => {
234
+ clipPlayers[path] = this.createClipPlayer(path, { ...config, preload: preloadString(config.preload), ephemeral: false, fit: 'contain' });
235
+ });
236
+ const updatedClips = Object.entries(previousClipPlayers).filter(([previousPath]) => previousPath in newVideoPaths);
237
+ updatedClips.forEach(([path, previousClipPlayer]) => {
238
+ if (previousClipPlayer.config.preload !== newVideoPaths[path].preload) {
239
+ this.updateVideoClipPlayer(path, (player) => {
240
+ player.config = {
241
+ ...player.config,
242
+ preload: preloadString(newVideoPaths[path].preload),
243
+ ephemeral: false,
244
+ };
245
+ player.videoElement.preload = player.config.preload;
246
+ return player;
247
+ });
248
+ }
249
+ });
250
+ return clipPlayers;
251
+ })();
252
+ this.notifyStateListeners();
253
+ }
254
+ notifyStateListeners() {
255
+ var _a, _b, _c, _d, _e, _f, _g;
256
+ const VideoState = {
257
+ globalVolume: this.globalVolume,
258
+ isPlaying: this.activeClip ? !((_a = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _a === void 0 ? void 0 : _a.paused) : false,
259
+ clips: { ...this.videoClipPlayers },
260
+ activeClip: this.activeClip
261
+ ? {
262
+ path: this.activeClip.path,
263
+ state: !((_b = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _b === void 0 ? void 0 : _b.paused) ? VideoState_1.ActiveVideoClipState.Playing : VideoState_1.ActiveVideoClipState.Paused,
264
+ loop: (_d = (_c = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _c === void 0 ? void 0 : _c.loop) !== null && _d !== void 0 ? _d : false,
265
+ volume: ((_e = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _e === void 0 ? void 0 : _e.muted)
266
+ ? 0
267
+ : ((_g = (_f = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _f === void 0 ? void 0 : _f.volume) !== null && _g !== void 0 ? _g : 0),
268
+ }
269
+ : undefined,
270
+ };
271
+ this.dispatchEvent('state', VideoState);
272
+ }
273
+ notifyClipStateListeners(playId, file, status) {
274
+ this.dispatchEvent('videoClipState', { playId, mediaType: 'video', file, status });
275
+ }
276
+ // Type-safe wrapper around EventTarget
277
+ addEventListener(type, listener, options) {
278
+ this.eventTarget.addEventListener(type, listener, options);
279
+ }
280
+ removeEventListener(type, listener, options) {
281
+ this.eventTarget.removeEventListener(type, listener, options);
282
+ }
283
+ dispatchEvent(type, detail) {
284
+ this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
285
+ }
286
+ createVideoElement(path, config, { volume }) {
287
+ const videoElement = document.createElement('video');
288
+ videoElement.playsInline = true; // Required for iOS
289
+ videoElement.src = this.cogsConnection.getAssetUrl(path);
290
+ videoElement.autoplay = false;
291
+ videoElement.loop = false;
292
+ setVideoElementVolume(videoElement, volume * this.globalVolume);
293
+ videoElement.preload = config.preload;
294
+ videoElement.addEventListener('playing', () => {
295
+ var _a, _b;
296
+ // If the clip is still the pending one when it actually start playing, then ensure it is in the correct state
297
+ if (((_a = this.pendingClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
298
+ switch (this.pendingClip.actionOncePlaying) {
299
+ case 'play': {
300
+ // Continue playing, show the video element, and notify listeners
301
+ videoElement.style.display = 'block';
302
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'playing');
303
+ break;
304
+ }
305
+ case 'pause': {
306
+ // Pause playback, show the video element, and notify listeners
307
+ videoElement.style.display = 'block';
308
+ videoElement.pause();
309
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'paused');
310
+ break;
311
+ }
312
+ case 'stop': {
313
+ // Pause playback, leave the video element hidden, and notify listeners
314
+ videoElement.pause();
315
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'stopped');
316
+ break;
317
+ }
318
+ }
319
+ // If there was a previously active clip, then stop it
320
+ if (this.activeClip) {
321
+ this.handleStoppedClip(this.activeClip.path);
322
+ }
323
+ this.activeClip = this.pendingClip;
324
+ this.pendingClip = undefined;
325
+ }
326
+ else if (((_b = this.activeClip) === null || _b === void 0 ? void 0 : _b.path) === path) {
327
+ // If we were the active clip then just notify listeners that we are now playing
328
+ this.notifyClipStateListeners(this.activeClip.playId, path, 'playing');
329
+ }
330
+ else {
331
+ // Otherwise it shouldn't be playing, like because another clip became pending before we loaded,
332
+ // so we pause and don't show or notify listeners
333
+ videoElement.pause();
334
+ }
335
+ });
336
+ videoElement.addEventListener('ended', () => {
337
+ // Ignore if there's a pending clip, as once that starts playing the active clip will be stopped
338
+ // Also ignore if the video is set to loop
339
+ if (!this.pendingClip && !videoElement.loop) {
340
+ this.handleStoppedClip(path);
341
+ }
342
+ });
343
+ videoElement.style.position = 'absolute';
344
+ videoElement.style.top = '0';
345
+ videoElement.style.left = '0';
346
+ videoElement.style.width = '100%';
347
+ videoElement.style.height = '100%';
348
+ videoElement.style.objectFit = config.fit;
349
+ videoElement.style.display = 'none';
350
+ this.parentElement.appendChild(videoElement);
351
+ return videoElement;
352
+ }
353
+ createClipPlayer(path, config) {
354
+ const volume = 1;
355
+ const player = {
356
+ config,
357
+ videoElement: this.createVideoElement(path, config, { volume }),
358
+ volume,
359
+ };
360
+ setPlayerSinkId(player, this.sinkId);
361
+ return player;
362
+ }
363
+ unloadClip(path) {
364
+ var _a, _b;
365
+ if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
366
+ const playId = this.activeClip.playId;
367
+ this.activeClip = undefined;
368
+ this.notifyClipStateListeners(playId, path, 'stopped');
369
+ }
370
+ (_b = this.videoClipPlayers[path]) === null || _b === void 0 ? void 0 : _b.videoElement.remove();
371
+ this.updateVideoClipPlayer(path, () => null);
372
+ }
373
+ }
374
+ exports.default = VideoPlayer;
375
+ function preloadString(preload) {
376
+ return typeof preload === 'string' ? preload : preload ? 'auto' : 'none';
377
+ }
378
+ function setPlayerSinkId(player, sinkId) {
379
+ if (sinkId === undefined) {
380
+ return;
381
+ }
382
+ if (typeof player.videoElement.setSinkId === 'function') {
383
+ player.videoElement.setSinkId(sinkId);
384
+ }
385
+ }
386
+ /**
387
+ * Set video volume
388
+ *
389
+ * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
390
+ */
391
+ function setVideoElementVolume(videoElement, volume) {
392
+ videoElement.volume = volume;
393
+ videoElement.muted = volume === 0;
394
+ }
@@ -0,0 +1,6 @@
1
+ export declare const COGS_SERVER_PORT = 12095;
2
+ /**
3
+ * Get the URL of an asset hosted by the COGS server.
4
+ */
5
+ export declare function assetUrl(file: string): string;
6
+ export declare function preloadUrl(url: string): Promise<string>;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.COGS_SERVER_PORT = void 0;
4
+ exports.assetUrl = assetUrl;
5
+ exports.preloadUrl = preloadUrl;
6
+ exports.COGS_SERVER_PORT = 12095;
7
+ /**
8
+ * Get the URL of an asset hosted by the COGS server.
9
+ */
10
+ function assetUrl(file) {
11
+ const location = typeof window !== 'undefined' ? window.location : undefined;
12
+ const path = `/assets/${encodeURIComponent(file)}`;
13
+ return `${location === null || location === void 0 ? void 0 : location.protocol}//${location === null || location === void 0 ? void 0 : location.hostname}:${exports.COGS_SERVER_PORT}${path}`;
14
+ }
15
+ async function preloadUrl(url) {
16
+ const response = await fetch(url);
17
+ // We used arrayBuffer()` instead of `blob()` because the latter seems to fail on Pis when preloading some files
18
+ return URL.createObjectURL(new Blob([await response.arrayBuffer()]));
19
+ }
@@ -0,0 +1,5 @@
1
+ export type MediaStatus = 'playing' | 'paused' | 'stopped';
2
+ export default interface AllMediaClipStatesMessage {
3
+ mediaType: 'audio' | 'video';
4
+ files: [string, MediaStatus][];
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,39 @@
1
+ export type ActiveAudioClipState = {
2
+ type: 'paused';
3
+ } | {
4
+ type: 'pause_requested';
5
+ fade: number | undefined;
6
+ } | {
7
+ type: 'pausing';
8
+ } | {
9
+ type: 'playing';
10
+ } | {
11
+ type: 'play_requested';
12
+ } | {
13
+ type: 'stopping';
14
+ } | {
15
+ type: 'stop_requested';
16
+ fade: number | undefined;
17
+ };
18
+ export interface AudioClip {
19
+ config: {
20
+ preload: boolean;
21
+ ephemeral: boolean;
22
+ };
23
+ activeClips: {
24
+ [soundId: number]: ActiveClip;
25
+ };
26
+ }
27
+ export interface ActiveClip {
28
+ state: ActiveAudioClipState;
29
+ loop: boolean;
30
+ volume: number;
31
+ playId: string;
32
+ }
33
+ export interface AudioState {
34
+ isPlaying: boolean;
35
+ globalVolume: number;
36
+ clips: {
37
+ [path: string]: AudioClip;
38
+ };
39
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ export type MediaStatus = 'playing' | 'paused' | 'stopped';
2
+ export default interface MediaClipStateMessage {
3
+ playId: string;
4
+ mediaType: 'audio' | 'video';
5
+ file: string;
6
+ status: MediaStatus;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,26 @@
1
+ import { MediaObjectFit } from '..';
2
+ export declare enum ActiveVideoClipState {
3
+ Paused = "paused",
4
+ Playing = "playing"
5
+ }
6
+ export interface VideoClip {
7
+ config: {
8
+ preload: 'auto' | 'metadata' | 'none';
9
+ ephemeral: boolean;
10
+ fit: MediaObjectFit;
11
+ };
12
+ }
13
+ export interface ActiveClip {
14
+ path: string;
15
+ state: ActiveVideoClipState;
16
+ loop: boolean;
17
+ volume: number;
18
+ }
19
+ export interface VideoState {
20
+ isPlaying: boolean;
21
+ globalVolume: number;
22
+ clips: {
23
+ [path: string]: VideoClip;
24
+ };
25
+ activeClip?: ActiveClip;
26
+ }
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ActiveVideoClipState = void 0;
4
+ var ActiveVideoClipState;
5
+ (function (ActiveVideoClipState) {
6
+ ActiveVideoClipState["Paused"] = "paused";
7
+ ActiveVideoClipState["Playing"] = "playing";
8
+ })(ActiveVideoClipState || (exports.ActiveVideoClipState = ActiveVideoClipState = {}));
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Patch `Date.now()` and `new Date()` given the current time is @param now
3
+ */
4
+ export declare function setDate(now: number): void;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ // Adapted from https://stackoverflow.com/a/58325977/244640
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.setDate = setDate;
5
+ const OriginalDateConstructor = globalThis.Date;
6
+ /**
7
+ * Patch `Date.now()` and `new Date()` given the current time is @param now
8
+ */
9
+ function setDate(now) {
10
+ const nowDelta = now - OriginalDateConstructor.now();
11
+ function Date(...args) {
12
+ if (args.length === 0) {
13
+ return new OriginalDateConstructor(Date.now()); // Date.now() is implemented below
14
+ }
15
+ // Specific date constructor
16
+ return new OriginalDateConstructor(...args);
17
+ }
18
+ // copy all properties from the original date, this includes the prototype
19
+ const propertyDescriptors = Object.getOwnPropertyDescriptors(OriginalDateConstructor);
20
+ Object.defineProperties(Date, propertyDescriptors);
21
+ // override Date.now to return the adjusted time
22
+ Date.now = function () {
23
+ return OriginalDateConstructor.now() + nowDelta;
24
+ };
25
+ globalThis.Date = Date;
26
+ }
@@ -0,0 +1,28 @@
1
+ export interface TimeSyncRequestData {
2
+ timesync: {
3
+ id: number;
4
+ };
5
+ }
6
+ export interface TimeSyncResponseData {
7
+ timesync: {
8
+ id: number;
9
+ now: number;
10
+ };
11
+ }
12
+ export interface TimeSyncClient {
13
+ receive(data: TimeSyncResponseData): void;
14
+ destroy(): void;
15
+ }
16
+ export interface TimeSyncServer {
17
+ receive(data: TimeSyncRequestData): void;
18
+ }
19
+ export declare function createTimeSyncClient({ interval, send, onChange, syncSampleSize, syncRequestTimeout, }: {
20
+ interval: number;
21
+ send: (data: TimeSyncRequestData) => void | Promise<void>;
22
+ onChange?: (now: number) => void;
23
+ syncSampleSize?: number;
24
+ syncRequestTimeout?: number;
25
+ }): TimeSyncClient;
26
+ export declare function createTimeSyncServer({ send }: {
27
+ send: (data: TimeSyncResponseData) => void | Promise<void>;
28
+ }): TimeSyncServer;