@brightspace-ui/labs 2.63.7 → 2.64.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.
package/package.json CHANGED
@@ -112,5 +112,5 @@
112
112
  "parse-srt": "^1.0.0-alpha",
113
113
  "webvtt-parser": "^2"
114
114
  },
115
- "version": "2.63.7"
115
+ "version": "2.64.0"
116
116
  }
@@ -16,6 +16,8 @@ export const AUDIO_DESCRIPTION_SKIP_KEY = 's';
16
16
  export const AUDIO_DESCRIPTION_TRACK_KIND = 'descriptions';
17
17
  const PREFERENCES_AUDIO_DESCRIPTION_LANGUAGE_KEY = 'D2L.MediaPlayer.Preferences.AudioDescriptionLanguage';
18
18
  const VOICES_LOAD_TIMEOUT_MS = 1000;
19
+ const AUDIO_DESCRIPTION_START_TIMEOUT_MS = 1500;
20
+ const AUDIO_DESCRIPTION_END_POLL_MS = 500;
19
21
  const AUDIO_DESCRIPTION_CONTROLS_IN_MENU_MAX_WIDTH_PX = 768;
20
22
  // macOS Eloquence and novelty voices, which Safari lists alphabetically ahead of the natural-sounding ones
21
23
  const LOW_QUALITY_VOICE_NAMES = new Set([
@@ -23,6 +25,8 @@ const LOW_QUALITY_VOICE_NAMES = new Set([
23
25
  'jester', 'junior', 'kathy', 'organ', 'ralph', 'reed', 'rocko', 'sandy', 'shelley', 'superstar', 'trinoids', 'whisper', 'wobble', 'zarvox'
24
26
  ]);
25
27
 
28
+ let speechUnlocked = false;
29
+
26
30
  export const MediaPlayerAudioDescriptionsMixin = superclass => class extends superclass {
27
31
 
28
32
  static properties = {
@@ -352,6 +356,16 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
352
356
  else this._pauseAudioDescription();
353
357
  }
354
358
 
359
+ // iOS Safari only speaks after speak() has been called once from a user gesture
360
+ _unlockAudioDescriptionSpeech() {
361
+ if (speechUnlocked || !this._selectedAudioDescriptionLanguage || !window.speechSynthesis || !window.SpeechSynthesisUtterance) return;
362
+ if (navigator.userActivation && !navigator.userActivation.isActive) return;
363
+ speechUnlocked = true;
364
+ const utterance = new window.SpeechSynthesisUtterance('');
365
+ utterance.volume = 0;
366
+ window.speechSynthesis.speak(utterance);
367
+ }
368
+
355
369
  #getAudioDescriptionVoice(language) {
356
370
  const normalizedLanguage = (language || '').toLowerCase();
357
371
  if (!normalizedLanguage || !window.speechSynthesis?.getVoices) return null;
@@ -405,6 +419,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
405
419
  #onAudioDescriptionMenuItemChange(e) {
406
420
  this._cancelAudioDescription();
407
421
  this._selectedAudioDescriptionLanguage = e.target.value || null;
422
+ this._unlockAudioDescriptionSpeech();
408
423
  this._resetAudioDescriptionCursor(this.currentTime);
409
424
  if (this._selectedAudioDescriptionLanguage) {
410
425
  this._setPreference(PREFERENCES_AUDIO_DESCRIPTION_LANGUAGE_KEY, this._selectedAudioDescriptionLanguage);
@@ -415,7 +430,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
415
430
  this.shadowRoot?.querySelector('#audio-description-dropdown-content')?.close();
416
431
  }
417
432
 
418
- #speakAudioDescription(description, track) {
433
+ #speakAudioDescription(description, track, isRetry = false) {
419
434
  if (!window.speechSynthesis || !window.SpeechSynthesisUtterance) return;
420
435
 
421
436
  this._cancelAudioDescription();
@@ -440,6 +455,18 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
440
455
  };
441
456
  utterance.onend = finish;
442
457
  utterance.onerror = finish;
458
+ let started = false;
459
+ utterance.onstart = () => {
460
+ started = true;
461
+ // Safari can drop onend (e.g. when iOS backgrounds the page), which would leave the video paused
462
+ const poll = setInterval(() => {
463
+ if (this._audioDescriptionUtterance !== utterance) clearInterval(poll);
464
+ else if (!this._audioDescriptionPaused && !window.speechSynthesis.speaking) {
465
+ clearInterval(poll);
466
+ finish();
467
+ }
468
+ }, AUDIO_DESCRIPTION_END_POLL_MS);
469
+ };
443
470
 
444
471
  const speak = () => {
445
472
  if (this._audioDescriptionUtterance !== utterance) return;
@@ -448,6 +475,19 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
448
475
  utterance.volume = this.volume;
449
476
  utterance.rate = this._media?.playbackRate || 1;
450
477
  window.speechSynthesis.speak(utterance);
478
+ // Chrome's paused state is browser-wide and survives reloads, and resume() is ignored until something is queued
479
+ if (!this._audioDescriptionPaused) window.speechSynthesis.resume?.();
480
+ if (isRetry) return;
481
+
482
+ setTimeout(() => {
483
+ if (started || this._audioDescriptionUtterance !== utterance || this._audioDescriptionPaused) return;
484
+ const pausedVideo = this._audioDescriptionPausedVideo;
485
+ // Detach first so the cancelled utterance's error event doesn't resume the video
486
+ this._audioDescriptionUtterance = null;
487
+ window.speechSynthesis.cancel();
488
+ this.#speakAudioDescription(description, track, true);
489
+ if (pausedVideo) this._audioDescriptionPausedVideo = true;
490
+ }, AUDIO_DESCRIPTION_START_TIMEOUT_MS);
451
491
  };
452
492
 
453
493
  if (window.speechSynthesis.getVoices().length) {
@@ -1935,6 +1935,7 @@ class MediaPlayer extends LocalizeLabsElement(MediaPlayerTranscriptMixin(MediaPl
1935
1935
 
1936
1936
  _togglePlay() {
1937
1937
  this._posterVisible = false;
1938
+ this._unlockAudioDescriptionSpeech();
1938
1939
  if (this._audioDescriptionPausedVideo) {
1939
1940
  this._toggleAudioDescriptionPause();
1940
1941
  return;