@brightspace-ui/labs 2.63.5 → 2.63.6

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.5"
115
+ "version": "2.63.6"
116
116
  }
@@ -51,6 +51,8 @@ A Lit element based media player component, designed for similarity across brows
51
51
  | poster | String | null | URL of the image to display in place of the media before it has loaded. |
52
52
  | src | String | | URL of the media to play. If multiple sources are desired, use `<source>` tags instead (see below). |
53
53
  | thumbnails | String | | If set, will show thumbnails on preview. See below for required format. |
54
+ | transcript-viewer | Boolean | false | If set and captions tracks are available, adds a "View transcript" item to the settings menu that toggles the transcript viewer. |
55
+ | transcript-viewer-on | Boolean | false | Whether the transcript viewer is shown. Reflected to the attribute. |
54
56
  | play-in-view | Boolean | false | If set, will stop the media playback if not in view
55
57
 
56
58
  ```
@@ -98,7 +100,10 @@ this.document.querySelector('d2l-labs-media-player').pause();
98
100
  | Event | Description |
99
101
  |--|--|
100
102
  | cuechange | Dispatched when the currently-displayed captions cue changes. |
103
+ | close-transcript | Dispatched when the transcript viewer's close button is clicked. |
101
104
  | descriptioncuechange | Dispatched when a new audio description cue is reached. Read the cue from `activeDescriptionCue`. |
105
+ | download-captions | Dispatched when "Captions (.vtt)" is selected in the transcript viewer's download menu. |
106
+ | download-transcript | Dispatched when "Transcript (.txt)" is selected in the transcript viewer's download menu. |
102
107
  | durationchange | Dispatched when the video or media displayed has changed its duration |
103
108
  | ended | Dispatched when the media has reached the end of its duration. |
104
109
  | error | Dispatched when the media failed to load. |
@@ -112,6 +117,7 @@ this.document.querySelector('d2l-labs-media-player').pause();
112
117
  | trackloaded | Dispatched when a track element has loaded. |
113
118
  | trackloadfailed | Dispatched when a track element could not be loaded from the provided src attribute. |
114
119
  | tracksmenuitemchanged | Dispatched when the tracks menu item has changed. |
120
+ | transcriptviewertoggle | Dispatched when the transcript viewer is opened or closed from within the player. |
115
121
  ```
116
122
  // Listen for the loadeddata event
117
123
 
@@ -0,0 +1,158 @@
1
+ import { css, html, nothing } from 'lit';
2
+ import { ifDefined } from 'lit/directives/if-defined.js';
3
+ import { styleMap } from 'lit/directives/style-map.js';
4
+
5
+ // Expects the host to provide _duration, _hovering, _hoverTime, _getPercentageTime() and _getTheme()
6
+ export const MediaPlayerChaptersMixin = superclass => class extends superclass {
7
+
8
+ static properties = {
9
+ metadata: { type: Object },
10
+ _chapters: { state: true },
11
+ };
12
+
13
+ static styles = [super.styles, css`
14
+ .chapter-marker, .chapter-marker-highlight {
15
+ cursor: pointer;
16
+ height: 6px;
17
+ pointer-events: none;
18
+ position: absolute;
19
+ top: 4px;
20
+ width: 3px;
21
+ z-index: 2;
22
+ }
23
+
24
+ .chapter-marker {
25
+ background-color: var(--d2l-color-ferrite);
26
+ }
27
+
28
+ .chapter-marker-highlight {
29
+ background-color: var(--d2l-color-celestine-minus-1);
30
+ }
31
+
32
+ .chapter-marker[theme="dark"] {
33
+ background-color: white;
34
+ }
35
+ `];
36
+
37
+ constructor() {
38
+ super();
39
+ this._chapters = [];
40
+ }
41
+
42
+ willUpdate(changedProperties) {
43
+ super.willUpdate(changedProperties);
44
+
45
+ if (changedProperties.has('metadata')) {
46
+ this._getMetadata();
47
+ }
48
+
49
+ // A chapter starting at the very end of the media has no visible range
50
+ if (this._chapters.at(-1)?.time === Math.floor(this._duration)) {
51
+ this._chapters = this._chapters.slice(0, -1);
52
+ }
53
+ }
54
+
55
+ _getChapterMarkersView() {
56
+ if (this._chapters.length === 0) return;
57
+
58
+ let start, end;
59
+ for (let i = 0; i < this._chapters.length; i++) {
60
+ if (i === this._chapters.length - 1) {
61
+ start = this._chapters[i].time;
62
+ break;
63
+ }
64
+ else if (this._hoverTime >= this._chapters[i].time && this._hoverTime < this._chapters[i + 1].time) {
65
+ start = this._chapters[i].time;
66
+ end = this._chapters[i + 1].time;
67
+ break;
68
+ }
69
+ }
70
+
71
+ return this._chapters.map(chapter => {
72
+ const highlight = this._hovering && this._hoverTime >= this._chapters[0].time && (chapter.time === start || chapter.time === end);
73
+ return chapter.time > 0 ? html`
74
+ <div
75
+ class=${highlight ? 'chapter-marker-highlight' : 'chapter-marker'}
76
+ theme="${ifDefined(this._getTheme())}"
77
+ style=${styleMap({ left: `${this._getPercentageTime(chapter.time)}%` })}
78
+ ></div>
79
+ ` : nothing;
80
+ });
81
+ }
82
+
83
+ _getChapterTitle() {
84
+ if (!(this._chapters.length > 0 && this._hoverTime >= this._chapters[0].time)) return;
85
+
86
+ const chapter = this._chapters.find((_chapter, index, chapters) => (
87
+ index === chapters.length - 1 || (this._hoverTime >= chapters[0].time && this._hoverTime < chapters[index + 1].time)
88
+ ));
89
+ const chapterTitle = chapter && chapter.title;
90
+
91
+ if (!chapterTitle) return;
92
+
93
+ if (typeof chapterTitle === 'string') {
94
+ return chapterTitle;
95
+ }
96
+
97
+ for (const locale in chapterTitle) {
98
+ if (locale.split('-')[0] === 'en') {
99
+ return chapterTitle[locale];
100
+ }
101
+ }
102
+ }
103
+
104
+ _getMetadata() {
105
+ if (!this.metadata) return;
106
+
107
+ const data = (typeof this.metadata === 'string' || this.metadata instanceof String) ? JSON.parse(this.metadata) : this.metadata;
108
+ if (!(data && data.chapters && data.chapters.length > 0)) return;
109
+ let chapters = data.chapters.map(({ time, title }) => {
110
+ return {
111
+ time: parseInt(time),
112
+ title
113
+ };
114
+ }).sort((a, b) => a.time - b.time);
115
+
116
+ if (!data.cuts) {
117
+ data.cuts = [];
118
+ }
119
+
120
+ // updating the chapter times based on the cuts, loops over all chapters per cut because it can change multiple chapters
121
+ let cutDiff = 0;
122
+ for (const cut of data.cuts) {
123
+ const cutIn = cut.in - cutDiff;
124
+
125
+ const newChapters = new Map(); // using map to preserve sort ordering
126
+
127
+ if (!cut.out) { // if cut is until the end of the video
128
+ for (const chapter of chapters) {
129
+ if (chapter.time < cutIn) {
130
+ newChapters.set(chapter.time, chapter.title);
131
+ }
132
+ }
133
+ } else {
134
+ const cutOut = cut.out - cutDiff;
135
+ const cutLength = cutOut - cutIn;
136
+
137
+ for (const chapter of chapters) {
138
+ let newTime = chapter.time;
139
+ if (chapter.time > cutIn && chapter.time <= cutOut) {
140
+ newTime = cutIn;
141
+ } else if (chapter.time > cutOut) {
142
+ newTime = chapter.time - cutLength;
143
+ }
144
+
145
+ newChapters.set(newTime, chapter.title);
146
+ }
147
+
148
+ cutDiff += cutLength;
149
+ }
150
+
151
+ chapters = [...newChapters].map(([chapterTime, chapterTitle]) => ({
152
+ time: chapterTime,
153
+ title: chapterTitle
154
+ }));
155
+ }
156
+ this._chapters = chapters;
157
+ }
158
+ };
@@ -0,0 +1,266 @@
1
+ import '@brightspace-ui/core/components/dropdown/dropdown-button-subtle.js';
2
+ import '@brightspace-ui/core/components/dropdown/dropdown-menu.js';
3
+ import '@brightspace-ui/core/components/icons/icon.js';
4
+ import '@brightspace-ui/core/components/menu/menu.js';
5
+ import '@brightspace-ui/core/components/menu/menu-item.js';
6
+ import { css, html, nothing } from 'lit';
7
+
8
+ // Expects the host to provide _media, _tracks, mediaType, currentTime, _selectedTrackIdentifier, _getSrclangFromTrackIdentifier(), _onCueChange() and localize()
9
+ export const MediaPlayerTranscriptMixin = superclass => class extends superclass {
10
+
11
+ static properties = {
12
+ transcriptViewer: { type: Boolean, attribute: 'transcript-viewer' },
13
+ transcriptViewerOn: { type: Boolean, attribute: 'transcript-viewer-on', reflect: true },
14
+ };
15
+
16
+ static styles = [super.styles, css`
17
+ :host([transcript-viewer-on]) #video,
18
+ :host([transcript-viewer-on]) #video-poster {
19
+ height: 30%;
20
+ left: 0;
21
+ min-height: 30%;
22
+ position: absolute;
23
+ top: 0;
24
+ width: 30%;
25
+ }
26
+ .transcript-cue-container {
27
+ padding-left: 10px;
28
+ }
29
+ .video-transcript-cue {
30
+ padding-left: 5px;
31
+ }
32
+ .audio-transcript-cue {
33
+ padding-left: 5px;
34
+ }
35
+ .video-transcript-cue[active] {
36
+ background-color: gray;
37
+ box-shadow: -5px 0 0 white;
38
+ }
39
+ .audio-transcript-cue[active] {
40
+ background-color: lightgray;
41
+ box-shadow: -5px 0 0 black;
42
+ }
43
+ #video-transcript-viewer {
44
+ bottom: 55px;
45
+ color: white;
46
+ overflow-anchor: none;
47
+ overflow-y: auto;
48
+ position: absolute;
49
+ right: 0;
50
+ top: 50px;
51
+ width: 65%;
52
+ z-index: 1;
53
+ }
54
+ #audio-transcript-viewer {
55
+ bottom: 60px;
56
+ color: black;
57
+ overflow-anchor: none;
58
+ overflow-y: auto;
59
+ position: absolute;
60
+ right: 0;
61
+ top: 45px;
62
+ width: 100%;
63
+ z-index: 1;
64
+ }
65
+ #close-transcript {
66
+ position: absolute;
67
+ right: 7px;
68
+ top: 0;
69
+ z-index: 1;
70
+ }
71
+ #video-transcript-download-button {
72
+ left: 35%;
73
+ position: absolute;
74
+ top: 5px;
75
+ z-index: 2;
76
+ }
77
+ #audio-transcript-download-button {
78
+ left: 10px;
79
+ position: absolute;
80
+ top: 0;
81
+ z-index: 2;
82
+ }
83
+ #audio-transcript-download-menu {
84
+ left: 35px;
85
+ }
86
+ #video-close-transcript-icon {
87
+ color: white;
88
+ }
89
+ #audio-close-transcript-icon {
90
+ color: black;
91
+ }
92
+ `];
93
+
94
+ constructor() {
95
+ super();
96
+ this.afterCaptions = [];
97
+ this.beforeCaptions = [];
98
+ this.transcriptViewer = false;
99
+ this.transcriptViewerOn = false;
100
+ }
101
+
102
+ willUpdate(changedProperties) {
103
+ super.willUpdate(changedProperties);
104
+ // The viewer element is re-created each time it is shown
105
+ if (changedProperties.has('transcriptViewerOn')) this._transcriptViewer = null;
106
+ }
107
+
108
+ _closeTranscript() {
109
+ this._setTranscriptViewerOn(false);
110
+ this.dispatchEvent(new CustomEvent('close-transcript', { bubbles: true, composed: true }));
111
+ }
112
+
113
+ _downloadCaptions() {
114
+ this.dispatchEvent(new CustomEvent('download-captions', { bubbles: true, composed: true }));
115
+ }
116
+
117
+ _downloadTranscript() {
118
+ this.dispatchEvent(new CustomEvent('download-transcript', { bubbles: true, composed: true }));
119
+ }
120
+
121
+ async _onTranscriptCueChange() {
122
+ if (!this._transcriptViewer) {
123
+ this._transcriptViewer = this.shadowRoot.getElementById('video-transcript-viewer')
124
+ || this.shadowRoot.getElementById('audio-transcript-viewer');
125
+ }
126
+ this._updateTranscriptViewerCues();
127
+ await this.requestUpdate();
128
+ this._scrollTranscriptViewer();
129
+ }
130
+
131
+ _renderTranscriptViewer() {
132
+ if (!this._media) {
133
+ return;
134
+ }
135
+ const captionsMenu = this.shadowRoot.getElementById('captions-menu');
136
+ if (captionsMenu) {
137
+ this._captionsMenuReturnItem = captionsMenu.shadowRoot.querySelector('d2l-menu-item-return');
138
+ this._captionsMenuReturnItem?.setAttribute('text', this.localize('components:mediaPlayer:language'));
139
+ }
140
+
141
+ if (!this._transcriptViewer) {
142
+ this._onCueChange();
143
+ }
144
+
145
+ const isVideo = this.mediaType === 'video';
146
+ const captionsToHtml = (item) => {
147
+ const updateTime = async() => {
148
+ this.currentTime = item.startTime;
149
+ this._media.currentTime = item.startTime;
150
+ };
151
+ return html`
152
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'}
153
+ @click=${updateTime}>
154
+ ${item.text}<br>
155
+ </div>`;
156
+ };
157
+
158
+ return html`
159
+ <span id="close-transcript"
160
+ @click=${this._closeTranscript}>
161
+ <d2l-icon class="d2l-button-icon"
162
+ id=${isVideo ? 'video-close-transcript-icon' : 'audio-close-transcript-icon'}
163
+ icon="tier1:close-small"></d2l-icon>
164
+ </span>
165
+ <div
166
+ id=${isVideo ? 'video-transcript-viewer' : 'audio-transcript-viewer'}
167
+ >
168
+ <div class="transcript-cue-container">
169
+ ${this.beforeCaptions.map(captionsToHtml)}
170
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'} active
171
+ id="transcript-viewer-active-cue">
172
+ ${this.transcriptActiveCue?.text}
173
+ </div>
174
+ ${this.afterCaptions.map(captionsToHtml)}
175
+ </div>
176
+ </div>
177
+ <d2l-dropdown-button-subtle
178
+ id=${isVideo ? 'video-transcript-download-button' : 'audio-transcript-download-button'}
179
+ text="${this.localize('components:mediaPlayer:download')}">
180
+ <d2l-dropdown-menu id=${isVideo ? 'video-transcript-download-menu' : 'audio-transcript-download-menu'}>
181
+ <d2l-menu>
182
+ <d2l-menu-item @click=${this._downloadTranscript} text="${this.localize('components:mediaPlayer:transcriptTxt')}"></d2l-menu-item>
183
+ <d2l-menu-item @click=${this._downloadCaptions} text="${this.localize('components:mediaPlayer:captionsVtt')}"></d2l-menu-item>
184
+ </d2l-menu>
185
+ </d2l-dropdown-menu>
186
+ </d2l-dropdown-button-subtle>
187
+ `;
188
+ }
189
+
190
+ _renderTranscriptViewerMenuItem() {
191
+ if (!this.transcriptViewer || this._tracks.length === 0) return nothing;
192
+ return html`
193
+ <d2l-menu-item
194
+ id="transcript-viewer-menu-item"
195
+ text="${this.transcriptViewerOn ? this.localize('components:mediaPlayer:hideTranscript') : this.localize('components:mediaPlayer:viewTranscript')}"
196
+ @d2l-menu-item-select=${this._toggleTranscriptViewer}
197
+ ></d2l-menu-item>
198
+ `;
199
+ }
200
+
201
+ _scrollTranscriptViewer() {
202
+ const cue = this.shadowRoot.getElementById('transcript-viewer-active-cue');
203
+ const cueRect = cue?.getBoundingClientRect();
204
+ const transcriptRect = this._transcriptViewer?.getBoundingClientRect();
205
+ if (cue && cueRect && transcriptRect) {
206
+ if (cueRect.bottom > transcriptRect.bottom && cueRect.height <= transcriptRect.height) {
207
+ this._transcriptViewer.scrollBy({ top: cueRect.bottom - transcriptRect.bottom + transcriptRect.height - cueRect.height, left: 0, behavior: 'smooth' });
208
+ } else if (cueRect.top < transcriptRect.top) {
209
+ this._transcriptViewer.scrollBy({ top: cueRect.top - transcriptRect.top, left: 0, behavior: 'smooth' });
210
+ }
211
+ }
212
+ }
213
+
214
+ _setTranscriptViewerOn(on) {
215
+ if (this.transcriptViewerOn === on) return;
216
+ this.transcriptViewerOn = on;
217
+ this.dispatchEvent(new CustomEvent('transcriptviewertoggle', { bubbles: true, composed: true, detail: { on } }));
218
+ }
219
+
220
+ _toggleTranscriptViewer() {
221
+ this._setTranscriptViewerOn(!this.transcriptViewerOn);
222
+ }
223
+
224
+ _updateTranscriptViewerCues() {
225
+ let cues = null;
226
+ const lang = this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
227
+ for (let i = 0; i < this._media.textTracks.length; i += 1) {
228
+ const currTrack = this._media.textTracks[i];
229
+ if (currTrack?.cues) {
230
+ const activeCues = currTrack.activeCues;
231
+ if (lang === currTrack.language) {
232
+ this.transcriptActiveCue = activeCues?.[activeCues?.length - 1];
233
+ cues = currTrack.cues;
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ if (!cues) {
239
+ let defaultTrack;
240
+ for (let i = 0; i < this._media.textTracks.length; i++) {
241
+ if (this._media.textTracks[i].default) {
242
+ defaultTrack = this._media.textTracks[i];
243
+ break;
244
+ }
245
+ }
246
+ defaultTrack = defaultTrack || this._media.textTracks[0];
247
+ if (defaultTrack) defaultTrack.mode = 'hidden';
248
+ this._selectedTrackIdentifier = { kind: defaultTrack?.kind, srclang: defaultTrack?.language };
249
+ this.requestUpdate();
250
+ return;
251
+ }
252
+
253
+ this.beforeCaptions = [];
254
+ this.afterCaptions = [];
255
+ for (let i = 0; i < cues.length; i += 1) {
256
+ const currCue = cues[i];
257
+ const currTime = this._media?.currentTime;
258
+ const before = currCue !== this.transcriptActiveCue && (currCue.endTime < currTime || currCue.endTime <= this.transcriptActiveCue?.endTime);
259
+ if (before) {
260
+ this.beforeCaptions.push(currCue);
261
+ } else if (currCue !== this.transcriptActiveCue) {
262
+ this.afterCaptions.push(currCue);
263
+ }
264
+ }
265
+ }
266
+ };