@clockworkdog/cogs-client 1.2.0 → 1.3.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.
@@ -9,6 +9,7 @@ export default class AudioPlayer {
9
9
  private eventTarget;
10
10
  private globalVolume;
11
11
  private audioClipPlayers;
12
+ private sinkId;
12
13
  constructor(cogsConnection: CogsConnection);
13
14
  setGlobalVolume(volume: number): void;
14
15
  playAudioClip(path: string, { playId, volume, fade, loop }: {
@@ -33,6 +34,7 @@ export default class AudioPlayer {
33
34
  private handleStoppedClip;
34
35
  private updateActiveAudioClip;
35
36
  private updateAudioClipPlayer;
37
+ setAudioSink(sinkId: string): void;
36
38
  private updateConfig;
37
39
  private notifyStateListeners;
38
40
  private notifyClipStateListeners;
@@ -8,6 +8,7 @@ class AudioPlayer {
8
8
  this.eventTarget = new EventTarget();
9
9
  this.globalVolume = 1;
10
10
  this.audioClipPlayers = {};
11
+ this.sinkId = '';
11
12
  // Send the current status of each clip to COGS
12
13
  this.addEventListener('audioClipState', ({ detail }) => {
13
14
  cogsConnection.sendMediaClipState(detail);
@@ -20,6 +21,10 @@ class AudioPlayer {
20
21
  if (this.globalVolume !== message.globalVolume) {
21
22
  this.setGlobalVolume(message.globalVolume);
22
23
  }
24
+ if (message.audioOutput !== undefined) {
25
+ const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
26
+ this.setAudioSink(sinkId !== null && sinkId !== void 0 ? sinkId : '');
27
+ }
23
28
  this.updateConfig(message.files);
24
29
  break;
25
30
  case 'audio_play':
@@ -293,6 +298,13 @@ class AudioPlayer {
293
298
  }
294
299
  this.notifyStateListeners();
295
300
  }
301
+ setAudioSink(sinkId) {
302
+ log(`Setting sink ID for all clips:`, sinkId);
303
+ for (const clipPlayer of Object.values(this.audioClipPlayers)) {
304
+ setPlayerSinkId(clipPlayer.player, sinkId);
305
+ }
306
+ this.sinkId = sinkId;
307
+ }
296
308
  updateConfig(newFiles) {
297
309
  const newAudioFiles = Object.fromEntries(Object.entries(newFiles).filter((file) => {
298
310
  const type = file[1].type;
@@ -356,8 +368,10 @@ class AudioPlayer {
356
368
  autoplay: false,
357
369
  loop: false,
358
370
  volume: 1,
359
- html5: !config.preload,
371
+ html5: true,
372
+ preload: config.preload,
360
373
  });
374
+ setPlayerSinkId(player, this.sinkId);
361
375
  return player;
362
376
  }
363
377
  createClip(file, config) {
@@ -385,3 +399,17 @@ function log(...data) {
385
399
  function isFadeValid(fade) {
386
400
  return typeof fade === 'number' && !isNaN(fade) && fade > 0;
387
401
  }
402
+ function setPlayerSinkId(player, sinkId) {
403
+ if (sinkId === undefined) {
404
+ return;
405
+ }
406
+ if (player._html5) {
407
+ player._sounds.forEach((sound) => {
408
+ sound._node.setSinkId(sinkId);
409
+ });
410
+ }
411
+ else {
412
+ // TODO: handle web audio
413
+ console.warn('Cannot set sink ID: web audio not supported', player);
414
+ }
415
+ }
@@ -56,6 +56,12 @@ export default class CogsConnection<CustomTypes extends {
56
56
  get showPhase(): ShowPhase;
57
57
  private _timerState;
58
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;
59
65
  constructor({ hostname, port }?: {
60
66
  hostname?: string;
61
67
  port?: number;
@@ -64,8 +70,10 @@ export default class CogsConnection<CustomTypes extends {
64
70
  close(): void;
65
71
  sendEvent<EventName extends keyof CustomTypes['outputEvents']>(eventName: EventName, ...[eventValue]: CustomTypes['outputEvents'][EventName] extends null ? [] : [CustomTypes['outputEvents'][EventName]]): void;
66
72
  setOutputPortValues(values: Partial<CustomTypes['outputPorts']>): void;
73
+ getAudioSinkId(audioOutput: string): string | undefined;
67
74
  sendInitialMediaClipStates(allMediaClipStates: AllMediaClipStatesMessage): void;
68
75
  sendMediaClipState(mediaClipState: MediaClipStateMessage): void;
76
+ sendAudioOutputs(audioOutputs: MediaDeviceInfo[]): void;
69
77
  addEventListener<EventName extends keyof ConnectionEventListeners<CustomTypes>, EventValue extends ConnectionEventListeners<CustomTypes>[EventName]>(type: EventName, listener: (ev: CustomEvent<EventValue>) => void, options?: boolean | AddEventListenerOptions): void;
70
78
  removeEventListener<EventName extends keyof ConnectionEventListeners<CustomTypes>, EventValue extends ConnectionEventListeners<CustomTypes>[EventName]>(type: EventName, listener: (ev: CustomEvent<EventValue>) => void, options?: boolean | EventListenerOptions): void;
71
79
  private dispatchEvent;
@@ -8,12 +8,18 @@ const reconnecting_websocket_1 = __importDefault(require("reconnecting-websocket
8
8
  const urls_1 = require("./helpers/urls");
9
9
  class CogsConnection {
10
10
  constructor({ hostname = document.location.hostname, port = urls_1.COGS_SERVER_PORT } = {}, outputPortValues = undefined) {
11
+ var _a;
11
12
  this.eventTarget = new EventTarget();
12
13
  this.currentConfig = {}; // Received on open connection
13
14
  this.currentInputPortValues = {}; // Received on open connection
14
15
  this.currentOutputPortValues = {}; // Sent on open connection
15
16
  this._showPhase = valueTypes_1.ShowPhase.Setup;
16
17
  this._timerState = null;
18
+ /**
19
+ * Cached audio outputs use to look up the device/sink ID when a different device label is requested
20
+ */
21
+ this.audioOutputs = undefined;
22
+ this._selectedAudioOutput = '';
17
23
  this.currentOutputPortValues = { ...outputPortValues };
18
24
  const { useReconnectingWebsocket, path, pathParams } = websocketParametersFromUrl(document.location.href);
19
25
  const socketUrl = `ws://${hostname}:${port}${path}${pathParams ? '?' + pathParams : ''}`;
@@ -37,7 +43,7 @@ class CogsConnection {
37
43
  }
38
44
  else if (parsed.updates) {
39
45
  this.currentInputPortValues = { ...this.currentInputPortValues, ...parsed.updates };
40
- this.dispatchEvent('updates', this.currentInputPortValues);
46
+ this.dispatchEvent('updates', parsed.updates);
41
47
  }
42
48
  else if (parsed.event && parsed.event.key) {
43
49
  this.dispatchEvent('event', parsed.event);
@@ -66,6 +72,20 @@ class CogsConnection {
66
72
  console.error('Unable to parse incoming data from server', data, e);
67
73
  }
68
74
  };
75
+ // Send a list of audio outputs to COGS and keep it up to date
76
+ {
77
+ const refreshAudioOutputs = async () => {
78
+ // `navigator.mediaDevices` is undefined on COGS AV <= 4.5 because of secure origin permissions
79
+ if (navigator.mediaDevices) {
80
+ const audioOutputs = (await navigator.mediaDevices.enumerateDevices()).filter(({ kind }) => kind === 'audiooutput');
81
+ this.sendAudioOutputs(audioOutputs);
82
+ this.audioOutputs = audioOutputs;
83
+ }
84
+ };
85
+ this.addEventListener('open', refreshAudioOutputs);
86
+ (_a = navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.addEventListener('devicechange', refreshAudioOutputs);
87
+ refreshAudioOutputs();
88
+ }
69
89
  }
70
90
  get config() {
71
91
  return { ...this.currentConfig };
@@ -82,6 +102,9 @@ class CogsConnection {
82
102
  get timerState() {
83
103
  return this._timerState ? { ...this._timerState } : null;
84
104
  }
105
+ get selectedAudioOutput() {
106
+ return this._selectedAudioOutput;
107
+ }
85
108
  get isConnected() {
86
109
  return this.websocket.readyState === WebSocket.OPEN;
87
110
  }
@@ -104,6 +127,10 @@ class CogsConnection {
104
127
  this.websocket.send(JSON.stringify({ updates: values }));
105
128
  }
106
129
  }
130
+ getAudioSinkId(audioOutput) {
131
+ var _a, _b;
132
+ return audioOutput ? (_b = (_a = this.audioOutputs) === null || _a === void 0 ? void 0 : _a.find(({ label }) => label === audioOutput)) === null || _b === void 0 ? void 0 : _b.deviceId : '';
133
+ }
107
134
  sendInitialMediaClipStates(allMediaClipStates) {
108
135
  if (this.isConnected) {
109
136
  this.websocket.send(JSON.stringify({ allMediaClipStates }));
@@ -114,6 +141,11 @@ class CogsConnection {
114
141
  this.websocket.send(JSON.stringify({ mediaClipState }));
115
142
  }
116
143
  }
144
+ sendAudioOutputs(audioOutputs) {
145
+ if (this.isConnected) {
146
+ this.websocket.send(JSON.stringify({ audioOutputs }));
147
+ }
148
+ }
117
149
  // Type-safe wrapper around EventTarget
118
150
  addEventListener(type, listener, options) {
119
151
  this.eventTarget.addEventListener(type, listener, options);
@@ -12,6 +12,7 @@ export default class VideoPlayer {
12
12
  private videoClipPlayers;
13
13
  private activeClip?;
14
14
  private parentElement;
15
+ private sinkId;
15
16
  constructor(cogsConnection: CogsConnection, parentElement?: HTMLElement);
16
17
  setParentElement(parentElement: HTMLElement): void;
17
18
  resetParentElement(): void;
@@ -35,6 +36,7 @@ export default class VideoPlayer {
35
36
  }): void;
36
37
  private handleStoppedClip;
37
38
  private updateVideoClipPlayer;
39
+ setAudioSink(sinkId: string): void;
38
40
  private updateConfig;
39
41
  private notifyStateListeners;
40
42
  private notifyClipStateListeners;
@@ -8,6 +8,7 @@ class VideoPlayer {
8
8
  this.eventTarget = new EventTarget();
9
9
  this.globalVolume = 1;
10
10
  this.videoClipPlayers = {};
11
+ this.sinkId = '';
11
12
  this.parentElement = parentElement;
12
13
  // Send the current status of each clip to COGS
13
14
  this.addEventListener('videoClipState', ({ detail }) => {
@@ -19,6 +20,10 @@ class VideoPlayer {
19
20
  switch (message.type) {
20
21
  case 'media_config_update':
21
22
  this.setGlobalVolume(message.globalVolume);
23
+ if (message.audioOutput !== undefined) {
24
+ const sinkId = cogsConnection.getAudioSinkId(message.audioOutput);
25
+ this.setAudioSink(sinkId !== null && sinkId !== void 0 ? sinkId : '');
26
+ }
22
27
  this.updateConfig(message.files);
23
28
  break;
24
29
  case 'video_play':
@@ -182,6 +187,12 @@ class VideoPlayer {
182
187
  this.notifyStateListeners();
183
188
  }
184
189
  }
190
+ setAudioSink(sinkId) {
191
+ for (const clipPlayer of Object.values(this.videoClipPlayers)) {
192
+ setPlayerSinkId(clipPlayer, sinkId);
193
+ }
194
+ this.sinkId = sinkId;
195
+ }
185
196
  updateConfig(newPaths) {
186
197
  const newVideoPaths = Object.fromEntries(Object.entries(newPaths).filter(([, { type }]) => type === 'video' || !type));
187
198
  const previousClipPlayers = this.videoClipPlayers;
@@ -283,11 +294,13 @@ class VideoPlayer {
283
294
  }
284
295
  createClipPlayer(path, config) {
285
296
  const volume = 1;
286
- return {
297
+ const player = {
287
298
  config,
288
299
  videoElement: this.createVideoElement(path, config, { volume }),
289
300
  volume,
290
301
  };
302
+ setPlayerSinkId(player, this.sinkId);
303
+ return player;
291
304
  }
292
305
  unloadClip(path) {
293
306
  var _a, _b;
@@ -304,3 +317,9 @@ exports.default = VideoPlayer;
304
317
  function preloadString(preload) {
305
318
  return typeof preload === 'string' ? preload : preload ? 'metadata' : 'none';
306
319
  }
320
+ function setPlayerSinkId(player, sinkId) {
321
+ if (sinkId === undefined) {
322
+ return;
323
+ }
324
+ player.videoElement.setSinkId(sinkId);
325
+ }