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