@clockworkdog/cogs-client 1.2.0-rc.1 → 1.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/dist/AudioPlayer.d.ts +2 -0
- package/dist/AudioPlayer.js +34 -1
- package/dist/CogsConnection.d.ts +8 -0
- package/dist/CogsConnection.js +33 -1
- package/dist/VideoPlayer.d.ts +2 -0
- package/dist/VideoPlayer.js +20 -1
- package/dist/browser/index.js +88 -4
- package/dist/types/CogsClientMessage.d.ts +1 -0
- package/package.json +1 -1
package/dist/AudioPlayer.d.ts
CHANGED
|
@@ -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;
|
package/dist/AudioPlayer.js
CHANGED
|
@@ -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':
|
|
@@ -104,6 +109,11 @@ class AudioPlayer {
|
|
|
104
109
|
clipPlayer.player.off('fade', undefined, soundId);
|
|
105
110
|
clipPlayer.player.off('end', undefined, soundId);
|
|
106
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
|
+
});
|
|
107
117
|
clipPlayer.player.once('stop', () => this.handleStoppedClip(path, playId, soundId), soundId);
|
|
108
118
|
// Looping clips fire the 'end' callback on every loop
|
|
109
119
|
clipPlayer.player.on('end', () => {
|
|
@@ -293,6 +303,13 @@ class AudioPlayer {
|
|
|
293
303
|
}
|
|
294
304
|
this.notifyStateListeners();
|
|
295
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
|
+
}
|
|
296
313
|
updateConfig(newFiles) {
|
|
297
314
|
const newAudioFiles = Object.fromEntries(Object.entries(newFiles).filter((file) => {
|
|
298
315
|
const type = file[1].type;
|
|
@@ -356,8 +373,10 @@ class AudioPlayer {
|
|
|
356
373
|
autoplay: false,
|
|
357
374
|
loop: false,
|
|
358
375
|
volume: 1,
|
|
359
|
-
html5:
|
|
376
|
+
html5: true,
|
|
377
|
+
preload: config.preload,
|
|
360
378
|
});
|
|
379
|
+
setPlayerSinkId(player, this.sinkId);
|
|
361
380
|
return player;
|
|
362
381
|
}
|
|
363
382
|
createClip(file, config) {
|
|
@@ -385,3 +404,17 @@ function log(...data) {
|
|
|
385
404
|
function isFadeValid(fade) {
|
|
386
405
|
return typeof fade === 'number' && !isNaN(fade) && fade > 0;
|
|
387
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
|
+
}
|
package/dist/CogsConnection.d.ts
CHANGED
|
@@ -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;
|
package/dist/CogsConnection.js
CHANGED
|
@@ -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',
|
|
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);
|
package/dist/VideoPlayer.d.ts
CHANGED
|
@@ -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;
|
package/dist/VideoPlayer.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|