@clockworkdog/cogs-client 3.0.0-alpha.15 → 3.0.0-alpha.16

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.d.ts CHANGED
@@ -1,15 +1,10 @@
1
1
  export { default as CogsConnection } from './CogsConnection';
2
2
  export * from './CogsConnection';
3
3
  export type { default as CogsClientMessage, MediaClientConfig } from './types/CogsClientMessage';
4
- export type { default as MediaClipStateMessage } from './types/MediaClipStateMessage';
5
4
  export type { default as ShowPhase } from './types/ShowPhase';
6
- export type { default as MediaObjectFit } from './types/MediaObjectFit';
7
5
  export * as MediaSchema from './types/MediaSchema';
8
- export { default as CogsAudioPlayer } from './AudioPlayer';
9
- export { default as CogsVideoPlayer } from './VideoPlayer';
10
6
  export { SurfaceManager } from './state-based/SurfaceManager';
11
7
  export { MediaPreloader } from './state-based/MediaPreloader';
12
- export * from './types/AudioState';
13
8
  export { assetUrl, preloadUrl } from './utils/urls';
14
9
  export { getStateAtTime } from './utils/getStateAtTime';
15
10
  export * from './types/CogsPluginManifest';
package/dist/index.js CHANGED
@@ -1,11 +1,8 @@
1
1
  export { default as CogsConnection } from './CogsConnection';
2
2
  export * from './CogsConnection';
3
3
  export * as MediaSchema from './types/MediaSchema';
4
- export { default as CogsAudioPlayer } from './AudioPlayer';
5
- export { default as CogsVideoPlayer } from './VideoPlayer';
6
4
  export { SurfaceManager } from './state-based/SurfaceManager';
7
5
  export { MediaPreloader } from './state-based/MediaPreloader';
8
- export * from './types/AudioState';
9
6
  export { assetUrl, preloadUrl } from './utils/urls';
10
7
  export { getStateAtTime } from './utils/getStateAtTime';
11
8
  export * from './types/CogsPluginManifest';
@@ -9,6 +9,6 @@ export declare class MediaPreloader {
9
9
  };
10
10
  setState(newState: MediaClientConfig['files']): void;
11
11
  private update;
12
- getElement(file: string, type: 'audio' | 'video'): HTMLAudioElement;
12
+ getElement(file: string, type: 'audio' | 'video'): HTMLMediaElement;
13
13
  releaseElement(resource: string | HTMLElement): void;
14
14
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Connect to COGS to build a custom Media Master",
4
4
  "author": "Clockwork Dog <info@clockwork.dog>",
5
5
  "homepage": "https://github.com/clockwork-dog/cogs-sdk/tree/main/packages/javascript",
6
- "version": "3.0.0-alpha.15",
6
+ "version": "3.0.0-alpha.16",
7
7
  "keywords": [],
8
8
  "license": "MIT",
9
9
  "repository": {
@@ -37,15 +37,13 @@
37
37
  "cy:generate": "cypress run --e2e"
38
38
  },
39
39
  "dependencies": {
40
- "@clockworkdog/timesync": "^3.0.0-alpha.15",
41
- "howler": "clockwork-dog/howler.js#fix-looping-clips",
40
+ "@clockworkdog/timesync": "^3.0.0-alpha.16",
42
41
  "reconnecting-websocket": "^4.4.0",
43
42
  "zod": "^4.1.13"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@cypress/mount-utils": "^4.1.2",
47
46
  "@eslint/js": "^9.17.0",
48
- "@types/howler": "2.2.12",
49
47
  "@types/jsdom": "^27",
50
48
  "@types/node": "^22.10.2",
51
49
  "cypress": "^14.5.4",
@@ -1,49 +0,0 @@
1
- import CogsConnection from './CogsConnection';
2
- import { AudioState } from './types/AudioState';
3
- import MediaClipStateMessage from './types/MediaClipStateMessage';
4
- type EventTypes = {
5
- state: AudioState;
6
- audioClipState: MediaClipStateMessage;
7
- };
8
- export default class AudioPlayer {
9
- private cogsConnection;
10
- private eventTarget;
11
- private globalVolume;
12
- private audioClipPlayers;
13
- private sinkId;
14
- constructor(cogsConnection: CogsConnection<any>);
15
- setGlobalVolume(volume: number): void;
16
- playAudioClip(path: string, { playId, volume, fade, loop }: {
17
- playId: string;
18
- volume: number;
19
- fade?: number;
20
- loop: boolean;
21
- }): void;
22
- pauseAudioClip(path: string, { fade }: {
23
- fade?: number;
24
- }, onlySoundId?: number, allowIfPauseRequested?: boolean): void;
25
- stopAudioClip(path: string, { fade }: {
26
- fade?: number;
27
- }, onlySoundId?: number, allowIfStopRequested?: boolean): void;
28
- stopAllAudioClips(options: {
29
- fade?: number;
30
- }): void;
31
- setAudioClipVolume(path: string, { volume, fade }: {
32
- volume: number;
33
- fade?: number;
34
- }): void;
35
- private handleStoppedClip;
36
- private updateActiveAudioClip;
37
- private updateAudioClipPlayer;
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 createPlayer;
46
- private createClip;
47
- private updatedClip;
48
- }
49
- export {};
@@ -1,474 +0,0 @@
1
- // eslint-disable-next-line @typescript-eslint/triple-slash-reference
2
- /// <reference path="./types/howler.d.ts" />
3
- import { Howl, Howler } from 'howler/dist/howler.core.min.js';
4
- const DEBUG = false;
5
- // Check an iOS-only property (See https://developer.mozilla.org/en-US/docs/Web/API/Navigator#non-standard_properties)
6
- const IS_IOS = typeof navigator !== 'undefined' && typeof navigator.standalone !== 'undefined';
7
- export default class AudioPlayer {
8
- cogsConnection;
9
- eventTarget = new EventTarget();
10
- globalVolume = 1;
11
- audioClipPlayers = {};
12
- sinkId = '';
13
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- constructor(cogsConnection) {
15
- this.cogsConnection = cogsConnection;
16
- // Send the current status of each clip to COGS
17
- this.addEventListener('audioClipState', ({ detail }) => {
18
- cogsConnection.sendMediaClipState(detail);
19
- });
20
- // Listen for audio control messages
21
- cogsConnection.addEventListener('message', ({ message }) => {
22
- switch (message.type) {
23
- case 'media_config_update':
24
- if (this.globalVolume !== message.globalVolume) {
25
- this.setGlobalVolume(message.globalVolume);
26
- }
27
- if (message.audioOutput !== undefined) {
28
- const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
29
- this.setAudioSink(sinkId ?? '');
30
- }
31
- this.updateConfig(message.files);
32
- break;
33
- case 'audio_play':
34
- this.playAudioClip(message.file, {
35
- playId: message.playId,
36
- volume: message.volume,
37
- loop: Boolean(message.loop),
38
- fade: message.fade,
39
- });
40
- break;
41
- case 'audio_pause':
42
- this.pauseAudioClip(message.file, { fade: message.fade });
43
- break;
44
- case 'audio_stop':
45
- if (message.file) {
46
- this.stopAudioClip(message.file, { fade: message.fade });
47
- }
48
- else {
49
- this.stopAllAudioClips({ fade: message.fade });
50
- }
51
- break;
52
- case 'audio_set_clip_volume':
53
- this.setAudioClipVolume(message.file, { volume: message.volume, fade: message.fade });
54
- break;
55
- }
56
- });
57
- // On connection, send the current playing state of all clips
58
- // (Usually empty unless websocket is reconnecting)
59
- const sendInitialClipStates = () => {
60
- const files = Object.entries(this.audioClipPlayers).map(([file, player]) => {
61
- const activeClips = Object.values(player.activeClips);
62
- const status = activeClips.some(({ state }) => state.type === 'playing' ||
63
- state.type === 'pausing' ||
64
- state.type === 'stopping' ||
65
- state.type === 'play_requested' ||
66
- state.type === 'pause_requested' ||
67
- state.type === 'stop_requested')
68
- ? 'playing'
69
- : activeClips.some(({ state }) => state.type === 'paused')
70
- ? 'paused'
71
- : 'stopped';
72
- return [file, status];
73
- });
74
- cogsConnection.sendInitialMediaClipStates({ mediaType: 'audio', files });
75
- };
76
- cogsConnection.addEventListener('open', sendInitialClipStates);
77
- sendInitialClipStates();
78
- }
79
- setGlobalVolume(volume) {
80
- this.globalVolume = volume;
81
- Howler.volume(volume);
82
- this.notifyStateListeners();
83
- }
84
- playAudioClip(path, { playId, volume, fade, loop }) {
85
- log('Playing clip', { path });
86
- if (!(path in this.audioClipPlayers)) {
87
- log('Creating ephemeral clip', { path });
88
- this.audioClipPlayers[path] = this.createClip(path, { preload: false, ephemeral: true });
89
- }
90
- this.updateAudioClipPlayer(path, (clipPlayer) => {
91
- // Paused clips need to be played again
92
- const pausedSoundIds = Object.entries(clipPlayer.activeClips)
93
- .filter(([, { state }]) => state.type === 'paused')
94
- .map(([id]) => parseInt(id));
95
- const pausingSoundIds = Object.entries(clipPlayer.activeClips)
96
- .filter(([, { state }]) => state.type === 'pausing')
97
- .map(([id]) => parseInt(id));
98
- pausedSoundIds.forEach((soundId) => {
99
- log('Resuming paused clip', soundId);
100
- clipPlayer.player.play(soundId);
101
- });
102
- // Clips with pause requested no longer need to pause, they can continue playing now
103
- const pauseRequestedSoundIds = Object.entries(clipPlayer.activeClips)
104
- .filter(([, { state }]) => state.type === 'pause_requested')
105
- .map(([id]) => parseInt(id));
106
- // If no currently paused/pausing/pause_requested clips, play a new clip
107
- const newSoundIds = pausedSoundIds.length > 0 || pausingSoundIds.length > 0 || pauseRequestedSoundIds.length > 0 ? [] : [clipPlayer.player.play()];
108
- // Pausing clips are technically currently playing as far as Howler is concerned
109
- pausingSoundIds.forEach((soundId) => {
110
- log('Stopping fade and resuming pausing clip', soundId);
111
- // Stop the fade callback
112
- clipPlayer.player.off('fade', undefined, soundId);
113
- // Set loop property
114
- clipPlayer.player.loop(loop, soundId);
115
- // Update state to 'playing'
116
- this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'playing' } }));
117
- // Set volume, or start a new fade
118
- if (isFadeValid(fade)) {
119
- // Start fade when clip starts
120
- fadeAudioPlayerVolume(clipPlayer.player, volume, fade * 1000, soundId);
121
- }
122
- else {
123
- setAudioPlayerVolume(clipPlayer.player, volume, soundId);
124
- }
125
- });
126
- // paused and pause_requested clips treated the same, they should have their properties
127
- // updated with the latest play action's properties
128
- [...pausedSoundIds, ...pauseRequestedSoundIds, ...newSoundIds].forEach((soundId) => {
129
- clipPlayer.player.loop(loop, soundId);
130
- // Cleanup any old callbacks first
131
- clipPlayer.player.off('play', undefined, soundId);
132
- clipPlayer.player.off('pause', undefined, soundId);
133
- clipPlayer.player.off('fade', undefined, soundId);
134
- clipPlayer.player.off('end', undefined, soundId);
135
- clipPlayer.player.off('stop', undefined, soundId);
136
- // Non-preloaded clips don't yet have an HTML audio node
137
- // so we need to set the audio output when it's playing
138
- clipPlayer.player.once('play', () => {
139
- setPlayerSinkId(clipPlayer.player, this.sinkId);
140
- });
141
- clipPlayer.player.once('stop', () => this.handleStoppedClip(path, playId, soundId), soundId);
142
- // Looping clips fire the 'end' callback on every loop
143
- clipPlayer.player.on('end', () => {
144
- if (!clipPlayer.activeClips[soundId]?.loop) {
145
- this.handleStoppedClip(path, playId, soundId);
146
- }
147
- }, soundId);
148
- const activeClip = {
149
- playId,
150
- state: { type: 'play_requested' },
151
- loop,
152
- volume,
153
- };
154
- log('CLIP -> play_requested', soundId);
155
- // Once clip starts, check if it should actually be paused or stopped
156
- // If not, then update state to 'playing'
157
- clipPlayer.player.once('play', () => {
158
- const clipState = clipPlayer.activeClips[soundId]?.state;
159
- if (clipState?.type === 'pause_requested') {
160
- log('Clip started playing but should be paused', { path, soundId });
161
- this.pauseAudioClip(path, { fade: clipState.fade }, soundId, true);
162
- }
163
- else if (clipState?.type === 'stop_requested') {
164
- log('Clip started playing but should be stopped', { path, soundId });
165
- this.stopAudioClip(path, { fade: clipState.fade }, soundId, true);
166
- }
167
- else {
168
- log('CLIP -> playing', soundId);
169
- this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'playing' } }));
170
- }
171
- }, soundId);
172
- // To fade or to no fade?
173
- if (isFadeValid(fade)) {
174
- // Start fade when clip starts
175
- clipPlayer.player.volume(0, soundId);
176
- clipPlayer.player.mute(false, soundId);
177
- clipPlayer.player.once('play', () => {
178
- fadeAudioPlayerVolume(clipPlayer.player, volume, fade * 1000, soundId);
179
- }, soundId);
180
- }
181
- else {
182
- setAudioPlayerVolume(clipPlayer.player, volume, soundId);
183
- }
184
- // Track new/updated active clip
185
- clipPlayer.activeClips = { ...clipPlayer.activeClips, [soundId]: activeClip };
186
- });
187
- return clipPlayer;
188
- });
189
- this.notifyClipStateListeners(playId, path, 'playing');
190
- }
191
- pauseAudioClip(path, { fade }, onlySoundId, allowIfPauseRequested) {
192
- // No active clips to pause
193
- if (Object.keys(this.audioClipPlayers[path]?.activeClips ?? {}).length === 0) {
194
- return;
195
- }
196
- this.updateAudioClipPlayer(path, (clipPlayer) => {
197
- clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
198
- const soundId = parseInt(soundIdStr);
199
- // If onlySoundId specified, only update that clip
200
- if (onlySoundId === undefined || onlySoundId === soundId) {
201
- if ((allowIfPauseRequested && clip.state.type === 'pause_requested') || clip.state.type === 'playing' || clip.state.type === 'pausing') {
202
- if (isFadeValid(fade)) {
203
- // Fade then pause
204
- clipPlayer.player.once('fade', (soundId) => {
205
- clipPlayer.player.pause(soundId);
206
- log('CLIP -> paused (after fade)', soundId);
207
- this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'paused' } }));
208
- this.notifyClipStateListeners(clip.playId, path, 'paused');
209
- }, soundId);
210
- fadeAudioPlayerVolume(clipPlayer.player, 0, fade * 1000, soundId);
211
- log('CLIP -> pausing', soundId);
212
- clip.state = { type: 'pausing' };
213
- }
214
- else {
215
- // Pause now
216
- clipPlayer.player.pause(soundId);
217
- log('CLIP -> paused', soundId);
218
- clip.state = { type: 'paused' };
219
- this.notifyClipStateListeners(clip.playId, path, 'paused');
220
- }
221
- }
222
- // Clip hasn't started playing yet, or has already had pause_requested (but fade may have changed so update here)
223
- else if (clip.state.type === 'play_requested' || clip.state.type === 'pause_requested') {
224
- log('CLIP -> pause_requested', soundId);
225
- clip.state = { type: 'pause_requested', fade };
226
- }
227
- }
228
- return [soundIdStr, clip];
229
- }));
230
- return clipPlayer;
231
- });
232
- }
233
- stopAudioClip(path, { fade }, onlySoundId, allowIfStopRequested) {
234
- log('Stop audio clip', { activeClips: this.audioClipPlayers[path]?.activeClips });
235
- // No active clips to stop
236
- if (Object.keys(this.audioClipPlayers[path]?.activeClips ?? {}).length === 0) {
237
- return;
238
- }
239
- this.updateAudioClipPlayer(path, (clipPlayer) => {
240
- clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
241
- const soundId = parseInt(soundIdStr);
242
- // If onlySoundId specified, only update that clip
243
- if (onlySoundId === undefined || onlySoundId === soundId) {
244
- if ((allowIfStopRequested && clip.state.type === 'stop_requested') ||
245
- clip.state.type === 'playing' ||
246
- clip.state.type === 'pausing' ||
247
- clip.state.type === 'paused' ||
248
- clip.state.type === 'stopping') {
249
- if (isFadeValid(fade) && clip.state.type !== 'paused') {
250
- // Cleanup any old fade callbacks first
251
- // TODO: Remove cast once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59411 is merged
252
- clipPlayer.player.off('fade', soundId);
253
- fadeAudioPlayerVolume(clipPlayer.player, 0, fade * 1000, soundId);
254
- // Set callback after starting new fade, otherwise it will fire straight away as the previous fade is cancelled
255
- clipPlayer.player.once('fade', (soundId) => {
256
- clipPlayer.player.loop(false, soundId);
257
- clipPlayer.player.stop(soundId);
258
- }, soundId);
259
- log('CLIP -> stopping', soundId);
260
- clip.state = { type: 'stopping' };
261
- }
262
- else {
263
- log('Stop clip', soundId);
264
- clipPlayer.player.loop(false, soundId);
265
- clipPlayer.player.stop(soundId);
266
- }
267
- }
268
- // Clip hasn't started playing yet, or has already had stop_requested (but fade may have changed so update here)
269
- // or has pause_requested, but stop takes precedence
270
- else if (clip.state.type === 'play_requested' || clip.state.type === 'pause_requested' || clip.state.type === 'stop_requested') {
271
- log("Trying to stop clip which hasn't started playing yet", { path, soundId });
272
- log('CLIP -> stop_requested', soundId);
273
- clip.state = { type: 'stop_requested', fade };
274
- }
275
- }
276
- return [soundIdStr, clip];
277
- }));
278
- return clipPlayer;
279
- });
280
- }
281
- stopAllAudioClips(options) {
282
- log('Stopping all clips');
283
- Object.keys(this.audioClipPlayers).forEach((path) => {
284
- this.stopAudioClip(path, options);
285
- });
286
- }
287
- setAudioClipVolume(path, { volume, fade }) {
288
- if (!(volume >= 0 && volume <= 1)) {
289
- console.warn('Invalid volume', volume);
290
- return;
291
- }
292
- // No active clips to set volume for
293
- if (Object.keys(this.audioClipPlayers[path]?.activeClips ?? {}).length === 0) {
294
- return;
295
- }
296
- this.updateAudioClipPlayer(path, (clipPlayer) => {
297
- clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
298
- // Ignored for pausing/stopping instances
299
- if (clip.state.type !== 'pausing' && clip.state.type !== 'stopping') {
300
- const soundId = parseInt(soundIdStr);
301
- if (isFadeValid(fade)) {
302
- fadeAudioPlayerVolume(clipPlayer.player, volume, fade * 1000, soundId);
303
- }
304
- else {
305
- setAudioPlayerVolume(clipPlayer.player, volume, soundId);
306
- }
307
- return [soundIdStr, { ...clip, volume }];
308
- }
309
- else {
310
- return [soundIdStr, clip];
311
- }
312
- }));
313
- return clipPlayer;
314
- });
315
- }
316
- handleStoppedClip(path, playId, soundId) {
317
- this.updateAudioClipPlayer(path, (clipPlayer) => {
318
- delete clipPlayer.activeClips[soundId];
319
- return clipPlayer;
320
- });
321
- this.notifyClipStateListeners(playId, path, 'stopped');
322
- }
323
- updateActiveAudioClip(path, soundId, update) {
324
- this.updateAudioClipPlayer(path, (clipPlayer) => soundId in clipPlayer.activeClips
325
- ? { ...clipPlayer, activeClips: { ...clipPlayer.activeClips, [soundId]: update(clipPlayer.activeClips[soundId]) } }
326
- : clipPlayer);
327
- }
328
- updateAudioClipPlayer(path, update) {
329
- if (path in this.audioClipPlayers) {
330
- this.audioClipPlayers = { ...this.audioClipPlayers, [path]: update(this.audioClipPlayers[path]) };
331
- }
332
- // Once last instance of an ephemeral clip is removed, cleanup and remove the player
333
- const clipPlayer = this.audioClipPlayers[path];
334
- if (clipPlayer && Object.keys(clipPlayer.activeClips ?? {}).length === 0 && clipPlayer.config.ephemeral) {
335
- clipPlayer.player.unload();
336
- delete this.audioClipPlayers[path];
337
- }
338
- this.notifyStateListeners();
339
- }
340
- setAudioSink(sinkId) {
341
- log(`Setting sink ID for all clips:`, sinkId);
342
- for (const clipPlayer of Object.values(this.audioClipPlayers)) {
343
- setPlayerSinkId(clipPlayer.player, sinkId);
344
- }
345
- this.sinkId = sinkId;
346
- }
347
- updateConfig(newFiles) {
348
- const newAudioFiles = Object.fromEntries(Object.entries(newFiles).filter((file) => {
349
- const type = file[1].type;
350
- // COGS 4.6 did not send a `type` but only reported audio files
351
- // so we assume audio if no `type` is given
352
- return type === 'audio' || !type;
353
- }));
354
- const previousClipPlayers = this.audioClipPlayers;
355
- this.audioClipPlayers = (() => {
356
- const clipPlayers = { ...previousClipPlayers };
357
- const removedClips = Object.keys(previousClipPlayers).filter((previousFile) => !(previousFile in newAudioFiles) && !previousClipPlayers[previousFile].config.ephemeral);
358
- removedClips.forEach((file) => {
359
- const player = previousClipPlayers[file].player;
360
- player.unload();
361
- delete clipPlayers[file];
362
- });
363
- const addedClips = Object.entries(newAudioFiles).filter(([newfile]) => !previousClipPlayers[newfile]);
364
- addedClips.forEach(([path, config]) => {
365
- clipPlayers[path] = this.createClip(path, { ...config, ephemeral: false });
366
- });
367
- const updatedClips = Object.keys(previousClipPlayers).filter((previousFile) => previousFile in newAudioFiles);
368
- updatedClips.forEach((path) => {
369
- clipPlayers[path] = this.updatedClip(path, clipPlayers[path], { ...newAudioFiles[path], ephemeral: false });
370
- });
371
- return clipPlayers;
372
- })();
373
- this.notifyStateListeners();
374
- }
375
- notifyStateListeners() {
376
- const clips = Object.entries(this.audioClipPlayers).reduce((clips, [path, clipPlayer]) => {
377
- clips[path] = {
378
- config: { preload: clipPlayer.config.preload, ephemeral: clipPlayer.config.ephemeral },
379
- activeClips: clipPlayer.activeClips,
380
- };
381
- return clips;
382
- }, {});
383
- const isPlaying = Object.values(this.audioClipPlayers).some(({ activeClips }) => Object.values(activeClips).some((clip) => clip.state.type === 'playing' || clip.state.type === 'pausing' || clip.state.type === 'stopping'));
384
- const audioState = {
385
- globalVolume: this.globalVolume,
386
- isPlaying,
387
- clips,
388
- };
389
- this.dispatchEvent('state', audioState);
390
- }
391
- notifyClipStateListeners(playId, file, status) {
392
- this.dispatchEvent('audioClipState', { mediaType: 'audio', playId, file, status });
393
- }
394
- // Type-safe wrapper around EventTarget
395
- addEventListener(type, listener, options) {
396
- this.eventTarget.addEventListener(type, listener, options);
397
- }
398
- removeEventListener(type, listener, options) {
399
- this.eventTarget.removeEventListener(type, listener, options);
400
- }
401
- dispatchEvent(type, detail) {
402
- this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
403
- }
404
- createPlayer(path, config) {
405
- const player = new Howl({
406
- src: this.cogsConnection.getAssetUrl(path),
407
- autoplay: false,
408
- loop: false,
409
- volume: 1,
410
- html5: true,
411
- preload: config.preload,
412
- });
413
- setPlayerSinkId(player, this.sinkId);
414
- return player;
415
- }
416
- createClip(file, config) {
417
- return {
418
- config,
419
- player: this.createPlayer(file, config),
420
- activeClips: {},
421
- };
422
- }
423
- updatedClip(clipPath, previousClip, newConfig) {
424
- const clip = { ...previousClip, config: newConfig };
425
- if (previousClip.config.preload !== newConfig.preload) {
426
- clip.player.unload();
427
- clip.player = this.createPlayer(clipPath, newConfig);
428
- }
429
- return clip;
430
- }
431
- }
432
- function log(...data) {
433
- if (DEBUG) {
434
- console.log(...data);
435
- }
436
- }
437
- /**
438
- * @returns `true` if this is this a valid fade duration. Always returns `false` on iOS
439
- */
440
- function isFadeValid(fade) {
441
- return !IS_IOS && typeof fade === 'number' && !isNaN(fade) && fade > 0;
442
- }
443
- function setPlayerSinkId(player, sinkId) {
444
- if (sinkId === undefined) {
445
- return;
446
- }
447
- if (player._html5) {
448
- player._sounds?.forEach((sound) => {
449
- sound._node?.setSinkId?.(sinkId);
450
- });
451
- }
452
- else {
453
- // TODO: handle web audio
454
- console.warn('Cannot set sink ID: web audio not supported', player);
455
- }
456
- }
457
- /**
458
- * Set audio volume
459
- *
460
- * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
461
- */
462
- function setAudioPlayerVolume(howl, volume, soundId) {
463
- log('Setting volume', volume, soundId);
464
- howl.volume(volume, soundId);
465
- howl.mute(volume === 0, soundId);
466
- }
467
- /**
468
- * Fade to audio volume
469
- *
470
- * Note: This doesn't work on iOS (volume is read-only)
471
- */
472
- function fadeAudioPlayerVolume(howl, volume, fade, soundId) {
473
- howl.fade(howl.volume(soundId), volume, fade, soundId);
474
- }
@@ -1,49 +0,0 @@
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 {};