@brightspace-ui/labs 2.63.1 → 2.63.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.
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.1"
115
+ "version": "2.63.2"
116
116
  }
@@ -42,6 +42,7 @@ A Lit element based media player component, designed for similarity across brows
42
42
  | crossorigin | String | null | If set, will set the `crossorigin` attribute on the underlying media element to the set value.
43
43
  | download-filename | String | null | If set along with `allow-download`, will use the provided value as the base of the filename (the extension will be automatically appended)
44
44
  | duration-hint | Number | 1 | Measured in seconds. If set and the duration cannot be determined automatically, this value will be used instead.
45
+ | hide-audio-description-selection | Boolean | false | If set, the button to configure audio descriptions is hidden. |
45
46
  | hide-captions-selection | Boolean | false | If set, the menu item to configure captions is hidden. |
46
47
  | hide-seek-bar | Boolean | false | If set, the seek bar will not be shown. |
47
48
  | loop | Boolean | false | If set, once the media has finished playing it will replay from the beginning. |
@@ -64,6 +65,7 @@ A Lit element based media player component, designed for similarity across brows
64
65
  |--|--|--|--|
65
66
  | currentTime | Number | Get & Set | Current time playback time of the media in seconds. |
66
67
  | activeCue | Object | Get | VTTCue instance for the currently-displayed captions cue. If no cue is currently displayed, the value is null. |
68
+ | activeDescriptionCue | Object | Get | The most recently reached audio description cue (`{ text, time }`). Reset to null on seek or when the audio description language changes. |
67
69
  | duration | Number | Get | Total duration of the media in seconds. |
68
70
  | ended | Boolean | Get | Whether or not the video has ended. |
69
71
  | paused | Boolean | Get | Whether or not the video is currently paused. |
@@ -96,6 +98,7 @@ this.document.querySelector('d2l-labs-media-player').pause();
96
98
  | Event | Description |
97
99
  |--|--|
98
100
  | cuechange | Dispatched when the currently-displayed captions cue changes. |
101
+ | descriptioncuechange | Dispatched when a new audio description cue is reached. Read the cue from `activeDescriptionCue`. |
99
102
  | durationchange | Dispatched when the video or media displayed has changed its duration |
100
103
  | ended | Dispatched when the media has reached the end of its duration. |
101
104
  | error | Dispatched when the media failed to load. |
@@ -16,6 +16,11 @@ export const AUDIO_DESCRIPTION_TRACK_KIND = 'descriptions';
16
16
  const PREFERENCES_AUDIO_DESCRIPTION_LANGUAGE_KEY = 'D2L.MediaPlayer.Preferences.AudioDescriptionLanguage';
17
17
  const VOICES_LOAD_TIMEOUT_MS = 1000;
18
18
  const AUDIO_DESCRIPTION_CONTROLS_IN_MENU_MAX_WIDTH_PX = 768;
19
+ // macOS Eloquence and novelty voices, which Safari lists alphabetically ahead of the natural-sounding ones
20
+ const LOW_QUALITY_VOICE_NAMES = new Set([
21
+ 'albert', 'bad news', 'bahh', 'bells', 'boing', 'bubbles', 'cellos', 'eddy', 'flo', 'fred', 'good news', 'grandma', 'grandpa',
22
+ 'jester', 'junior', 'kathy', 'organ', 'ralph', 'reed', 'rocko', 'sandy', 'shelley', 'superstar', 'trinoids', 'whisper', 'wobble', 'zarvox'
23
+ ]);
19
24
 
20
25
  export const MediaPlayerAudioDescriptionsMixin = superclass => class extends superclass {
21
26
 
@@ -45,7 +50,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
45
50
  }
46
51
  .audio-description-header {
47
52
  background-color: var(--d2l-color-tungsten);
48
- border-bottom: 1px solid var(--d2l-color-mica);
53
+ border-bottom: 1px solid var(--d2l-color-chromite);
49
54
  border-start-end-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
50
55
  border-start-start-radius: var(--d2l-popover-border-radius, var(--d2l-popover-default-border-radius));
51
56
  color: white;
@@ -65,6 +70,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
65
70
  constructor() {
66
71
  super();
67
72
 
73
+ this._activeDescriptionCue = null;
68
74
  this._audioDescriptionTracks = [];
69
75
  this._audioDescriptionIndex = 0;
70
76
  this._audioDescriptionPreviousTime = -0.001;
@@ -74,6 +80,10 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
74
80
  this._canReplayAudioDescription = false;
75
81
  }
76
82
 
83
+ get activeDescriptionCue() {
84
+ return this._activeDescriptionCue;
85
+ }
86
+
77
87
  connectedCallback() {
78
88
  super.connectedCallback();
79
89
  this.#audioDescriptionResizeObserver.observe(this);
@@ -101,7 +111,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
101
111
  }
102
112
 
103
113
  _getAudioDescriptionsButtonView() {
104
- if (!this._audioDescriptionTracks?.length) return null;
114
+ if (!this._audioDescriptionTracks?.length || this.hideAudioDescriptionSelection) return null;
105
115
 
106
116
  const tooltip = this.localize('components:mediaPlayer:audioDescription');
107
117
  const replayTooltip = this.localize('components:mediaPlayer:replayAudioDescription');
@@ -111,28 +121,57 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
111
121
  return html`
112
122
  ${showControls && !this._audioDescriptionControlsInMenu ? html`
113
123
  <d2l-button-icon
114
- icon="tier1:undo"
115
124
  id="audio-description-replay-button"
125
+ text="${this.localize('components:mediaPlayer:replay')}"
116
126
  theme="${ifDefined(this._getTheme())}"
117
127
  disabled-tooltip="${replayTooltip}"
118
128
  ?disabled="${!this._audioDescriptionPlaying}"
119
129
  @click="${this._replayAudioDescription}"
120
- ></d2l-button-icon>
130
+ ><d2l-icon-custom slot="icon"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
131
+ <g clip-path="url(#clip0_777_1621)">
132
+ <mask id="mask0_777_1621" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="2" width="18" height="14">
133
+ <path d="M0 2H18V16H0V2Z" fill="white"/>
134
+ </mask>
135
+ <g mask="url(#mask0_777_1621)">
136
+ <path d="M14.846 2C16.036 2 17 2.73967 17 3.65278V14.3472C17 14.957 16.563 15.517 15.862 15.8048C15.162 16.0926 14.312 16.0607 13.651 15.7223L3.958 10.3751C3.66426 10.2256 3.42291 10.0217 3.25568 9.78167C3.08845 9.54163 3.00059 9.27302 3 9C3 8.44778 3.36 7.93133 3.96 7.62489L13.651 2.27767C14.0059 2.09624 14.4213 1.9997 14.846 2Z" fill="#F1F5FB"/>
137
+ </g>
138
+ <path d="M2 1V17" stroke="#F1F5FB" stroke-width="2" stroke-linecap="round"/>
139
+ </g>
140
+ <defs>
141
+ <clipPath id="clip0_777_1621">
142
+ <rect width="18" height="18" fill="white"/>
143
+ </clipPath>
144
+ </defs>
145
+ </svg></d2l-icon-custom></d2l-button-icon>
121
146
  <d2l-button-icon
122
- icon="tier1:redo"
123
147
  id="audio-description-skip-button"
148
+ text="${this.localize('components:mediaPlayer:skip')}"
124
149
  theme="${ifDefined(this._getTheme())}"
125
- disabled-tooltip="${skipTooltip}"
150
+ disabled-tooltip="${skipTooltip}"
126
151
  ?disabled="${!this._audioDescriptionPlaying}"
127
152
  @click="${this._skipAudioDescription}"
128
- ></d2l-button-icon>
153
+ ><d2l-icon-custom slot="icon"><svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
154
+ <g clip-path="url(#clip0_777_1614)">
155
+ <mask id="mask0_777_1614" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="2" width="18" height="14">
156
+ <path d="M18 2H0V16H18V2Z" fill="white"/>
157
+ </mask>
158
+ <g mask="url(#mask0_777_1614)">
159
+ <path d="M3.154 2C1.964 2 1 2.73967 1 3.65278V14.3472C1 14.957 1.437 15.517 2.138 15.8048C2.838 16.0926 3.688 16.0607 4.349 15.7223L14.042 10.3751C14.3357 10.2256 14.5771 10.0217 14.7443 9.78167C14.9115 9.54163 14.9994 9.27302 15 9C15 8.44778 14.64 7.93133 14.04 7.62489L4.349 2.27767C3.9941 2.09624 3.5787 1.9997 3.154 2Z" fill="#F1F5FB"/>
160
+ </g>
161
+ <path d="M16 1V17" stroke="#F1F5FB" stroke-width="2" stroke-linecap="round"/>
162
+ </g>
163
+ <defs>
164
+ <clipPath id="clip0_777_1614">
165
+ <rect width="18" height="18" fill="white" transform="matrix(-1 0 0 1 18 0)"/>
166
+ </clipPath>
167
+ </defs>
168
+ </svg></d2l-icon-custom></d2l-button-icon>
129
169
  ` : null}
130
170
  <d2l-dropdown>
131
171
  <d2l-button-icon
132
- aria-label="${tooltip}"
133
172
  class="d2l-dropdown-opener"
134
173
  id="audio-description-button"
135
- title="${tooltip}"
174
+ text="${tooltip}"
136
175
  theme="${ifDefined(this._getTheme())}"
137
176
  ?data-enabled="${!!this._selectedAudioDescriptionLanguage}"
138
177
  ><d2l-icon-custom slot="icon"><svg width="28" height="14" viewBox="0 0 28 14" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -217,11 +256,14 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
217
256
  this._audioDescriptionIndex = track.descriptions.findIndex(description => description.time >= currentTime);
218
257
  if (this._audioDescriptionIndex < 0) this._audioDescriptionIndex = track.descriptions.length;
219
258
  this._cancelAudioDescription();
259
+ this._activeDescriptionCue = null;
220
260
  }
221
261
 
222
262
  const next = track.descriptions[this._audioDescriptionIndex];
223
263
  if (next && this._audioDescriptionPreviousTime < next.time && currentTime >= next.time) {
224
264
  this._audioDescriptionIndex += 1;
265
+ this._activeDescriptionCue = next;
266
+ this.dispatchEvent(new CustomEvent('descriptioncuechange'));
225
267
  this.#speakAudioDescription(next, track);
226
268
  }
227
269
  this._audioDescriptionPreviousTime = currentTime;
@@ -242,6 +284,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
242
284
 
243
285
  _resetAudioDescriptionCursor(time) {
244
286
  this._cancelAudioDescription();
287
+ this._activeDescriptionCue = null;
245
288
  const track = this._audioDescriptionTracks?.find(track => track.srclang === this._selectedAudioDescriptionLanguage);
246
289
  this._audioDescriptionIndex = track
247
290
  ? track.descriptions.findIndex(description => description.time >= time)
@@ -272,13 +315,34 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
272
315
  const voices = window.speechSynthesis.getVoices();
273
316
  if (!voices.length) return null;
274
317
 
275
- const bestMatch = voices.find(voice => (voice.lang || '').toLowerCase() === normalizedLanguage)
276
- || voices.find(voice => (voice.lang || '').toLowerCase().startsWith(`${normalizedLanguage.split('-')[0]}-`))
277
- || voices.find(voice => (voice.lang || '').toLowerCase().startsWith(`${normalizedLanguage.split('-')[0]}_`))
278
- || voices.find(voice => (voice.lang || '').toLowerCase().startsWith(normalizedLanguage.split('-')[0]))
279
- || voices.find(voice => (voice.lang || '').toLowerCase().includes(normalizedLanguage.split('-')[0]))
280
- || voices[0];
281
- return bestMatch || null;
318
+ const baseLanguage = normalizedLanguage.split('-')[0];
319
+ const getLanguageScore = voice => {
320
+ const voiceLanguage = (voice.lang || '').toLowerCase();
321
+ if (voiceLanguage === normalizedLanguage) return 4;
322
+ if (voiceLanguage.startsWith(`${baseLanguage}-`) || voiceLanguage.startsWith(`${baseLanguage}_`)) return 3;
323
+ if (voiceLanguage.startsWith(baseLanguage)) return 2;
324
+ if (voiceLanguage.includes(baseLanguage)) return 1;
325
+ return 0;
326
+ };
327
+ const getQualityScore = voice => {
328
+ const uri = (voice.voiceURI || '').toLowerCase();
329
+ const name = (voice.name || '').toLowerCase().split(' (')[0];
330
+ if (uri.includes('eloquence') || uri.includes('speech.synthesis.voice') || LOW_QUALITY_VOICE_NAMES.has(name)) return 0;
331
+ if (uri.includes('premium') || name.includes('premium')) return 3;
332
+ if (uri.includes('enhanced') || name.includes('enhanced')) return 2;
333
+ return 1;
334
+ };
335
+
336
+ let bestMatch = voices[0];
337
+ let bestScore = -1;
338
+ for (const voice of voices) {
339
+ const score = getLanguageScore(voice) * 10 + getQualityScore(voice);
340
+ if (score > bestScore) {
341
+ bestMatch = voice;
342
+ bestScore = score;
343
+ }
344
+ }
345
+ return bestMatch;
282
346
  }
283
347
 
284
348
  #getSelectedAudioDescriptionTrack() {
@@ -329,6 +393,7 @@ export const MediaPlayerAudioDescriptionsMixin = superclass => class extends sup
329
393
  const voice = this.#getAudioDescriptionVoice(track.srclang);
330
394
  if (voice) utterance.voice = voice;
331
395
  utterance.volume = this.volume;
396
+ utterance.rate = this._media?.playbackRate || 1;
332
397
  window.speechSynthesis.speak(utterance);
333
398
  };
334
399
 
@@ -84,6 +84,7 @@ class MediaPlayer extends LocalizeLabsElement(MediaPlayerAudioDescriptionsMixin(
84
84
  crossorigin: { type: String },
85
85
  downloadFilename: { type: String, attribute: 'download-filename' },
86
86
  durationHint: { type: Number, attribute: 'duration-hint' },
87
+ hideAudioDescriptionSelection: { type: Boolean, attribute: 'hide-audio-description-selection' },
87
88
  hideCaptionsSelection: { type: Boolean, attribute: 'hide-captions-selection' },
88
89
  hideSeekBar: { type: Boolean, attribute: 'hide-seek-bar' },
89
90
  loop: { type: Boolean },
@@ -809,7 +810,7 @@ class MediaPlayer extends LocalizeLabsElement(MediaPlayerAudioDescriptionsMixin(
809
810
  const mediaControlsClass = { 'hidden': this._hidingCustomControls() };
810
811
  const theme = this.mediaType === SOURCE_TYPES.video ? 'dark' : undefined;
811
812
  const volumeLevelContainerClass = { 'hidden': !this._usingVolumeContainer || this._hidingCustomControls() };
812
- const hideSearchForAudioDescriptions = this._audioDescriptionTracks.length > 0 && this._audioDescriptionControlsInMenu;
813
+ const hideSearchForAudioDescriptions = !this.hideAudioDescriptionSelection && this._audioDescriptionTracks.length > 0 && this._audioDescriptionControlsInMenu;
813
814
  const searchContainerClass = { 'search-container-hidden' : !this._searchInstances[this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier)] || hideSearchForAudioDescriptions };
814
815
  this._captionsMenuReturnItem?.setAttribute('text', (this.transcriptViewerOn ? this.localize('components:mediaPlayer:language') : this.localize('components:mediaPlayer:captions')));
815
816