@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 +100 -0
- package/dist/AudioPlayer.d.ts +48 -0
- package/dist/AudioPlayer.js +420 -0
- package/dist/CogsConnection.d.ts +81 -0
- package/dist/CogsConnection.js +198 -0
- package/dist/VideoPlayer.d.ts +50 -0
- package/dist/VideoPlayer.js +325 -0
- package/dist/browser/index.js +4863 -0
- package/dist/helpers/urls.d.ts +3 -0
- package/dist/helpers/urls.js +31 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +28 -0
- package/dist/types/AllMediaClipStatesMessage.d.ts +5 -0
- package/dist/types/AllMediaClipStatesMessage.js +2 -0
- package/dist/types/AudioState.d.ts +39 -0
- package/dist/types/AudioState.js +2 -0
- package/dist/types/CogsClientMessage.d.ts +85 -0
- package/dist/types/CogsClientMessage.js +2 -0
- package/dist/types/MediaClipStateMessage.d.ts +7 -0
- package/dist/types/MediaClipStateMessage.js +2 -0
- package/dist/types/MediaObjectFit.d.ts +2 -0
- package/dist/types/MediaObjectFit.js +2 -0
- package/dist/types/VideoState.d.ts +26 -0
- package/dist/types/VideoState.js +8 -0
- package/dist/types/valueTypes.d.ts +18 -0
- package/dist/types/valueTypes.js +10 -0
- package/package.json +45 -0
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 {};
|