@clockworkdog/cogs-client 0.3.1

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/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # COGS Client library
2
+
3
+ Create content for your COGS Media Master
4
+
5
+ ## [Documentation](https://clockwork-dog.github.io/cogs-client-lib/)
6
+
7
+ ## Add to your project
8
+
9
+ ### Browser
10
+
11
+ ```html
12
+ <script src="https://unpkg.com/@clockworkdog/cogs-client@1"></script>
13
+ ```
14
+
15
+ ### NPM
16
+
17
+ ```shell
18
+ npm install --save @clockworkdog/cogs-client
19
+ ```
20
+
21
+ ### Yarn
22
+
23
+ ```shell
24
+ yarn add @clockworkdog/cogs-client
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Import the library
30
+
31
+ #### Browser
32
+
33
+ ```js
34
+ const { CogsConnection, CogsAudioPlayer } = COGS;
35
+ ```
36
+
37
+ #### Javascript
38
+
39
+ ```js
40
+ const { CogsConnection, CogsAudioPlayer } = require('@clockworkdog/cogs-client');
41
+ ```
42
+
43
+ #### Typesript / ES6
44
+
45
+ ```ts
46
+ import { CogsConnection, CogsAudioPlayer } from '@clockworkdog/cogs-client';
47
+ ```
48
+
49
+ ### Connect to COGS
50
+
51
+ ```ts
52
+ let connected = false;
53
+
54
+ const cogsConnection = new CogsConnection();
55
+ cogsConnection.addEventListener('open', () => {
56
+ connected = true;
57
+ });
58
+ cogsConnection.addEventListener('close', () => {
59
+ connected = false;
60
+ });
61
+ cogsConnection.addEventListener('config', (event) => {
62
+ const config = event.detail;
63
+ // Handle new config.
64
+ // `config` is of type `{ [configKey: string]: number | string | boolean }`
65
+ });
66
+ cogsConnection.addEventListener('updates', (event) => {
67
+ const updates = event.detail;
68
+ // Handle input port updates.
69
+ // `updates` is of type `{ [portName: string]: number | string | boolean }`
70
+ });
71
+ cogsConnection.addEventListener('event', (event) => {
72
+ const { key, value } = event.detail;
73
+ // Handle event. See 'types/Callback.ts`
74
+ // `key` is the event name.
75
+ // `value` is the type defined in COGS, one of `number | string | boolean | undefined`
76
+ });
77
+ cogsConnection.addEventListener('message', (event) => {
78
+ const message = event.detail;
79
+ // Handle message. See `types/CogsClientMessage.ts`
80
+ });
81
+
82
+ function sendEventToCogs() {
83
+ cogsConnection.sendEvent('Hello');
84
+ }
85
+
86
+ function sendPortUpdateToCogs() {
87
+ cogsConnection.setOutputPortValues({ port1: 100 });
88
+ }
89
+
90
+ const audioPlayer = new CogsAudioPlayer(cogsConnection);
91
+ audioPlayer.addEventListener('state', (audioState) => {
92
+ // Handle audio state. See `types/AudioState.ts`
93
+ });
94
+ ```
95
+
96
+ ### Local development
97
+
98
+ When developing locally you should connect to COGS in "simulator" mode by appending `?simulator=true&t=media_master&name=MEDIA_MASTER_NAME` to the URL. Replace `MEDIA_MASTER_NAME` with the name of the Media Master you set in COGS.
99
+
100
+ For example, with your custom content hosted on port 3000, http://localhost:3000?simulator=true&t=media_master&name=Timer+screen will connect as the simulator for `Timer screen`.
@@ -0,0 +1,48 @@
1
+ import CogsConnection from './CogsConnection';
2
+ import { AudioState } from './types/AudioState';
3
+ import MediaClipStateMessage from './types/MediaClipStateMessage';
4
+ declare type EventTypes = {
5
+ state: AudioState;
6
+ audioClipState: MediaClipStateMessage;
7
+ };
8
+ export default class AudioPlayer {
9
+ private eventTarget;
10
+ private globalVolume;
11
+ private audioClipPlayers;
12
+ private sinkId;
13
+ constructor(cogsConnection: CogsConnection);
14
+ setGlobalVolume(volume: number): void;
15
+ playAudioClip(path: string, { playId, volume, fade, loop }: {
16
+ playId: string;
17
+ volume: number;
18
+ fade?: number;
19
+ loop: boolean;
20
+ }): void;
21
+ pauseAudioClip(path: string, { fade }: {
22
+ fade?: number;
23
+ }, onlySoundId?: number, allowIfPauseRequested?: boolean): void;
24
+ stopAudioClip(path: string, { fade }: {
25
+ fade?: number;
26
+ }, onlySoundId?: number, allowIfStopRequested?: boolean): void;
27
+ stopAllAudioClips(options: {
28
+ fade?: number;
29
+ }): void;
30
+ setAudioClipVolume(path: string, { volume, fade }: {
31
+ volume: number;
32
+ fade?: number;
33
+ }): void;
34
+ private handleStoppedClip;
35
+ private updateActiveAudioClip;
36
+ private updateAudioClipPlayer;
37
+ setAudioSink(sinkId: string): void;
38
+ private updateConfig;
39
+ private notifyStateListeners;
40
+ private notifyClipStateListeners;
41
+ addEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | AddEventListenerOptions): void;
42
+ removeEventListener<EventName extends keyof EventTypes>(type: EventName, listener: (ev: CustomEvent<EventTypes[EventName]>) => void, options?: boolean | EventListenerOptions): void;
43
+ private dispatchEvent;
44
+ private createPlayer;
45
+ private createClip;
46
+ private updatedClip;
47
+ }
48
+ export {};
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const howler_1 = require("howler");
4
+ const urls_1 = require("./helpers/urls");
5
+ const DEBUG = true;
6
+ class AudioPlayer {
7
+ constructor(cogsConnection) {
8
+ this.eventTarget = new EventTarget();
9
+ this.globalVolume = 1;
10
+ this.audioClipPlayers = {};
11
+ this.sinkId = '';
12
+ // Send the current status of each clip to COGS
13
+ this.addEventListener('audioClipState', ({ detail }) => {
14
+ cogsConnection.sendMediaClipState(detail);
15
+ });
16
+ // Listen for audio control messages
17
+ cogsConnection.addEventListener('message', (event) => {
18
+ const message = event.detail;
19
+ switch (message.type) {
20
+ case 'media_config_update':
21
+ if (this.globalVolume !== message.globalVolume) {
22
+ this.setGlobalVolume(message.globalVolume);
23
+ }
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 'audio_play':
31
+ this.playAudioClip(message.file, {
32
+ playId: message.playId,
33
+ volume: message.volume,
34
+ loop: Boolean(message.loop),
35
+ fade: message.fade,
36
+ });
37
+ break;
38
+ case 'audio_pause':
39
+ this.pauseAudioClip(message.file, { fade: message.fade });
40
+ break;
41
+ case 'audio_stop':
42
+ if (message.file) {
43
+ this.stopAudioClip(message.file, { fade: message.fade });
44
+ }
45
+ else {
46
+ this.stopAllAudioClips({ fade: message.fade });
47
+ }
48
+ break;
49
+ case 'audio_set_clip_volume':
50
+ this.setAudioClipVolume(message.file, { volume: message.volume, fade: message.fade });
51
+ break;
52
+ }
53
+ });
54
+ // On connection, send the current playing state of all clips
55
+ // (Usually empty unless websocket is reconnecting)
56
+ const sendInitialClipStates = () => {
57
+ const files = Object.entries(this.audioClipPlayers).map(([file, player]) => {
58
+ const activeClips = Object.values(player.activeClips);
59
+ const status = activeClips.some(({ state }) => state.type === 'playing' ||
60
+ state.type === 'pausing' ||
61
+ state.type === 'stopping' ||
62
+ state.type === 'play_requested' ||
63
+ state.type === 'pause_requested' ||
64
+ state.type === 'stop_requested')
65
+ ? 'playing'
66
+ : activeClips.some(({ state }) => state.type === 'paused')
67
+ ? 'paused'
68
+ : 'stopped';
69
+ return [file, status];
70
+ });
71
+ cogsConnection.sendInitialMediaClipStates({ mediaType: 'audio', files });
72
+ };
73
+ cogsConnection.addEventListener('open', sendInitialClipStates);
74
+ sendInitialClipStates();
75
+ }
76
+ setGlobalVolume(volume) {
77
+ this.globalVolume = volume;
78
+ howler_1.Howler.volume(volume);
79
+ this.notifyStateListeners();
80
+ }
81
+ playAudioClip(path, { playId, volume, fade, loop }) {
82
+ log('Playing clip', { path });
83
+ if (!(path in this.audioClipPlayers)) {
84
+ log('Creating ephemeral clip', { path });
85
+ this.audioClipPlayers[path] = this.createClip(path, { preload: false, ephemeral: true });
86
+ }
87
+ this.updateAudioClipPlayer(path, (clipPlayer) => {
88
+ // Paused clips need to be played again
89
+ const pausedSoundIds = Object.entries(clipPlayer.activeClips)
90
+ .filter(([, { state }]) => state.type === 'paused' || state.type === 'pausing')
91
+ .map(([id]) => parseInt(id));
92
+ pausedSoundIds.forEach((soundId) => {
93
+ log('Resuming paused clip', { soundId });
94
+ clipPlayer.player.play(soundId);
95
+ });
96
+ // Clips with pause requested no longer need to pause, they can continue playing now
97
+ const pauseRequestedSoundIds = Object.entries(clipPlayer.activeClips)
98
+ .filter(([, { state }]) => state.type === 'pause_requested')
99
+ .map(([id]) => parseInt(id));
100
+ // If no currently paused/pausing/pause_requested clips, play a new clip
101
+ const newSoundIds = pausedSoundIds.length > 0 || pauseRequestedSoundIds.length > 0 ? [] : [clipPlayer.player.play()];
102
+ // paused and pause_requested clips treated the same, they should have their properties
103
+ // updated with the latest play action's properties
104
+ [...pausedSoundIds, ...pauseRequestedSoundIds, ...newSoundIds].forEach((soundId) => {
105
+ clipPlayer.player.loop(loop, soundId);
106
+ // Cleanup any old callbacks first
107
+ clipPlayer.player.off('play', undefined, soundId);
108
+ clipPlayer.player.off('pause', undefined, soundId);
109
+ clipPlayer.player.off('fade', undefined, soundId);
110
+ clipPlayer.player.off('end', undefined, soundId);
111
+ clipPlayer.player.off('stop', undefined, soundId);
112
+ // Non-preloaded clips don't yet have an HTML audio node
113
+ // so we need to set the audio output when it's playing
114
+ clipPlayer.player.once('play', () => {
115
+ setPlayerSinkId(clipPlayer.player, this.sinkId);
116
+ });
117
+ clipPlayer.player.once('stop', () => this.handleStoppedClip(path, playId, soundId), soundId);
118
+ // Looping clips fire the 'end' callback on every loop
119
+ clipPlayer.player.on('end', () => {
120
+ var _a;
121
+ if (!((_a = clipPlayer.activeClips[soundId]) === null || _a === void 0 ? void 0 : _a.loop)) {
122
+ this.handleStoppedClip(path, playId, soundId);
123
+ }
124
+ }, soundId);
125
+ const activeClip = {
126
+ playId,
127
+ state: { type: 'play_requested' },
128
+ loop,
129
+ volume,
130
+ };
131
+ // Once clip starts, check if it should actually be paused or stopped
132
+ // If not, then update state to 'playing'
133
+ clipPlayer.player.once('play', () => {
134
+ const clipState = clipPlayer.activeClips[soundId].state;
135
+ if (clipState.type === 'pause_requested') {
136
+ log('Clip started playing but should be paused', { path, soundId });
137
+ this.pauseAudioClip(path, { fade: clipState.fade }, soundId, true);
138
+ }
139
+ else if (clipState.type === 'stop_requested') {
140
+ log('Clip started playing but should be stopped', { path, soundId });
141
+ this.stopAudioClip(path, { fade: clipState.fade }, soundId, true);
142
+ }
143
+ else {
144
+ this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'playing' } }));
145
+ }
146
+ }, soundId);
147
+ // To fade or to no fade?
148
+ if (isFadeValid(fade)) {
149
+ // Start fade when clip starts
150
+ clipPlayer.player.volume(0, soundId);
151
+ clipPlayer.player.once('play', () => {
152
+ clipPlayer.player.fade(0, volume, fade * 1000, soundId);
153
+ }, soundId);
154
+ }
155
+ else {
156
+ clipPlayer.player.volume(volume, soundId);
157
+ }
158
+ // Track new/updated active clip
159
+ clipPlayer.activeClips = { ...clipPlayer.activeClips, [soundId]: activeClip };
160
+ });
161
+ return clipPlayer;
162
+ });
163
+ this.notifyClipStateListeners(playId, path, 'playing');
164
+ }
165
+ pauseAudioClip(path, { fade }, onlySoundId, allowIfPauseRequested) {
166
+ var _a, _b;
167
+ // No active clips to pause
168
+ if (Object.keys((_b = (_a = this.audioClipPlayers[path]) === null || _a === void 0 ? void 0 : _a.activeClips) !== null && _b !== void 0 ? _b : {}).length === 0) {
169
+ return;
170
+ }
171
+ this.updateAudioClipPlayer(path, (clipPlayer) => {
172
+ clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
173
+ const soundId = parseInt(soundIdStr);
174
+ // If onlySoundId specified, only update that clip
175
+ if (onlySoundId === undefined || onlySoundId === soundId) {
176
+ if ((allowIfPauseRequested && clip.state.type === 'pause_requested') || clip.state.type === 'playing' || clip.state.type === 'pausing') {
177
+ if (isFadeValid(fade)) {
178
+ // Fade then pause
179
+ clipPlayer.player.once('fade', (soundId) => {
180
+ clipPlayer.player.pause(soundId);
181
+ this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'paused' } }));
182
+ this.notifyClipStateListeners(clip.playId, path, 'paused');
183
+ }, soundId);
184
+ clipPlayer.player.fade(clipPlayer.player.volume(soundId), 0, fade * 1000, soundId);
185
+ clip.state = { type: 'pausing' };
186
+ }
187
+ else {
188
+ // Pause now
189
+ clipPlayer.player.pause(soundId);
190
+ clip.state = { type: 'paused' };
191
+ this.notifyClipStateListeners(clip.playId, path, 'paused');
192
+ }
193
+ }
194
+ // Clip hasn't started playing yet, or has already had pause_requested (but fade may have changed so update here)
195
+ else if (clip.state.type === 'play_requested' || clip.state.type === 'pause_requested') {
196
+ clip.state = { type: 'pause_requested', fade };
197
+ }
198
+ }
199
+ return [soundIdStr, clip];
200
+ }));
201
+ return clipPlayer;
202
+ });
203
+ }
204
+ stopAudioClip(path, { fade }, onlySoundId, allowIfStopRequested) {
205
+ var _a, _b;
206
+ // No active clips to stop
207
+ if (Object.keys((_b = (_a = this.audioClipPlayers[path]) === null || _a === void 0 ? void 0 : _a.activeClips) !== null && _b !== void 0 ? _b : {}).length === 0) {
208
+ return;
209
+ }
210
+ this.updateAudioClipPlayer(path, (clipPlayer) => {
211
+ clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
212
+ const soundId = parseInt(soundIdStr);
213
+ // If onlySoundId specified, only update that clip
214
+ if (onlySoundId === undefined || onlySoundId === soundId) {
215
+ if ((allowIfStopRequested && clip.state.type === 'stop_requested') ||
216
+ clip.state.type === 'playing' ||
217
+ clip.state.type === 'pausing' ||
218
+ clip.state.type === 'paused' ||
219
+ clip.state.type === 'stopping') {
220
+ if (isFadeValid(fade) && clip.state.type !== 'paused') {
221
+ // Cleanup any old fade callbacks first
222
+ // TODO: Remove cast once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59411 is merged
223
+ clipPlayer.player.off('fade', soundId);
224
+ clipPlayer.player.fade(clipPlayer.player.volume(soundId), 0, fade * 1000, soundId);
225
+ // Set callback after starting new fade, otherwise it will fire straight away as the previous fade is cancelled
226
+ clipPlayer.player.once('fade', (soundId) => clipPlayer.player.stop(soundId), soundId);
227
+ clip.state = { type: 'stopping' };
228
+ }
229
+ else {
230
+ clipPlayer.player.stop(soundId);
231
+ }
232
+ }
233
+ // Clip hasn't started playing yet, or has already had stop_requested (but fade may have changed so update here)
234
+ // or has pause_requested, but stop takes precedence
235
+ else if (clip.state.type === 'play_requested' || clip.state.type === 'pause_requested' || clip.state.type === 'stop_requested') {
236
+ log("Trying to stop clip which hasn't started playing yet", { path, soundId });
237
+ clip.state = { type: 'stop_requested', fade };
238
+ }
239
+ }
240
+ return [soundIdStr, clip];
241
+ }));
242
+ return clipPlayer;
243
+ });
244
+ }
245
+ stopAllAudioClips(options) {
246
+ log('Stopping all clips');
247
+ Object.keys(this.audioClipPlayers).forEach((path) => {
248
+ this.stopAudioClip(path, options);
249
+ });
250
+ }
251
+ setAudioClipVolume(path, { volume, fade }) {
252
+ var _a, _b;
253
+ if (!(volume >= 0 && volume <= 1)) {
254
+ console.warn('Invalid volume', volume);
255
+ return;
256
+ }
257
+ // No active clips to set volume for
258
+ if (Object.keys((_b = (_a = this.audioClipPlayers[path]) === null || _a === void 0 ? void 0 : _a.activeClips) !== null && _b !== void 0 ? _b : {}).length === 0) {
259
+ return;
260
+ }
261
+ this.updateAudioClipPlayer(path, (clipPlayer) => {
262
+ clipPlayer.activeClips = Object.fromEntries(Object.entries(clipPlayer.activeClips).map(([soundIdStr, clip]) => {
263
+ // Ignored for pausing/stopping instances
264
+ if (clip.state.type !== 'pausing' && clip.state.type !== 'stopping') {
265
+ const soundId = parseInt(soundIdStr);
266
+ if (isFadeValid(fade)) {
267
+ clipPlayer.player.fade(clipPlayer.player.volume(soundId), volume, fade * 1000);
268
+ }
269
+ else {
270
+ clipPlayer.player.volume(volume);
271
+ }
272
+ return [soundIdStr, { ...clip, volume }];
273
+ }
274
+ else {
275
+ return [soundIdStr, clip];
276
+ }
277
+ }));
278
+ return clipPlayer;
279
+ });
280
+ }
281
+ handleStoppedClip(path, playId, soundId) {
282
+ this.updateAudioClipPlayer(path, (clipPlayer) => {
283
+ delete clipPlayer.activeClips[soundId];
284
+ return clipPlayer;
285
+ });
286
+ this.notifyClipStateListeners(playId, path, 'stopped');
287
+ }
288
+ updateActiveAudioClip(path, soundId, update) {
289
+ this.updateAudioClipPlayer(path, (clipPlayer) => soundId in clipPlayer.activeClips
290
+ ? { ...clipPlayer, activeClips: { ...clipPlayer.activeClips, [soundId]: update(clipPlayer.activeClips[soundId]) } }
291
+ : clipPlayer);
292
+ }
293
+ updateAudioClipPlayer(path, update) {
294
+ var _a;
295
+ if (path in this.audioClipPlayers) {
296
+ this.audioClipPlayers = { ...this.audioClipPlayers, [path]: update(this.audioClipPlayers[path]) };
297
+ }
298
+ // Once last instance of an ephemeral clip is removed, cleanup and remove the player
299
+ const clipPlayer = this.audioClipPlayers[path];
300
+ if (clipPlayer && Object.keys((_a = clipPlayer.activeClips) !== null && _a !== void 0 ? _a : {}).length === 0 && clipPlayer.config.ephemeral) {
301
+ clipPlayer.player.unload();
302
+ delete this.audioClipPlayers[path];
303
+ }
304
+ this.notifyStateListeners();
305
+ }
306
+ setAudioSink(sinkId) {
307
+ log(`Setting sink ID for all clips:`, sinkId);
308
+ for (const clipPlayer of Object.values(this.audioClipPlayers)) {
309
+ setPlayerSinkId(clipPlayer.player, sinkId);
310
+ }
311
+ this.sinkId = sinkId;
312
+ }
313
+ updateConfig(newFiles) {
314
+ const newAudioFiles = Object.fromEntries(Object.entries(newFiles).filter((file) => {
315
+ const type = file[1].type;
316
+ // COGS 4.6 did not send a `type` but only reported audio files
317
+ // so we assume audio if no `type` is given
318
+ return type === 'audio' || !type;
319
+ }));
320
+ const previousClipPlayers = this.audioClipPlayers;
321
+ this.audioClipPlayers = (() => {
322
+ const clipPlayers = { ...previousClipPlayers };
323
+ const removedClips = Object.keys(previousClipPlayers).filter((previousFile) => !(previousFile in newAudioFiles) && !previousClipPlayers[previousFile].config.ephemeral);
324
+ removedClips.forEach((file) => {
325
+ const player = previousClipPlayers[file].player;
326
+ player.unload();
327
+ delete clipPlayers[file];
328
+ });
329
+ const addedClips = Object.entries(newAudioFiles).filter(([newfile]) => !previousClipPlayers[newfile]);
330
+ addedClips.forEach(([path, config]) => {
331
+ clipPlayers[path] = this.createClip(path, { ...config, ephemeral: false });
332
+ });
333
+ const updatedClips = Object.keys(previousClipPlayers).filter((previousFile) => previousFile in newAudioFiles);
334
+ updatedClips.forEach((path) => {
335
+ clipPlayers[path] = this.updatedClip(path, clipPlayers[path], { ...newAudioFiles[path], ephemeral: false });
336
+ });
337
+ return clipPlayers;
338
+ })();
339
+ this.notifyStateListeners();
340
+ }
341
+ notifyStateListeners() {
342
+ const clips = Object.entries(this.audioClipPlayers).reduce((clips, [path, clipPlayer]) => {
343
+ clips[path] = {
344
+ config: { preload: clipPlayer.config.preload, ephemeral: clipPlayer.config.ephemeral },
345
+ activeClips: clipPlayer.activeClips,
346
+ };
347
+ return clips;
348
+ }, {});
349
+ 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'));
350
+ const audioState = {
351
+ globalVolume: this.globalVolume,
352
+ isPlaying,
353
+ clips,
354
+ };
355
+ this.dispatchEvent('state', audioState);
356
+ }
357
+ notifyClipStateListeners(playId, file, status) {
358
+ this.dispatchEvent('audioClipState', { mediaType: 'audio', playId, file, status });
359
+ }
360
+ // Type-safe wrapper around EventTarget
361
+ addEventListener(type, listener, options) {
362
+ this.eventTarget.addEventListener(type, listener, options);
363
+ }
364
+ removeEventListener(type, listener, options) {
365
+ this.eventTarget.removeEventListener(type, listener, options);
366
+ }
367
+ dispatchEvent(type, detail) {
368
+ this.eventTarget.dispatchEvent(new CustomEvent(type, { detail }));
369
+ }
370
+ createPlayer(path, config) {
371
+ const player = new howler_1.Howl({
372
+ src: urls_1.assetUrl(path),
373
+ autoplay: false,
374
+ loop: false,
375
+ volume: 1,
376
+ html5: true,
377
+ preload: config.preload,
378
+ });
379
+ setPlayerSinkId(player, this.sinkId);
380
+ return player;
381
+ }
382
+ createClip(file, config) {
383
+ return {
384
+ config,
385
+ player: this.createPlayer(file, config),
386
+ activeClips: {},
387
+ };
388
+ }
389
+ updatedClip(clipPath, previousClip, newConfig) {
390
+ const clip = { ...previousClip, config: newConfig };
391
+ if (previousClip.config.preload !== newConfig.preload) {
392
+ clip.player.unload();
393
+ clip.player = this.createPlayer(clipPath, newConfig);
394
+ }
395
+ return clip;
396
+ }
397
+ }
398
+ exports.default = AudioPlayer;
399
+ function log(...data) {
400
+ if (DEBUG) {
401
+ console.log(...data);
402
+ }
403
+ }
404
+ function isFadeValid(fade) {
405
+ return typeof fade === 'number' && !isNaN(fade) && fade > 0;
406
+ }
407
+ function setPlayerSinkId(player, sinkId) {
408
+ if (sinkId === undefined) {
409
+ return;
410
+ }
411
+ if (player._html5) {
412
+ player._sounds.forEach((sound) => {
413
+ sound._node.setSinkId(sinkId);
414
+ });
415
+ }
416
+ else {
417
+ // TODO: handle web audio
418
+ console.warn('Cannot set sink ID: web audio not supported', player);
419
+ }
420
+ }
@@ -0,0 +1,81 @@
1
+ import { ConfigValue, EventKeyValue, EventValue, PortValue, ShowPhase } from './types/valueTypes';
2
+ import CogsClientMessage from './types/CogsClientMessage';
3
+ import MediaClipStateMessage from './types/MediaClipStateMessage';
4
+ import AllMediaClipStatesMessage from './types/AllMediaClipStatesMessage';
5
+ interface ConnectionEventListeners<CustomTypes extends {
6
+ config?: {
7
+ [configKey: string]: ConfigValue;
8
+ };
9
+ inputPorts?: {
10
+ [port: string]: PortValue;
11
+ };
12
+ inputEvents?: {
13
+ [key: string]: EventValue | null;
14
+ };
15
+ }> {
16
+ open: undefined;
17
+ close: undefined;
18
+ message: CogsClientMessage;
19
+ config: CustomTypes['config'];
20
+ updates: Partial<CustomTypes['inputPorts']>;
21
+ event: CustomTypes['inputEvents'] extends {
22
+ [key: string]: EventValue | null;
23
+ } ? EventKeyValue<CustomTypes['inputEvents']> : Record<string, never>;
24
+ }
25
+ export declare type TimerState = Omit<Extract<CogsClientMessage, {
26
+ type: 'adjustable_timer_update';
27
+ }>, 'type'> & {
28
+ startedAt: number;
29
+ };
30
+ export default class CogsConnection<CustomTypes extends {
31
+ config?: {
32
+ [configKey: string]: ConfigValue;
33
+ };
34
+ inputPorts?: {
35
+ [port: string]: PortValue;
36
+ };
37
+ outputPorts?: {
38
+ [port: string]: PortValue;
39
+ };
40
+ inputEvents?: {
41
+ [key: string]: EventValue | null;
42
+ };
43
+ outputEvents?: {
44
+ [key: string]: EventValue | null;
45
+ };
46
+ } = Record<never, never>> {
47
+ private websocket;
48
+ private eventTarget;
49
+ private currentConfig;
50
+ get config(): CustomTypes['config'];
51
+ private currentInputPortValues;
52
+ get inputPortValues(): CustomTypes['inputPorts'];
53
+ private currentOutputPortValues;
54
+ get outputPortValues(): CustomTypes['outputPorts'];
55
+ private _showPhase;
56
+ get showPhase(): ShowPhase;
57
+ private _timerState;
58
+ get timerState(): TimerState | null;
59
+ /**
60
+ * Cached audio outputs use to look up the device/sink ID when a different device label is requested
61
+ */
62
+ private audioOutputs;
63
+ private _selectedAudioOutput;
64
+ get selectedAudioOutput(): string;
65
+ constructor({ hostname, port }?: {
66
+ hostname?: string;
67
+ port?: number;
68
+ }, outputPortValues?: CustomTypes['outputPorts']);
69
+ get isConnected(): boolean;
70
+ close(): void;
71
+ sendEvent<EventName extends keyof CustomTypes['outputEvents']>(eventName: EventName, ...[eventValue]: CustomTypes['outputEvents'][EventName] extends null ? [] : [CustomTypes['outputEvents'][EventName]]): void;
72
+ setOutputPortValues(values: Partial<CustomTypes['outputPorts']>): void;
73
+ getAudioSinkId(audioOutput: string): string | undefined;
74
+ sendInitialMediaClipStates(allMediaClipStates: AllMediaClipStatesMessage): void;
75
+ sendMediaClipState(mediaClipState: MediaClipStateMessage): void;
76
+ sendAudioOutputs(audioOutputs: MediaDeviceInfo[]): void;
77
+ addEventListener<EventName extends keyof ConnectionEventListeners<CustomTypes>, EventValue extends ConnectionEventListeners<CustomTypes>[EventName]>(type: EventName, listener: (ev: CustomEvent<EventValue>) => void, options?: boolean | AddEventListenerOptions): void;
78
+ removeEventListener<EventName extends keyof ConnectionEventListeners<CustomTypes>, EventValue extends ConnectionEventListeners<CustomTypes>[EventName]>(type: EventName, listener: (ev: CustomEvent<EventValue>) => void, options?: boolean | EventListenerOptions): void;
79
+ private dispatchEvent;
80
+ }
81
+ export {};