@clockworkdog/cogs-client 3.0.0-alpha.8 → 3.0.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.
Files changed (42) hide show
  1. package/README.md +20 -9
  2. package/dist/CogsConnection.d.ts +0 -4
  3. package/dist/CogsConnection.js +0 -10
  4. package/dist/browser/index.mjs +1871 -2773
  5. package/dist/browser/index.umd.js +13 -13
  6. package/dist/index.d.ts +1 -7
  7. package/dist/index.js +1 -5
  8. package/dist/state-based/MediaClipManager.d.ts +66 -0
  9. package/dist/state-based/MediaClipManager.js +420 -0
  10. package/dist/state-based/MediaPreloader.d.ts +2 -2
  11. package/dist/state-based/MediaPreloader.js +10 -3
  12. package/dist/state-based/SurfaceManager.d.ts +5 -1
  13. package/dist/state-based/SurfaceManager.js +31 -10
  14. package/dist/types/MediaSchema.d.ts +19 -13
  15. package/dist/types/MediaSchema.js +3 -1
  16. package/dist/utils/device.d.ts +2 -0
  17. package/dist/utils/device.js +4 -0
  18. package/dist/utils/getStateAtTime.d.ts +12 -2
  19. package/dist/utils/getStateAtTime.js +6 -1
  20. package/dist/utils/modulo.d.ts +6 -0
  21. package/dist/utils/modulo.js +17 -0
  22. package/package.json +3 -6
  23. package/dist/AudioPlayer.d.ts +0 -49
  24. package/dist/AudioPlayer.js +0 -474
  25. package/dist/VideoPlayer.d.ts +0 -49
  26. package/dist/VideoPlayer.js +0 -385
  27. package/dist/state-based/AudioManager.d.ts +0 -17
  28. package/dist/state-based/AudioManager.js +0 -114
  29. package/dist/state-based/ClipManager.d.ts +0 -23
  30. package/dist/state-based/ClipManager.js +0 -60
  31. package/dist/state-based/ImageManager.d.ts +0 -9
  32. package/dist/state-based/ImageManager.js +0 -54
  33. package/dist/state-based/VideoManager.d.ts +0 -19
  34. package/dist/state-based/VideoManager.js +0 -215
  35. package/dist/types/AllMediaClipStatesMessage.d.ts +0 -5
  36. package/dist/types/AllMediaClipStatesMessage.js +0 -1
  37. package/dist/types/AudioState.d.ts +0 -39
  38. package/dist/types/AudioState.js +0 -1
  39. package/dist/types/MediaClipStateMessage.d.ts +0 -7
  40. package/dist/types/MediaClipStateMessage.js +0 -1
  41. package/dist/types/VideoState.d.ts +0 -26
  42. package/dist/types/VideoState.js +0 -5
@@ -1,385 +0,0 @@
1
- import { ActiveVideoClipState } from './types/VideoState';
2
- export default class VideoPlayer {
3
- cogsConnection;
4
- eventTarget = new EventTarget();
5
- globalVolume = 1;
6
- videoClipPlayers = {};
7
- activeClip;
8
- pendingClip;
9
- parentElement;
10
- sinkId = '';
11
- constructor(
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
- cogsConnection, parentElement = document.body) {
14
- this.cogsConnection = cogsConnection;
15
- this.parentElement = parentElement;
16
- // Send the current status of each clip to COGS
17
- this.addEventListener('videoClipState', ({ detail }) => {
18
- cogsConnection.sendMediaClipState(detail);
19
- });
20
- // Listen for video control messages
21
- cogsConnection.addEventListener('message', ({ message }) => {
22
- switch (message.type) {
23
- case 'media_config_update':
24
- this.setGlobalVolume(message.globalVolume);
25
- if (message.audioOutput !== undefined) {
26
- const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
27
- this.setAudioSink(sinkId ?? '');
28
- }
29
- this.updateConfig(message.files);
30
- break;
31
- case 'video_play':
32
- this.playVideoClip(message.file, {
33
- playId: message.playId,
34
- volume: message.volume,
35
- loop: Boolean(message.loop),
36
- fit: message.fit,
37
- });
38
- break;
39
- case 'video_pause':
40
- this.pauseVideoClip();
41
- break;
42
- case 'video_stop':
43
- this.stopVideoClip();
44
- break;
45
- case 'video_set_volume':
46
- this.setVideoClipVolume({ volume: message.volume });
47
- break;
48
- case 'video_set_fit':
49
- this.setVideoClipFit({ fit: message.fit });
50
- break;
51
- }
52
- });
53
- // On connection, send the current playing state of all clips
54
- // (Usually empty unless websocket is reconnecting)
55
- const sendInitialClipStates = () => {
56
- const files = Object.entries(this.videoClipPlayers).map(([file, player]) => {
57
- const status = !player.videoElement.paused
58
- ? 'playing'
59
- : player.videoElement.currentTime === 0 || player.videoElement.currentTime === player.videoElement.duration
60
- ? 'paused'
61
- : 'stopped';
62
- return [file, status];
63
- });
64
- cogsConnection.sendInitialMediaClipStates({ mediaType: 'video', files });
65
- };
66
- cogsConnection.addEventListener('open', sendInitialClipStates);
67
- sendInitialClipStates();
68
- }
69
- setParentElement(parentElement) {
70
- this.parentElement = parentElement;
71
- Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
72
- parentElement.appendChild(clipPlayer.videoElement);
73
- });
74
- }
75
- resetParentElement() {
76
- this.setParentElement(document.body);
77
- }
78
- setGlobalVolume(globalVolume) {
79
- Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
80
- setVideoElementVolume(clipPlayer.videoElement, clipPlayer.volume * globalVolume);
81
- });
82
- this.globalVolume = globalVolume;
83
- this.notifyStateListeners();
84
- }
85
- playVideoClip(path, { playId, volume, loop, fit }) {
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 (this.activeClip?.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
- clipPlayer.videoElement?.pause();
127
- return clipPlayer;
128
- });
129
- this.notifyClipStateListeners(playId, path, 'paused');
130
- }
131
- }
132
- stopVideoClip() {
133
- // Pending clip should be stopped when it loads and becomes active
134
- if (this.pendingClip) {
135
- this.pendingClip.actionOncePlaying = 'stop';
136
- }
137
- // Stop the currently active clip
138
- if (this.activeClip) {
139
- this.handleStoppedClip(this.activeClip.path);
140
- }
141
- }
142
- setVideoClipVolume({ volume }) {
143
- // If there is a pending clip, this is latest to have been played so update its volume
144
- const clipToUpdate = this.pendingClip ?? this.activeClip ?? undefined;
145
- if (!clipToUpdate) {
146
- return;
147
- }
148
- if (!(volume >= 0 && volume <= 1)) {
149
- console.warn('Invalid volume', volume);
150
- return;
151
- }
152
- this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
153
- if (clipPlayer.videoElement) {
154
- clipPlayer.volume = volume;
155
- setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
156
- }
157
- return clipPlayer;
158
- });
159
- }
160
- setVideoClipFit({ fit }) {
161
- // If there is a pending clip, this is latest to have been played so update its fit
162
- const clipToUpdate = this.pendingClip ?? this.activeClip ?? undefined;
163
- if (!clipToUpdate) {
164
- return;
165
- }
166
- this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
167
- if (clipPlayer.videoElement) {
168
- clipPlayer.videoElement.style.objectFit = fit;
169
- }
170
- return clipPlayer;
171
- });
172
- }
173
- handleStoppedClip(path) {
174
- if (!this.activeClip || this.activeClip.path !== path) {
175
- return;
176
- }
177
- const playId = this.activeClip.playId;
178
- // Once an ephemeral clip stops, cleanup and remove the player
179
- if (this.videoClipPlayers[this.activeClip.path]?.config.ephemeral) {
180
- this.unloadClip(path);
181
- }
182
- this.activeClip = undefined;
183
- this.updateVideoClipPlayer(path, (clipPlayer) => {
184
- clipPlayer.videoElement.pause();
185
- clipPlayer.videoElement.currentTime = 0;
186
- clipPlayer.videoElement.style.display = 'none';
187
- return clipPlayer;
188
- });
189
- this.notifyClipStateListeners(playId, path, 'stopped');
190
- }
191
- updateVideoClipPlayer(path, update) {
192
- if (this.videoClipPlayers[path]) {
193
- const newPlayer = update(this.videoClipPlayers[path]);
194
- if (newPlayer) {
195
- this.videoClipPlayers[path] = newPlayer;
196
- }
197
- else {
198
- delete this.videoClipPlayers[path];
199
- }
200
- this.notifyStateListeners();
201
- }
202
- }
203
- setAudioSink(sinkId) {
204
- for (const clipPlayer of Object.values(this.videoClipPlayers)) {
205
- setPlayerSinkId(clipPlayer, sinkId);
206
- }
207
- this.sinkId = sinkId;
208
- }
209
- updateConfig(newPaths) {
210
- const newVideoPaths = Object.fromEntries(Object.entries(newPaths).filter(([, { type }]) => type === 'video' || !type));
211
- const previousClipPlayers = this.videoClipPlayers;
212
- this.videoClipPlayers = (() => {
213
- const clipPlayers = { ...previousClipPlayers };
214
- const removedClips = Object.keys(previousClipPlayers).filter((previousPath) => !(previousPath in newVideoPaths));
215
- removedClips.forEach((path) => {
216
- if (this.activeClip?.path === path && previousClipPlayers[path]?.config.ephemeral === false) {
217
- this.updateVideoClipPlayer(path, (player) => {
218
- player.config = { ...player.config, ephemeral: true };
219
- return player;
220
- });
221
- }
222
- else {
223
- this.unloadClip(path);
224
- delete clipPlayers[path];
225
- }
226
- });
227
- const addedClips = Object.entries(newVideoPaths).filter(([newFile]) => !previousClipPlayers[newFile]);
228
- addedClips.forEach(([path, config]) => {
229
- clipPlayers[path] = this.createClipPlayer(path, { ...config, preload: preloadString(config.preload), ephemeral: false, fit: 'contain' });
230
- });
231
- const updatedClips = Object.entries(previousClipPlayers).filter(([previousPath]) => previousPath in newVideoPaths);
232
- updatedClips.forEach(([path, previousClipPlayer]) => {
233
- if (previousClipPlayer.config.preload !== newVideoPaths[path].preload) {
234
- this.updateVideoClipPlayer(path, (player) => {
235
- player.config = {
236
- ...player.config,
237
- preload: preloadString(newVideoPaths[path].preload),
238
- ephemeral: false,
239
- };
240
- player.videoElement.preload = player.config.preload;
241
- return player;
242
- });
243
- }
244
- });
245
- return clipPlayers;
246
- })();
247
- this.notifyStateListeners();
248
- }
249
- notifyStateListeners() {
250
- const VideoState = {
251
- globalVolume: this.globalVolume,
252
- isPlaying: this.activeClip ? !this.videoClipPlayers[this.activeClip.path].videoElement?.paused : false,
253
- clips: { ...this.videoClipPlayers },
254
- activeClip: this.activeClip
255
- ? {
256
- path: this.activeClip.path,
257
- state: !this.videoClipPlayers[this.activeClip.path].videoElement?.paused ? ActiveVideoClipState.Playing : ActiveVideoClipState.Paused,
258
- loop: this.videoClipPlayers[this.activeClip.path].videoElement?.loop ?? false,
259
- volume: this.videoClipPlayers[this.activeClip.path].videoElement?.muted
260
- ? 0
261
- : (this.videoClipPlayers[this.activeClip.path].videoElement?.volume ?? 0),
262
- }
263
- : undefined,
264
- };
265
- this.dispatchEvent('state', VideoState);
266
- }
267
- notifyClipStateListeners(playId, file, status) {
268
- this.dispatchEvent('videoClipState', { playId, mediaType: 'video', file, status });
269
- }
270
- // Type-safe wrapper around EventTarget
271
- addEventListener(type, listener, options) {
272
- this.eventTarget.addEventListener(type, listener, options);
273
- }
274
- removeEventListener(type, listener, options) {
275
- this.eventTarget.removeEventListener(type, listener, options);
276
- }
277
- dispatchEvent(type, detail) {
278
- this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
279
- }
280
- createVideoElement(path, config, { volume }) {
281
- const videoElement = document.createElement('video');
282
- videoElement.playsInline = true; // Required for iOS
283
- videoElement.src = this.cogsConnection.getAssetUrl(path);
284
- videoElement.autoplay = false;
285
- videoElement.loop = false;
286
- setVideoElementVolume(videoElement, volume * this.globalVolume);
287
- videoElement.preload = config.preload;
288
- videoElement.addEventListener('playing', () => {
289
- // If the clip is still the pending one when it actually start playing, then ensure it is in the correct state
290
- if (this.pendingClip?.path === path) {
291
- switch (this.pendingClip.actionOncePlaying) {
292
- case 'play': {
293
- // Continue playing, show the video element, and notify listeners
294
- videoElement.style.display = 'block';
295
- this.notifyClipStateListeners(this.pendingClip.playId, path, 'playing');
296
- break;
297
- }
298
- case 'pause': {
299
- // Pause playback, show the video element, and notify listeners
300
- videoElement.style.display = 'block';
301
- videoElement.pause();
302
- this.notifyClipStateListeners(this.pendingClip.playId, path, 'paused');
303
- break;
304
- }
305
- case 'stop': {
306
- // Pause playback, leave the video element hidden, and notify listeners
307
- videoElement.pause();
308
- this.notifyClipStateListeners(this.pendingClip.playId, path, 'stopped');
309
- break;
310
- }
311
- }
312
- // If there was a previously active clip, then stop it
313
- if (this.activeClip) {
314
- this.handleStoppedClip(this.activeClip.path);
315
- }
316
- this.activeClip = this.pendingClip;
317
- this.pendingClip = undefined;
318
- }
319
- else if (this.activeClip?.path === path) {
320
- // If we were the active clip then just notify listeners that we are now playing
321
- this.notifyClipStateListeners(this.activeClip.playId, path, 'playing');
322
- }
323
- else {
324
- // Otherwise it shouldn't be playing, like because another clip became pending before we loaded,
325
- // so we pause and don't show or notify listeners
326
- videoElement.pause();
327
- }
328
- });
329
- videoElement.addEventListener('ended', () => {
330
- // Ignore if there's a pending clip, as once that starts playing the active clip will be stopped
331
- // Also ignore if the video is set to loop
332
- if (!this.pendingClip && !videoElement.loop) {
333
- this.handleStoppedClip(path);
334
- }
335
- });
336
- videoElement.style.position = 'absolute';
337
- videoElement.style.top = '0';
338
- videoElement.style.left = '0';
339
- videoElement.style.width = '100%';
340
- videoElement.style.height = '100%';
341
- videoElement.style.objectFit = config.fit;
342
- videoElement.style.display = 'none';
343
- this.parentElement.appendChild(videoElement);
344
- return videoElement;
345
- }
346
- createClipPlayer(path, config) {
347
- const volume = 1;
348
- const player = {
349
- config,
350
- videoElement: this.createVideoElement(path, config, { volume }),
351
- volume,
352
- };
353
- setPlayerSinkId(player, this.sinkId);
354
- return player;
355
- }
356
- unloadClip(path) {
357
- if (this.activeClip?.path === path) {
358
- const playId = this.activeClip.playId;
359
- this.activeClip = undefined;
360
- this.notifyClipStateListeners(playId, path, 'stopped');
361
- }
362
- this.videoClipPlayers[path]?.videoElement.remove();
363
- this.updateVideoClipPlayer(path, () => null);
364
- }
365
- }
366
- function preloadString(preload) {
367
- return typeof preload === 'string' ? preload : preload ? 'auto' : 'none';
368
- }
369
- function setPlayerSinkId(player, sinkId) {
370
- if (sinkId === undefined) {
371
- return;
372
- }
373
- if (typeof player.videoElement.setSinkId === 'function') {
374
- player.videoElement.setSinkId(sinkId);
375
- }
376
- }
377
- /**
378
- * Set video volume
379
- *
380
- * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
381
- */
382
- function setVideoElementVolume(videoElement, volume) {
383
- videoElement.volume = volume;
384
- videoElement.muted = volume === 0;
385
- }
@@ -1,17 +0,0 @@
1
- import { AudioState } from '../types/MediaSchema';
2
- import { ClipManager } from './ClipManager';
3
- import { MediaPreloader } from './MediaPreloader';
4
- export declare class AudioManager extends ClipManager<AudioState> {
5
- private mediaPreloader;
6
- private audioElement?;
7
- private isSeeking;
8
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: AudioState, constructAssetURL: (file: string) => string, mediaPreloader: MediaPreloader);
9
- private updateAudioElement;
10
- /**
11
- * Helper function to seek to a specified time.
12
- * Works with the update loop to poll until seeked event has fired.
13
- */
14
- private seekTo;
15
- protected update(): void;
16
- destroy(): void;
17
- }
@@ -1,114 +0,0 @@
1
- import { defaultAudioOptions } from '../types/MediaSchema';
2
- import { getStateAtTime } from '../utils/getStateAtTime';
3
- import { ClipManager } from './ClipManager';
4
- const DEFAULT_AUDIO_POLLING = 1_000;
5
- const TARGET_SYNC_THRESHOLD_MS = 10; // If we're closer than this we're good enough
6
- const MAX_SYNC_THRESHOLD_MS = 1_000; // If we're further away than this, we'll seek instead
7
- const SEEK_LOOKAHEAD_MS = 200; // We won't seek ahead instantly, so lets seek ahead
8
- const MAX_PLAYBACK_RATE_ADJUSTMENT = 0.2;
9
- // We smoothly ramp playbackRate up and down
10
- const PLAYBACK_ADJUSTMENT_SMOOTHING = 0.5;
11
- function playbackSmoothing(deltaTime) {
12
- return Math.sign(deltaTime) * Math.pow(Math.abs(deltaTime) / MAX_SYNC_THRESHOLD_MS, PLAYBACK_ADJUSTMENT_SMOOTHING) * MAX_PLAYBACK_RATE_ADJUSTMENT;
13
- }
14
- export class AudioManager extends ClipManager {
15
- mediaPreloader;
16
- audioElement;
17
- isSeeking = false;
18
- constructor(surfaceElement, clipElement, state, constructAssetURL, mediaPreloader) {
19
- super(surfaceElement, clipElement, state, constructAssetURL);
20
- this.mediaPreloader = mediaPreloader;
21
- this.clipElement = clipElement;
22
- }
23
- updateAudioElement() {
24
- const element = this.mediaPreloader.getElement(this._state.file, 'audio');
25
- this.audioElement = element;
26
- this.clipElement.replaceChildren(this.audioElement);
27
- }
28
- /**
29
- * Helper function to seek to a specified time.
30
- * Works with the update loop to poll until seeked event has fired.
31
- */
32
- seekTo(time) {
33
- if (!this.audioElement)
34
- return;
35
- this.audioElement.addEventListener('seeked', () => {
36
- this.isSeeking = false;
37
- }, { once: true, passive: true });
38
- this.audioElement.currentTime = time / 1_000;
39
- }
40
- update() {
41
- // Update loop used to poll until seek finished
42
- if (this.isSeeking)
43
- return;
44
- this.delay = DEFAULT_AUDIO_POLLING;
45
- // Does the <audio /> element need adding/removing?
46
- const currentState = getStateAtTime(this._state, Date.now());
47
- if (currentState) {
48
- if (!this.audioElement || !this.isConnected(this.audioElement)) {
49
- this.updateAudioElement();
50
- }
51
- }
52
- else {
53
- this.destroy();
54
- }
55
- if (!currentState || !this.audioElement)
56
- return;
57
- const { t, rate, volume } = { ...defaultAudioOptions, ...currentState };
58
- // this.videoElement.src will be a fully qualified URL
59
- const assetURL = this.constructAssetURL(this._state.file);
60
- if (!this.audioElement.src.includes(assetURL)) {
61
- this.updateAudioElement();
62
- }
63
- if (this.audioElement.volume !== volume) {
64
- this.audioElement.volume = volume;
65
- }
66
- // Should the element be playing?
67
- if (this.audioElement.paused && rate > 0) {
68
- this.audioElement.play().catch(() => {
69
- // Do nothing - this will be retried in the next loop
70
- });
71
- }
72
- const currentTime = this.audioElement.currentTime * 1000;
73
- const deltaTime = currentTime - t;
74
- const deltaTimeAbs = Math.abs(deltaTime);
75
- this.delay = 100;
76
- switch (true) {
77
- case deltaTimeAbs <= TARGET_SYNC_THRESHOLD_MS:
78
- // We are on course:
79
- // - The audio is within accepted latency of the server time
80
- // - The playback rate is aligned with the server rate
81
- if (this.audioElement.playbackRate !== rate) {
82
- this.audioElement.playbackRate = rate;
83
- }
84
- break;
85
- case rate > 0 && deltaTimeAbs > TARGET_SYNC_THRESHOLD_MS && deltaTimeAbs <= MAX_SYNC_THRESHOLD_MS: {
86
- // We are close, we can smoothly adjust with playbackRate:
87
- // - The audio must be playing
88
- // - We must be close in time to the server time
89
- const playbackRateAdjustment = playbackSmoothing(deltaTime);
90
- const adjustedPlaybackRate = Math.max(0, rate - playbackRateAdjustment);
91
- if (this.audioElement.playbackRate !== adjustedPlaybackRate) {
92
- this.audioElement.playbackRate = adjustedPlaybackRate;
93
- }
94
- break;
95
- }
96
- default: {
97
- // We cannot smoothly recover:
98
- // - We seek just ahead of server time
99
- if (this.audioElement.playbackRate !== rate) {
100
- this.audioElement.playbackRate = rate;
101
- }
102
- // delay to poll until seeked
103
- this.delay = 10;
104
- this.seekTo(t + rate * (SEEK_LOOKAHEAD_MS / 1000));
105
- break;
106
- }
107
- }
108
- }
109
- destroy() {
110
- if (this.audioElement) {
111
- this.audioElement.remove();
112
- }
113
- }
114
- }
@@ -1,23 +0,0 @@
1
- import { MediaClipState } from '../types/MediaSchema';
2
- /**
3
- * Each instance of a ClipManager is responsible for displaying
4
- * an image/audio/video clip in the correct state.
5
- */
6
- export declare abstract class ClipManager<T extends MediaClipState> {
7
- private surfaceElement;
8
- protected clipElement: HTMLElement;
9
- protected constructAssetURL: (file: string) => string;
10
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: T, constructAssetURL: (file: string) => string);
11
- /**
12
- * This is the delay to be used in the update loop.
13
- * It is intended to be dynamic for each loop.
14
- */
15
- protected delay: number;
16
- protected abstract update(): void;
17
- abstract destroy(): void;
18
- isConnected(element?: HTMLElement): boolean;
19
- protected _state: T;
20
- setState(newState: T): void;
21
- private timeout;
22
- private loop;
23
- }
@@ -1,60 +0,0 @@
1
- const DEFAULT_DELAY = 1_000;
2
- /**
3
- * Each instance of a ClipManager is responsible for displaying
4
- * an image/audio/video clip in the correct state.
5
- */
6
- export class ClipManager {
7
- surfaceElement;
8
- clipElement;
9
- constructAssetURL;
10
- constructor(surfaceElement, clipElement, state, constructAssetURL) {
11
- this.surfaceElement = surfaceElement;
12
- this.clipElement = clipElement;
13
- this.constructAssetURL = constructAssetURL;
14
- this._state = state;
15
- // Allow the class to be constructed, then call the loop
16
- setTimeout(this.loop);
17
- }
18
- /**
19
- * This is the delay to be used in the update loop.
20
- * It is intended to be dynamic for each loop.
21
- */
22
- delay = DEFAULT_DELAY;
23
- isConnected(element) {
24
- if (!this.surfaceElement) {
25
- return false;
26
- }
27
- if (!this.clipElement) {
28
- return false;
29
- }
30
- if (!this.surfaceElement.contains(this.clipElement)) {
31
- return false;
32
- }
33
- if (element) {
34
- if (!this.clipElement.contains(element))
35
- return false;
36
- }
37
- return true;
38
- }
39
- _state;
40
- setState(newState) {
41
- if (this._state.file !== newState.file) {
42
- throw new Error(`Cannot change from ${this._state.file} to ${newState.file}. Create a new clip instead.`);
43
- }
44
- this._state = newState;
45
- clearTimeout(this.timeout);
46
- this.loop();
47
- }
48
- timeout;
49
- loop = async () => {
50
- if (this.isConnected()) {
51
- this.update();
52
- if (isFinite(this.delay)) {
53
- this.timeout = setTimeout(this.loop, this.delay);
54
- }
55
- }
56
- else {
57
- this.destroy();
58
- }
59
- };
60
- }
@@ -1,9 +0,0 @@
1
- import { ImageState } from '../types/MediaSchema';
2
- import { ClipManager } from './ClipManager';
3
- export declare class ImageManager extends ClipManager<ImageState> {
4
- private imageElement?;
5
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: ImageState, constructAssetURL: (file: string) => string);
6
- private updateImageElement;
7
- protected update(): void;
8
- destroy(): void;
9
- }
@@ -1,54 +0,0 @@
1
- import { defaultImageOptions } from '../types/MediaSchema';
2
- import { getStateAtTime } from '../utils/getStateAtTime';
3
- import { ClipManager } from './ClipManager';
4
- export class ImageManager extends ClipManager {
5
- imageElement;
6
- constructor(surfaceElement, clipElement, state, constructAssetURL) {
7
- super(surfaceElement, clipElement, state, constructAssetURL);
8
- this.clipElement = clipElement;
9
- }
10
- updateImageElement() {
11
- this.imageElement = document.createElement('img');
12
- this.clipElement.replaceChildren(this.imageElement);
13
- this.imageElement.style.position = 'absolute';
14
- this.imageElement.style.height = '100%';
15
- this.imageElement.style.widows = '100%';
16
- }
17
- update() {
18
- const currentState = getStateAtTime(this._state, Date.now());
19
- // Does the <img /> element need adding/removing?
20
- if (currentState) {
21
- if (!this.imageElement || !this.isConnected(this.imageElement)) {
22
- this.updateImageElement();
23
- }
24
- }
25
- else {
26
- this.imageElement?.remove();
27
- this.imageElement = undefined;
28
- }
29
- if (!this.imageElement || !currentState)
30
- return;
31
- // this.videoElement.src will be a fully qualified URL
32
- const assetURL = this.constructAssetURL(this._state.file);
33
- if (!this.imageElement.src.includes(assetURL)) {
34
- this.imageElement.src = assetURL;
35
- }
36
- if (this.imageElement.style.objectFit !== this._state.fit) {
37
- this.imageElement.style.objectFit = this._state.fit;
38
- }
39
- if (parseFloat(this.imageElement.style.opacity) !== currentState.opacity) {
40
- this.imageElement.style.opacity = String(currentState.opacity ?? defaultImageOptions.opacity);
41
- }
42
- const z = Math.round(currentState.zIndex ?? defaultImageOptions.zIndex);
43
- if (parseInt(this.imageElement.style.zIndex) !== z) {
44
- this.imageElement.style.zIndex = String(z);
45
- }
46
- const { opacity } = currentState;
47
- if (typeof opacity === 'string' && opacity !== this.imageElement.style.opacity) {
48
- this.imageElement.style.opacity = opacity;
49
- }
50
- }
51
- destroy() {
52
- this.imageElement?.remove();
53
- }
54
- }
@@ -1,19 +0,0 @@
1
- import { VideoState } from '../types/MediaSchema';
2
- import { ClipManager } from './ClipManager';
3
- import { MediaPreloader } from './MediaPreloader';
4
- export declare class VideoManager extends ClipManager<VideoState> {
5
- private mediaPreloader;
6
- private videoElement?;
7
- private isSeeking;
8
- private timeToIntercept;
9
- constructor(surfaceElement: HTMLElement, clipElement: HTMLElement, state: VideoState, constructAssetURL: (file: string) => string, mediaPreloader: MediaPreloader);
10
- private updateVideoElement;
11
- private get videoDuration();
12
- /**
13
- * Helper function to seek to a specified time.
14
- * Works with the update loop to poll until seeked event has fired.
15
- */
16
- private seekTo;
17
- protected update(): void;
18
- destroy(): void;
19
- }