@clockworkdog/cogs-client 1.5.0 → 1.5.2

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.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const howler_1 = require("howler");
4
4
  const urls_1 = require("./helpers/urls");
5
5
  const DEBUG = true;
6
+ // Check an iOS-only property (See https://developer.mozilla.org/en-US/docs/Web/API/Navigator#non-standard_properties)
7
+ const IS_IOS = typeof navigator.standalone !== 'undefined';
6
8
  class AudioPlayer {
7
9
  constructor(cogsConnection) {
8
10
  this.eventTarget = new EventTarget();
@@ -148,12 +150,13 @@ class AudioPlayer {
148
150
  if (isFadeValid(fade)) {
149
151
  // Start fade when clip starts
150
152
  clipPlayer.player.volume(0, soundId);
153
+ clipPlayer.player.mute(false, soundId);
151
154
  clipPlayer.player.once('play', () => {
152
- clipPlayer.player.fade(0, volume, fade * 1000, soundId);
155
+ fadeAudioPlayerVolume(clipPlayer.player, volume, fade * 1000, soundId);
153
156
  }, soundId);
154
157
  }
155
158
  else {
156
- clipPlayer.player.volume(volume, soundId);
159
+ setAudioPlayerVolume(clipPlayer.player, volume, soundId);
157
160
  }
158
161
  // Track new/updated active clip
159
162
  clipPlayer.activeClips = { ...clipPlayer.activeClips, [soundId]: activeClip };
@@ -181,7 +184,7 @@ class AudioPlayer {
181
184
  this.updateActiveAudioClip(path, soundId, (clip) => ({ ...clip, state: { type: 'paused' } }));
182
185
  this.notifyClipStateListeners(clip.playId, path, 'paused');
183
186
  }, soundId);
184
- clipPlayer.player.fade(clipPlayer.player.volume(soundId), 0, fade * 1000, soundId);
187
+ fadeAudioPlayerVolume(clipPlayer.player, 0, fade * 1000, soundId);
185
188
  clip.state = { type: 'pausing' };
186
189
  }
187
190
  else {
@@ -221,7 +224,7 @@ class AudioPlayer {
221
224
  // Cleanup any old fade callbacks first
222
225
  // TODO: Remove cast once https://github.com/DefinitelyTyped/DefinitelyTyped/pull/59411 is merged
223
226
  clipPlayer.player.off('fade', soundId);
224
- clipPlayer.player.fade(clipPlayer.player.volume(soundId), 0, fade * 1000, soundId);
227
+ fadeAudioPlayerVolume(clipPlayer.player, 0, fade * 1000, soundId);
225
228
  // Set callback after starting new fade, otherwise it will fire straight away as the previous fade is cancelled
226
229
  clipPlayer.player.once('fade', (soundId) => clipPlayer.player.stop(soundId), soundId);
227
230
  clip.state = { type: 'stopping' };
@@ -264,10 +267,10 @@ class AudioPlayer {
264
267
  if (clip.state.type !== 'pausing' && clip.state.type !== 'stopping') {
265
268
  const soundId = parseInt(soundIdStr);
266
269
  if (isFadeValid(fade)) {
267
- clipPlayer.player.fade(clipPlayer.player.volume(soundId), volume, fade * 1000);
270
+ fadeAudioPlayerVolume(clipPlayer.player, volume, fade * 1000, soundId);
268
271
  }
269
272
  else {
270
- clipPlayer.player.volume(volume);
273
+ setAudioPlayerVolume(clipPlayer.player, volume, soundId);
271
274
  }
272
275
  return [soundIdStr, { ...clip, volume }];
273
276
  }
@@ -401,8 +404,11 @@ function log(...data) {
401
404
  console.log(...data);
402
405
  }
403
406
  }
407
+ /**
408
+ * @returns `true` if this is this a valid fade duration. Always returns `false` on iOS
409
+ */
404
410
  function isFadeValid(fade) {
405
- return typeof fade === 'number' && !isNaN(fade) && fade > 0;
411
+ return !IS_IOS && typeof fade === 'number' && !isNaN(fade) && fade > 0;
406
412
  }
407
413
  function setPlayerSinkId(player, sinkId) {
408
414
  var _a;
@@ -420,3 +426,21 @@ function setPlayerSinkId(player, sinkId) {
420
426
  console.warn('Cannot set sink ID: web audio not supported', player);
421
427
  }
422
428
  }
429
+ /**
430
+ * Set audio volume
431
+ *
432
+ * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
433
+ */
434
+ function setAudioPlayerVolume(howl, volume, soundId) {
435
+ howl.volume(volume, soundId);
436
+ howl.mute(volume === 0, soundId);
437
+ }
438
+ /**
439
+ * Fade to audio volume
440
+ *
441
+ * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
442
+ */
443
+ function fadeAudioPlayerVolume(howl, volume, fade, soundId) {
444
+ howl.mute(false, soundId);
445
+ howl.fade(howl.volume(soundId), volume, fade, soundId);
446
+ }
@@ -22,6 +22,7 @@ class RtspStreamer {
22
22
  play(params) {
23
23
  var _a;
24
24
  const { uri, videoElement } = params;
25
+ videoElement.playsInline = true; // Required for iOS
25
26
  let pipeline;
26
27
  const startPipeline = () => {
27
28
  pipeline === null || pipeline === void 0 ? void 0 : pipeline.close();
@@ -11,6 +11,7 @@ export default class VideoPlayer {
11
11
  private globalVolume;
12
12
  private videoClipPlayers;
13
13
  private activeClip?;
14
+ private pendingClip?;
14
15
  private parentElement;
15
16
  private sinkId;
16
17
  constructor(cogsConnection: CogsConnection, parentElement?: HTMLElement);
@@ -28,9 +29,6 @@ export default class VideoPlayer {
28
29
  setVideoClipVolume({ volume }: {
29
30
  volume: number;
30
31
  }): void;
31
- setVideoClipLoop({ loop }: {
32
- loop: true | undefined;
33
- }): void;
34
32
  setVideoClipFit({ fit }: {
35
33
  fit: MediaObjectFit;
36
34
  }): void;
@@ -75,35 +75,50 @@ class VideoPlayer {
75
75
  }
76
76
  setGlobalVolume(globalVolume) {
77
77
  Object.values(this.videoClipPlayers).forEach((clipPlayer) => {
78
- clipPlayer.videoElement.volume = clipPlayer.volume * globalVolume;
78
+ setVideoElementVolume(clipPlayer.videoElement, clipPlayer.volume * globalVolume);
79
79
  });
80
80
  this.globalVolume = globalVolume;
81
81
  this.notifyStateListeners();
82
82
  }
83
83
  playVideoClip(path, { playId, volume, loop, fit }) {
84
- if (this.activeClip) {
85
- if (this.activeClip.path !== path) {
86
- this.stopVideoClip();
87
- }
88
- }
84
+ var _a;
89
85
  if (!this.videoClipPlayers[path]) {
90
86
  this.videoClipPlayers[path] = this.createClipPlayer(path, { preload: 'none', ephemeral: true, fit });
91
87
  }
92
- this.activeClip = { path, playId };
88
+ // Check if there's already a pending clip, which has now been superseded and abort the play operation
89
+ if (this.pendingClip) {
90
+ this.updateVideoClipPlayer(this.pendingClip.path, (clipPlayer) => {
91
+ clipPlayer.videoElement.load(); // Resets the media element
92
+ return clipPlayer;
93
+ });
94
+ }
95
+ // New pending clip is video being requested
96
+ if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) !== path) {
97
+ this.pendingClip = { path, playId, actionOncePlaying: 'play' };
98
+ }
99
+ // Setup and play the pending clip's player
93
100
  this.updateVideoClipPlayer(path, (clipPlayer) => {
94
101
  clipPlayer.volume = volume;
95
- clipPlayer.videoElement.volume = volume * this.globalVolume;
102
+ setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
96
103
  clipPlayer.videoElement.loop = loop;
97
104
  clipPlayer.videoElement.style.objectFit = fit;
98
105
  if (clipPlayer.videoElement.currentTime === clipPlayer.videoElement.duration) {
99
106
  clipPlayer.videoElement.currentTime = 0;
100
107
  }
101
108
  clipPlayer.videoElement.play();
102
- clipPlayer.videoElement.style.display = 'block';
109
+ // Display right away if there's currently no active clip
110
+ if (!this.activeClip) {
111
+ clipPlayer.videoElement.style.display = 'block';
112
+ }
103
113
  return clipPlayer;
104
114
  });
105
115
  }
106
116
  pauseVideoClip() {
117
+ // Pending clip should be paused when it loads and becomes active
118
+ if (this.pendingClip) {
119
+ this.pendingClip.actionOncePlaying = 'pause';
120
+ }
121
+ // Pause the currently active clip
107
122
  if (this.activeClip) {
108
123
  const { playId, path } = this.activeClip;
109
124
  this.updateVideoClipPlayer(path, (clipPlayer) => {
@@ -115,41 +130,42 @@ class VideoPlayer {
115
130
  }
116
131
  }
117
132
  stopVideoClip() {
133
+ // Pending clip should be stopped when it loads and becomes active
134
+ if (this.pendingClip) {
135
+ this.pendingClip.actionOncePlaying = 'stop';
136
+ }
137
+ // Stop the currently active clip
118
138
  if (this.activeClip) {
119
139
  this.handleStoppedClip(this.activeClip.path);
120
140
  }
121
141
  }
122
142
  setVideoClipVolume({ volume }) {
123
- if (!this.activeClip) {
143
+ var _a, _b;
144
+ // If there is a pending clip, this is latest to have been played so update its volume
145
+ const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
146
+ if (!clipToUpdate) {
124
147
  return;
125
148
  }
126
149
  if (!(volume >= 0 && volume <= 1)) {
127
150
  console.warn('Invalid volume', volume);
128
151
  return;
129
152
  }
130
- this.updateVideoClipPlayer(this.activeClip.path, (clipPlayer) => {
153
+ this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
131
154
  if (clipPlayer.videoElement) {
132
- clipPlayer.videoElement.volume = volume * this.globalVolume;
133
- }
134
- return clipPlayer;
135
- });
136
- }
137
- setVideoClipLoop({ loop }) {
138
- if (!this.activeClip) {
139
- return;
140
- }
141
- this.updateVideoClipPlayer(this.activeClip.path, (clipPlayer) => {
142
- if (clipPlayer.videoElement) {
143
- clipPlayer.videoElement.loop = loop || false;
155
+ clipPlayer.volume = volume;
156
+ setVideoElementVolume(clipPlayer.videoElement, volume * this.globalVolume);
144
157
  }
145
158
  return clipPlayer;
146
159
  });
147
160
  }
148
161
  setVideoClipFit({ fit }) {
149
- if (!this.activeClip) {
162
+ var _a, _b;
163
+ // If there is a pending clip, this is latest to have been played so update its fit
164
+ const clipToUpdate = (_b = (_a = this.pendingClip) !== null && _a !== void 0 ? _a : this.activeClip) !== null && _b !== void 0 ? _b : undefined;
165
+ if (!clipToUpdate) {
150
166
  return;
151
167
  }
152
- this.updateVideoClipPlayer(this.activeClip.path, (clipPlayer) => {
168
+ this.updateVideoClipPlayer(clipToUpdate.path, (clipPlayer) => {
153
169
  if (clipPlayer.videoElement) {
154
170
  clipPlayer.videoElement.style.objectFit = fit;
155
171
  }
@@ -225,7 +241,7 @@ class VideoPlayer {
225
241
  preload: preloadString(newVideoPaths[path].preload),
226
242
  ephemeral: false,
227
243
  };
228
- player.videoElement.preload = player.config.preload ? 'auto' : 'none';
244
+ player.videoElement.preload = player.config.preload;
229
245
  return player;
230
246
  });
231
247
  }
@@ -235,7 +251,7 @@ class VideoPlayer {
235
251
  this.notifyStateListeners();
236
252
  }
237
253
  notifyStateListeners() {
238
- var _a, _b, _c, _d, _e, _f;
254
+ var _a, _b, _c, _d, _e, _f, _g;
239
255
  const VideoState = {
240
256
  globalVolume: this.globalVolume,
241
257
  isPlaying: this.activeClip ? !((_a = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _a === void 0 ? void 0 : _a.paused) : false,
@@ -245,7 +261,8 @@ class VideoPlayer {
245
261
  path: this.activeClip.path,
246
262
  state: !((_b = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _b === void 0 ? void 0 : _b.paused) ? VideoState_1.ActiveVideoClipState.Playing : VideoState_1.ActiveVideoClipState.Paused,
247
263
  loop: (_d = (_c = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _c === void 0 ? void 0 : _c.loop) !== null && _d !== void 0 ? _d : false,
248
- volume: (_f = (_e = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _e === void 0 ? void 0 : _e.volume) !== null && _f !== void 0 ? _f : 0,
264
+ volume: ((_e = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _e === void 0 ? void 0 : _e.muted) ? 0
265
+ : (_g = (_f = this.videoClipPlayers[this.activeClip.path].videoElement) === null || _f === void 0 ? void 0 : _f.volume) !== null && _g !== void 0 ? _g : 0,
249
266
  }
250
267
  : undefined,
251
268
  };
@@ -266,19 +283,58 @@ class VideoPlayer {
266
283
  }
267
284
  createVideoElement(path, config, { volume }) {
268
285
  const videoElement = document.createElement('video');
286
+ videoElement.playsInline = true; // Required for iOS
269
287
  videoElement.src = urls_1.assetUrl(path);
270
288
  videoElement.autoplay = false;
271
289
  videoElement.loop = false;
272
- videoElement.volume = this.globalVolume * volume;
290
+ setVideoElementVolume(videoElement, volume * this.globalVolume);
273
291
  videoElement.preload = config.preload;
274
292
  videoElement.addEventListener('playing', () => {
275
- var _a;
276
- if (((_a = this.activeClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
293
+ var _a, _b;
294
+ // If the clip is still the pending one when it actually start playing, then ensure it is in the correct state
295
+ if (((_a = this.pendingClip) === null || _a === void 0 ? void 0 : _a.path) === path) {
296
+ switch (this.pendingClip.actionOncePlaying) {
297
+ case 'play': {
298
+ // Continue playing, show the video element, and notify listeners
299
+ videoElement.style.display = 'block';
300
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'playing');
301
+ break;
302
+ }
303
+ case 'pause': {
304
+ // Pause playback, show the video element, and notify listeners
305
+ videoElement.style.display = 'block';
306
+ videoElement.pause();
307
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'paused');
308
+ break;
309
+ }
310
+ case 'stop': {
311
+ // Pause playback, leave the video element hidden, and notify listeners
312
+ videoElement.pause();
313
+ this.notifyClipStateListeners(this.pendingClip.playId, path, 'stopped');
314
+ break;
315
+ }
316
+ }
317
+ // If there was a previously active clip, then stop it
318
+ if (this.activeClip) {
319
+ this.handleStoppedClip(this.activeClip.path);
320
+ }
321
+ this.activeClip = this.pendingClip;
322
+ this.pendingClip = undefined;
323
+ }
324
+ else if (((_b = this.activeClip) === null || _b === void 0 ? void 0 : _b.path) === path) {
325
+ // If we were the active clip then just notify listeners that we are now playing
277
326
  this.notifyClipStateListeners(this.activeClip.playId, path, 'playing');
278
327
  }
328
+ else {
329
+ // Otherwise it shouldn't be playing, like because another clip became pending before we loaded,
330
+ // so we pause and don't show or notify listeners
331
+ videoElement.pause();
332
+ }
279
333
  });
280
334
  videoElement.addEventListener('ended', () => {
281
- if (!videoElement.loop) {
335
+ // Ignore if there's a pending clip, as once that starts playing the active clip will be stopped
336
+ // Also ignore if the video is set to loop
337
+ if (!this.pendingClip && !videoElement.loop) {
282
338
  this.handleStoppedClip(path);
283
339
  }
284
340
  });
@@ -315,7 +371,7 @@ class VideoPlayer {
315
371
  }
316
372
  exports.default = VideoPlayer;
317
373
  function preloadString(preload) {
318
- return typeof preload === 'string' ? preload : preload ? 'metadata' : 'none';
374
+ return typeof preload === 'string' ? preload : preload ? 'auto' : 'none';
319
375
  }
320
376
  function setPlayerSinkId(player, sinkId) {
321
377
  if (sinkId === undefined) {
@@ -325,3 +381,12 @@ function setPlayerSinkId(player, sinkId) {
325
381
  player.videoElement.setSinkId(sinkId);
326
382
  }
327
383
  }
384
+ /**
385
+ * Set video volume
386
+ *
387
+ * This doesn't work on iOS (volume is read-only) so at least mute it if the volume is zero
388
+ */
389
+ function setVideoElementVolume(videoElement, volume) {
390
+ videoElement.volume = volume;
391
+ videoElement.muted = volume === 0;
392
+ }