@clockworkdog/cogs-client 3.0.0-alpha.0 → 3.0.0-alpha.2

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