@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
|
@@ -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;
|