@brightspace-ui/labs 2.19.1 → 2.20.1

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.
@@ -0,0 +1,2329 @@
1
+ import '@brightspace-ui/core/components/alert/alert.js';
2
+ import '@brightspace-ui/core/components/button/button-icon.js';
3
+ import '@brightspace-ui/core/components/colors/colors.js';
4
+ import '@brightspace-ui/core/components/dropdown/dropdown.js';
5
+ import '@brightspace-ui/core/components/dropdown/dropdown-menu.js';
6
+ import '@brightspace-ui/core/components/dropdown/dropdown-button-subtle.js';
7
+ import '@brightspace-ui/core/components/icons/icon.js';
8
+ import '@brightspace-ui/core/components/loading-spinner/loading-spinner.js';
9
+ import '@brightspace-ui/core/components/menu/menu.js';
10
+ import '@brightspace-ui/core/components/menu/menu-item.js';
11
+ import '@brightspace-ui/core/components/menu/menu-item-link.js';
12
+ import '@brightspace-ui/core/components/menu/menu-item-radio.js';
13
+ import '@brightspace-ui/core/components/offscreen/offscreen.js';
14
+ import './slider-bar.js';
15
+ import 'webvtt-parser';
16
+ import './media-player-audio-bars.js';
17
+ import { css, html, LitElement, unsafeCSS } from 'lit';
18
+ import { classMap } from 'lit/directives/class-map.js';
19
+ import fullscreenApi from './fullscreen-api.js';
20
+ import Fuse from 'fuse.js';
21
+ import { getFocusPseudoClass } from '@brightspace-ui/core/helpers/focus.js';
22
+ import { ifDefined } from 'lit/directives/if-defined.js';
23
+ import { labelStyles } from '@brightspace-ui/core/components/typography/styles.js';
24
+ import { LocalizeLabsElement } from '../localize-labs-element.js';
25
+ import parseSRT from 'parse-srt/src/parse-srt.js';
26
+ import ResizeObserver from 'resize-observer-polyfill';
27
+ import { RtlMixin } from '@brightspace-ui/core/mixins/rtl-mixin.js';
28
+ import { styleMap } from 'lit/directives/style-map.js';
29
+
30
+ const DEFAULT_SPEED = '1.0';
31
+ const DEFAULT_VOLUME = '1.0';
32
+ const FULLSCREEN_ENABLED = fullscreenApi.isEnabled;
33
+ const HIDE_DELAY_MS = 3000;
34
+ const KEY_BINDINGS = {
35
+ play: 'k',
36
+ mute: 'm',
37
+ fullscreen: 'f'
38
+ };
39
+ const MIN_TRACK_WIDTH_PX = 250;
40
+ const IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform);
41
+ const PLAYBACK_SPEEDS = ['0.25', '0.5', '0.75', DEFAULT_SPEED, '1.25', '1.5', '2.0'];
42
+ const PREFERENCES_KEY_PREFIX = 'D2L.MediaPlayer.Preferences';
43
+ const PREFERENCES_SPEED_KEY = `${PREFERENCES_KEY_PREFIX}.Speed`;
44
+ const PREFERENCES_TRACK_IDENTIFIER_KEY = `${PREFERENCES_KEY_PREFIX}.Track`;
45
+ const PREFERENCES_VOLUME_KEY = `${PREFERENCES_KEY_PREFIX}.Volume`;
46
+ const SEEK_BAR_UPDATE_PERIOD_MS = 0;
47
+ const SOURCE_TYPES = {
48
+ audio: 'audio',
49
+ video: 'video'
50
+ };
51
+ const TIMEOUT_FOR_DOUBLE_CLICK_MS = 500;
52
+ const TRACK_KINDS = {
53
+ captions: 'captions',
54
+ subtitles: 'subtitles'
55
+ };
56
+ const Url = URL || window.URL;
57
+ const FUSE_OPTIONS = options => ({
58
+ keys: ['text'],
59
+ ignoreLocation: true,
60
+ threshold: 0.1,
61
+ ...options
62
+ });
63
+ const SEARCH_CONTAINER_HOVER_CLASS = 'd2l-labs-media-player-search-container-hover';
64
+ const DEFAULT_PREVIEW_WIDTH = 160;
65
+ const DEFAULT_PREVIEW_HEIGHT = 90;
66
+
67
+ const SAFARI_EXPIRY_EARLY_SWAP_SECONDS = 10;
68
+ const SAFARI_EXPIRY_MIN_ERROR_EMIT_SECONDS = 30;
69
+ const isSafari = () => navigator.userAgent.indexOf('Safari') > -1 && navigator.userAgent.indexOf('Chrome') === -1;
70
+ const tryParseUrlExpiry = url => {
71
+ try {
72
+ const urlObj = new URL(url);
73
+ return urlObj.searchParams ? urlObj.searchParams.get('Expires') : null;
74
+ } catch {
75
+ return null;
76
+ }
77
+ };
78
+
79
+ class MediaPlayer extends LocalizeLabsElement(RtlMixin(LitElement)) {
80
+
81
+ static get properties() {
82
+ return {
83
+ allowDownload: { type: Boolean, attribute: 'allow-download', reflect: true },
84
+ autoplay: { type: Boolean },
85
+ crossorigin: { type: String },
86
+ downloadFilename: { type: String, attribute: 'download-filename' },
87
+ durationHint: { type: Number, attribute: 'duration-hint' },
88
+ hideCaptionsSelection: { type: Boolean, attribute: 'hide-captions-selection' },
89
+ hideSeekBar: { type: Boolean, attribute: 'hide-seek-bar' },
90
+ loop: { type: Boolean },
91
+ mediaType: { type: String, attribute: 'media-type' },
92
+ metadata: { type: Object },
93
+ poster: { type: String },
94
+ src: { type: String },
95
+ thumbnails: { type: String },
96
+ disableSetPreferences: { type: Boolean, attribute: 'disable-set-preferences' },
97
+ transcriptViewerOn: { type: Boolean, attribute: 'transcript-viewer-on' },
98
+ playInView: { type: Boolean, attribute: 'play-in-view' },
99
+ _chapters: { type: Array, attribute: false },
100
+ _currentTime: { type: Number, attribute: false },
101
+ _duration: { type: Number, attribute: false },
102
+ _heightPixels: { type: Number, attribute: false },
103
+ _hoverTime: { type: Number, attribute: false },
104
+ _hovering: { type: Boolean, attribute: false },
105
+ _loading: { type: Boolean, attribute: false },
106
+ _maintainHeight: { type: Number, attribute: false },
107
+ _mediaContainerAspectRatio: { type: Object, attribute: false },
108
+ _message: { type: Object, attribute: false },
109
+ _muted: { type: Boolean, attribute: false },
110
+ _playing: { type: Boolean, attribute: false },
111
+ _posterVisible: { type: Boolean, attribute: false },
112
+ _recentlyShowedCustomControls: { type: Boolean, attribute: false },
113
+ _searchResults: { type: Array, attribute: false },
114
+ _selectedQuality: { type: String, attribute: false },
115
+ _selectedSpeed: { type: String, attribute: false },
116
+ _selectedTrackIdentifier: { type: Object, attribute: false },
117
+ _sources: { type: Object, attribute: false },
118
+ _thumbnailsImage: { type: Object, attribute: false },
119
+ _timelinePreviewOffset: { type: Number, attribute: false },
120
+ _trackFontSizeRem: { type: Number, attribute: false },
121
+ _timeFontSizeRem: { type: Number, attribute: false },
122
+ _trackText: { type: String, attribute: false },
123
+ _tracks: { type: Array, attribute: false },
124
+ _usingVolumeContainer: { type: Boolean, attribute: false },
125
+ _volume: { type: Number, attribute: false },
126
+ };
127
+ }
128
+
129
+ static get styles() {
130
+ return [ labelStyles, css`
131
+ :host {
132
+ display: block;
133
+ min-height: 140px;
134
+ position: relative;
135
+ }
136
+
137
+ :host([hidden]) {
138
+ display: none;
139
+ }
140
+
141
+ #d2l-labs-media-player-media-container {
142
+ align-items: center;
143
+ justify-content: center;
144
+ /* This max-height prevents the video from growing out of bounds and appearing cut off inside of ISF iframes */
145
+ max-height: 100vh;
146
+ overflow: hidden;
147
+ position: relative;
148
+ width: 100%;
149
+ }
150
+
151
+ .d2l-labs-media-player-type-is-audio {
152
+ background-color: #ffffff;
153
+ }
154
+
155
+ .d2l-labs-media-player-type-is-video {
156
+ background-color: #000000;
157
+ color: #ffffff;
158
+ }
159
+
160
+
161
+ #d2l-labs-media-player-video {
162
+ display: block;
163
+ height: 100%;
164
+ max-height: var(--d2l-labs-media-player-video-max-height, 100vh);
165
+ min-height: 100%;
166
+ position: relative;
167
+ width: 100%;
168
+ }
169
+
170
+ #d2l-labs-media-player-video-poster {
171
+ cursor: pointer;
172
+ height: auto;
173
+ position: absolute;
174
+ width: 100%;
175
+ z-index: 1;
176
+ }
177
+
178
+ #d2l-labs-media-player-video-poster-play-button {
179
+ background-color: rgba(0, 0, 0, 0.69);
180
+ border: none;
181
+ border-radius: 50%;
182
+ cursor: pointer;
183
+ padding: 2em;
184
+ position: absolute;
185
+ z-index: 2;
186
+ }
187
+
188
+ #d2l-labs-media-player-video-poster-play-button[transcript] {
189
+ background-color: rgba(0, 0, 0, 0.69);
190
+ border: none;
191
+ border-radius: 50%;
192
+ cursor: pointer;
193
+ left: 10%;
194
+ padding: 2em;
195
+ position: absolute;
196
+ top: 10%;
197
+ transform: scale(0.5);
198
+ z-index: 2;
199
+ }
200
+
201
+
202
+ #d2l-labs-media-player-video-poster-play-button > d2l-icon {
203
+ color: #ffffff;
204
+ }
205
+
206
+ #d2l-labs-media-player-media-controls {
207
+ bottom: 0;
208
+ position: absolute;
209
+ transition: bottom 500ms ease;
210
+ width: 100%;
211
+ z-index: 2;
212
+ }
213
+
214
+ .d2l-labs-media-player-type-is-audio #d2l-labs-media-player-media-controls {
215
+ background-color: #ffffff;
216
+ }
217
+ .d2l-labs-media-player-type-is-video #d2l-labs-media-player-media-controls {
218
+ background-color: rgba(0, 0, 0, 0.69);
219
+ }
220
+
221
+ #d2l-labs-media-player-media-controls.d2l-labs-media-player-hidden {
222
+ bottom: -8rem;
223
+ }
224
+
225
+ #d2l-labs-media-player-seek-bar {
226
+ --d2l-knob-focus-color: #ffffff;
227
+ --d2l-knob-focus-size: 4px;
228
+ --d2l-knob-size: 16px;
229
+ --d2l-outer-knob-color: var(--d2l-color-celestine-plus-1);
230
+ --d2l-progress-border-radius: 0;
231
+ position: absolute;
232
+ top: -9px;
233
+ width: 100%;
234
+ z-index: 1;
235
+ }
236
+
237
+ #d2l-labs-media-player-seek-bar:focus {
238
+ --d2l-knob-box-shadow: 0 2px 6px 3px rgba(0, 0, 0, 1);
239
+ }
240
+
241
+ #d2l-labs-media-player-buttons {
242
+ align-items: center;
243
+ direction: ltr;
244
+ display: flex;
245
+ flex-direction: row;
246
+ justify-content: space-between;
247
+ margin-left: 6px;
248
+ }
249
+
250
+ [dir="rtl"] #d2l-labs-media-player-buttons {
251
+ margin-left: 0;
252
+ margin-right: 6px;
253
+ }
254
+
255
+ .d2l-labs-media-player-flex-filler {
256
+ flex: auto;
257
+ }
258
+
259
+ d2l-button-icon {
260
+ --d2l-button-icon-min-height: 1.8rem;
261
+ --d2l-button-icon-min-width: 1.8rem;
262
+ margin: 6px 6px 6px 0;
263
+ }
264
+
265
+ #d2l-labs-media-player-time:hover {
266
+ cursor: auto;
267
+ }
268
+
269
+ #d2l-labs-media-player-volume-container {
270
+ position: relative;
271
+ }
272
+
273
+ #d2l-labs-media-player-volume-level-container {
274
+ bottom: calc(1.8rem + 6px);
275
+ height: 11px;
276
+ left: 0;
277
+ position: absolute;
278
+ width: 1.8rem;
279
+ z-index: 2;
280
+ }
281
+
282
+ #d2l-labs-media-player-volume-level-container.d2l-labs-media-player-hidden {
283
+ left: -10000px;
284
+ }
285
+
286
+ #d2l-labs-media-player-volume-level-background {
287
+ align-items: center;
288
+ background-color: rgba(0, 0, 0, 0.69);
289
+ border-radius: 0 0.3rem 0.3rem 0;
290
+ bottom: 4.55rem;
291
+ display: flex;
292
+ height: 1.8rem;
293
+ justify-content: center;
294
+ left: -2.7rem;
295
+ padding: 0 0.625rem;
296
+ position: relative;
297
+ width: 6rem;
298
+ }
299
+
300
+ #d2l-labs-media-player-volume-slider-container {
301
+ height: 100%;
302
+ width: 100%;
303
+ }
304
+
305
+ #d2l-labs-media-player-volume-slider {
306
+ --d2l-knob-focus-color: #ffffff;
307
+ --d2l-knob-focus-size: 0.25rem;
308
+ --d2l-knob-size: 0.8rem;
309
+ --d2l-outer-knob-color: var(--d2l-color-celestine-plus-1);
310
+ position: relative;
311
+ top: calc(0.5rem + 1px);
312
+ }
313
+
314
+ .d2l-labs-media-player-rotated {
315
+ transform: rotate(-90deg);
316
+ }
317
+
318
+ #d2l-labs-media-player-audio-bars-container {
319
+ align-items: center;
320
+ display: flex;
321
+ flex-wrap: nowrap;
322
+ height: 8.5rem;
323
+ justify-content: center;
324
+ left: calc(2.1rem + 6px);
325
+ position: absolute;
326
+ top: calc(50% - 5.125rem);
327
+ width: calc(100% - 4.2rem - 12px);
328
+ }
329
+
330
+ d2l-labs-media-player-audio-bars {
331
+ height: 2rem;
332
+ }
333
+
334
+ #d2l-labs-media-player-track-container {
335
+ align-items: center;
336
+ color: #ffffff;
337
+ display: flex;
338
+ justify-content: center;
339
+ overflow: hidden;
340
+ position: absolute;
341
+ text-align: center;
342
+ transition: bottom 500ms ease;
343
+ width: 100%;
344
+ }
345
+
346
+ @media screen and (min-width: 768px) {
347
+ #d2l-labs-media-player-track-container > div {
348
+ align-items: center;
349
+ display: flex;
350
+ justify-content: center;
351
+ min-width: ${MIN_TRACK_WIDTH_PX}px;
352
+ width: 50%;
353
+ }
354
+ }
355
+
356
+ @media screen and (max-width: 767px) {
357
+ #d2l-labs-media-player-track-container > div {
358
+ align-items: center;
359
+ display: flex;
360
+ justify-content: center;
361
+ width: 100%;
362
+ }
363
+ }
364
+
365
+ #d2l-labs-media-player-track-container > div > span {
366
+ background-color: rgba(0, 0, 0, 0.69);
367
+ box-shadow: 0.3rem 0 0 rgba(0, 0, 0, 0.69), -0.3rem 0 0 rgba(0, 0, 0, 0.69);
368
+ color: white;
369
+ line-height: 1.35rem;
370
+ white-space: pre-wrap;
371
+ }
372
+
373
+ #d2l-labs-media-player-audio-play-button-container {
374
+ background-color: white;
375
+ position: absolute;
376
+ }
377
+
378
+ #d2l-labs-media-player-audio-play-button {
379
+ background-color: transparent;
380
+ border: none;
381
+ border-radius: 12px;
382
+ margin: 2px;
383
+ padding: 2px;
384
+ }
385
+
386
+ #d2l-labs-media-player-audio-play-button:focus {
387
+ outline: none;
388
+ }
389
+
390
+ #d2l-labs-media-player-audio-play-button:hover {
391
+ background: var(--d2l-color-mica);
392
+ background-clip: content-box;
393
+ cursor: pointer;
394
+ }
395
+
396
+ #d2l-labs-media-player-audio-play-button:${unsafeCSS(getFocusPseudoClass())} {
397
+ border: 2px solid var(--d2l-color-celestine);
398
+ }
399
+
400
+ #d2l-labs-media-player-audio-play-button > d2l-icon {
401
+ height: 2.75rem;
402
+ width: 2.75rem;
403
+ }
404
+
405
+ .d2l-labs-media-player-chapter-marker, .d2l-labs-media-player-chapter-marker-highlight {
406
+ cursor: pointer;
407
+ height: 6px;
408
+ pointer-events: none;
409
+ position: absolute;
410
+ top: 4px;
411
+ width: 3px;
412
+ z-index: 2;
413
+ }
414
+
415
+ .d2l-labs-media-player-chapter-marker {
416
+ background-color: var(--d2l-color-ferrite);
417
+ }
418
+
419
+ .d2l-labs-media-player-chapter-marker-highlight {
420
+ background-color: var(--d2l-color-celestine-minus-1);
421
+ }
422
+
423
+ .d2l-labs-media-player-chapter-marker[theme="dark"] {
424
+ background-color: white;
425
+ }
426
+
427
+ #d2l-labs-media-player-search-container {
428
+ align-items: center;
429
+ display: flex;
430
+ }
431
+ #d2l-labs-media-player-search-container.d2l-labs-media-player-search-container-hidden {
432
+ display: none;
433
+ }
434
+
435
+ #d2l-labs-media-player-search-container #d2l-labs-media-player-search-input {
436
+ border-color: rgba(48, 52, 54, 0.1);
437
+ border-radius: 20px;
438
+ color: var(--d2l-color-ferrite);
439
+ font-size: 1rem;
440
+ opacity: 0;
441
+ outline: 0;
442
+ padding: 0;
443
+ position: relative;
444
+ transition: width 0.4s, opacity 0.4s, visibility 0s;
445
+ visibility: hidden;
446
+ width: 0;
447
+ }
448
+
449
+ #d2l-labs-media-player-search-container #d2l-labs-media-player-search-input[theme="dark"] {
450
+ background-color: rgba(24, 26, 27, 0.15);
451
+ color: rgb(206, 216, 225);
452
+ }
453
+
454
+ #d2l-labs-media-player-search-container #d2l-labs-media-player-search-input:focus,
455
+ #d2l-labs-media-player-search-container #d2l-labs-media-player-search-input:active {
456
+ box-shadow: rgb(24 26 27) 0 0 1px;
457
+ outline-color: initial;
458
+ }
459
+
460
+ #d2l-labs-media-player-search-container:hover #d2l-labs-media-player-search-input,
461
+ #d2l-labs-media-player-search-container.d2l-labs-media-player-search-container-hover #d2l-labs-media-player-search-input {
462
+ height: 1.2rem;
463
+ opacity: 1;
464
+ padding: 0 0.35rem;
465
+ visibility: visible;
466
+ width: 6rem;
467
+ }
468
+
469
+ #d2l-labs-media-player-timeline-markers-container {
470
+ position: absolute;
471
+ top: -9px;
472
+ transition: all 0.2s;
473
+ width: 100%;
474
+ }
475
+
476
+ #d2l-labs-media-player-thumbnails-preview-container {
477
+ bottom: 60px;
478
+ position: absolute;
479
+ transform: translateX(-50%);
480
+ z-index: 2;
481
+ }
482
+
483
+ #d2l-labs-media-player-thumbnails-preview-chapter {
484
+ background: #00000072;
485
+ position: absolute;
486
+ text-align: center;
487
+ text-shadow: 0 0 5px rgb(0 0 0 / 75%);
488
+ width: 100%;
489
+ z-index: 2;
490
+ }
491
+
492
+ #d2l-labs-media-player-thumbnails-preview-time {
493
+ background: #00000042;
494
+ bottom: 3px;
495
+ font-size: 14px;
496
+ left: 0;
497
+ position: absolute;
498
+ text-align: center;
499
+ text-shadow: 0 0 4px rgb(0 0 0 / 75%);
500
+ width: 100%;
501
+ z-index: 2;
502
+ }
503
+
504
+ #d2l-labs-media-player-thumbnails-preview-image {
505
+ background-repeat: no-repeat;
506
+ position: relative;
507
+ width: 100%;
508
+ z-index: 2;
509
+ }
510
+
511
+ .d2l-labs-media-player-search-marker {
512
+ color: var(--d2l-color-ferrite);
513
+ cursor: pointer;
514
+ height: 6px;
515
+ pointer-events: none;
516
+ position: absolute;
517
+ top: 4px;
518
+ width: 6px;
519
+ z-index: 2;
520
+ }
521
+
522
+ .d2l-labs-media-player-search-marker[theme="dark"] {
523
+ color: white;
524
+ }
525
+
526
+ #d2l-labs-media-player-settings-menu {
527
+ bottom: calc(1.8rem + 18px);
528
+ left: calc(0.2rem + 14px);
529
+ }
530
+
531
+ [dir="rtl"] #d2l-labs-media-player-settings-menu {
532
+ left: 0;
533
+ right: -0.8rem;
534
+ }
535
+
536
+ .d2l-labs-media-player-full-area-centered {
537
+ align-items: center;
538
+ display: flex;
539
+ height: 100%;
540
+ justify-content: center;
541
+ left: 0;
542
+ position: absolute;
543
+ top: 0;
544
+ width: 100%;
545
+ z-index: 2;
546
+ }
547
+
548
+ #d2l-labs-media-player-alert-inner {
549
+ display: flex;
550
+ flex-direction: row;
551
+ justify-content: flex-start;
552
+ }
553
+
554
+ #d2l-labs-media-player-alert-inner > svg {
555
+ flex-shrink: 0;
556
+ margin-right: 0.5rem;
557
+ }
558
+
559
+ #d2l-labs-media-player-alert-inner > span {
560
+ font-size: 1rem;
561
+ line-height: 2.1rem;
562
+ }
563
+
564
+ .transcript-cue-container {
565
+ padding-left: 10px;
566
+ }
567
+ .video-transcript-cue {
568
+ padding-left: 5px;
569
+ }
570
+ .audio-transcript-cue {
571
+ padding-left: 5px;
572
+ }
573
+ .video-transcript-cue[active] {
574
+ background-color: gray;
575
+ box-shadow: -5px 0 0 white;
576
+ }
577
+ .audio-transcript-cue[active] {
578
+ background-color: lightgray;
579
+ box-shadow: -5px 0 0 black;
580
+ }
581
+ #video-transcript-viewer {
582
+ bottom: 55px;
583
+ color: white;
584
+ overflow-anchor: none;
585
+ overflow-y: auto;
586
+ position: absolute;
587
+ right: 0;
588
+ top: 50px;
589
+ width: 65%;
590
+ z-index: 1;
591
+ }
592
+ #audio-transcript-viewer {
593
+ bottom: 60px;
594
+ color: black;
595
+ overflow-anchor: none;
596
+ overflow-y: auto;
597
+ position: absolute;
598
+ right: 0;
599
+ top: 45px;
600
+ width: 100%;
601
+ z-index: 1;
602
+ }
603
+ #close-transcript {
604
+ position: absolute;
605
+ right: 7px;
606
+ top: 0;
607
+ z-index: 1;
608
+ }
609
+ #video-transcript-download-button {
610
+ left: 35%;
611
+ position: absolute;
612
+ top: 5px;
613
+ z-index: 2;
614
+ }
615
+ #audio-transcript-download-button {
616
+ left: 10px;
617
+ position: absolute;
618
+ top: 0;
619
+ z-index: 2;
620
+ }
621
+ #audio-transcript-download-menu {
622
+ left: 35px;
623
+ }
624
+ #video-close-transcript-icon {
625
+ color: white;
626
+ }
627
+ #audio-close-transcript-icon {
628
+ color: black;
629
+ }
630
+ ` ];
631
+ }
632
+
633
+ constructor() {
634
+ super();
635
+
636
+ this.allowDownload = false;
637
+ this.autoplay = false;
638
+ this.loop = false;
639
+ this.playInView = false;
640
+
641
+ this._chapters = [];
642
+ this._currentTime = 0;
643
+ this._determiningSourceType = true;
644
+ this._duration = this.durationHint || 1;
645
+ this._heightPixels = null;
646
+ this._hoverTime = 0;
647
+ this._hovering = false;
648
+ this._hoveringMediaControls = false;
649
+ this._loading = false;
650
+ this._message = {
651
+ text: null,
652
+ type: null
653
+ };
654
+ this._muted = false;
655
+ this._playing = false;
656
+ this._posterVisible = true;
657
+ this._recentlyShowedCustomControls = false;
658
+ this._searchInputFocused = false;
659
+ this._searchInstances = {};
660
+ this._searchResults = [];
661
+ this._settingsMenu = null;
662
+ this._sources = {};
663
+ this._timelinePreviewOffset = 0;
664
+ this._trackFontSizeRem = 1;
665
+ this._timeFontSizeRem = 0.95; // 0.95rem is the default font size for d2l-typography
666
+ this._trackText = null;
667
+ this._tracks = [];
668
+ this._usingVolumeContainer = false;
669
+ this._videoClicked = false;
670
+ this._volume = 1;
671
+ this._webVTTParser = new window.WebVTTParser();
672
+ this._playRequested = false;
673
+ this._mediaContainerAspectRatio = {
674
+ };
675
+ this.afterCaptions = [];
676
+ this.beforeCaptions = [];
677
+ }
678
+
679
+ get currentTime() {
680
+ return this._currentTime;
681
+ }
682
+
683
+ set currentTime(time) {
684
+ this._currentTime = time;
685
+ if (this._media) {
686
+ this._media.currentTime = time;
687
+ }
688
+ this._syncDisplayedTrackTextToSelectedTrack();
689
+ }
690
+
691
+ get volume() {
692
+ return this._media ? this._media.volume : 0;
693
+ }
694
+
695
+ set volume(volume) {
696
+ if (this._media) {
697
+ this._media.volume = volume;
698
+ }
699
+ this._setPreference(PREFERENCES_VOLUME_KEY, volume);
700
+ }
701
+
702
+ get activeCue() {
703
+ if (!this._media) return null;
704
+ for (let i = 0; i < this._media.textTracks.length; i++) {
705
+ if (this._media.textTracks[i].mode === 'hidden') {
706
+ if (this._media &&
707
+ this._media.textTracks &&
708
+ this._media.textTracks[0] &&
709
+ this._media.textTracks[0].activeCues &&
710
+ this._media.textTracks[0].activeCues[0]) {
711
+ return this._media.textTracks[0].activeCues[0];
712
+ } else {
713
+ return null;
714
+ }
715
+ }
716
+ }
717
+ return null;
718
+ }
719
+
720
+ get duration() {
721
+ return this._duration;
722
+ }
723
+
724
+ get ended() {
725
+ return (this._media && this._media.ended);
726
+ }
727
+
728
+ get isIOSVideo() {
729
+ return IS_IOS && this.mediaType === SOURCE_TYPES.video;
730
+ }
731
+
732
+ get paused() {
733
+ return (this._media && this._media.paused);
734
+ }
735
+
736
+ get selectedTrackSrcLang() {
737
+ return this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
738
+ }
739
+
740
+ get textTracks() {
741
+ return this._media ? this._media.textTracks : null;
742
+ }
743
+
744
+ firstUpdated(changedProperties) {
745
+ super.firstUpdated(changedProperties);
746
+
747
+ if (!this.src) {
748
+ const sourceNodes = Array.from(this.getElementsByTagName('source'));
749
+ if (sourceNodes.length < 1) console.warn('d2l-labs-media-player component requires source tags if src is not set');
750
+ }
751
+
752
+ this._mediaContainer = this.shadowRoot.getElementById('d2l-labs-media-player-media-container');
753
+ this._playButton = this.shadowRoot.getElementById('d2l-labs-media-player-play-button');
754
+ this._seekBar = this.shadowRoot.getElementById('d2l-labs-media-player-seek-bar');
755
+ this._settingsMenu = this.shadowRoot.getElementById('d2l-labs-media-player-settings-menu');
756
+ this._speedLevelBackground = this.shadowRoot.getElementById('d2l-labs-media-player-speed-level-background');
757
+ this._volumeSlider = this.shadowRoot.getElementById('d2l-labs-media-player-volume-slider');
758
+ this._searchInput = this.shadowRoot.getElementById('d2l-labs-media-player-search-input');
759
+ this._searchContainer = this.shadowRoot.getElementById('d2l-labs-media-player-search-container');
760
+
761
+ this._getMetadata();
762
+
763
+ this._startUpdatingCurrentTime();
764
+ new ResizeObserver((entries) => {
765
+ for (const entry of entries) {
766
+ const { height, width } = entry.contentRect;
767
+ // Handles potential flickering of video dimensions - given two heights (A, B), if we see that
768
+ // the heights alternate A -> B -> A (height === two heights ago), we set the height to the larger of A/B
769
+ // Furthermore, check that the height difference was within the threshold of a flicker (i.e., not a full screen toggle)
770
+ const flickerThreshold = 20;
771
+ if ((height === this._twoHeightsAgo && width === this._twoWidthsAgo)
772
+ && Math.abs(this._lastHeight - height) < flickerThreshold
773
+ ) {
774
+ this._heightPixels = Math.floor(Math.max(height, this._lastHeight));
775
+ } else {
776
+ this._heightPixels = null;
777
+ }
778
+
779
+ this._twoHeightsAgo = this._lastHeight;
780
+ this._lastHeight = height;
781
+
782
+ this._twoWidthsAgo = this._lastWidth;
783
+ this._lastWidth = width;
784
+
785
+ const multiplier = Math.sqrt(Math.min(height, width) / MIN_TRACK_WIDTH_PX);
786
+ this._trackFontSizeRem = multiplier;
787
+ this._timeFontSizeRem = Math.min(multiplier * 0.9 * 0.95, 0.95);
788
+ }
789
+ }).observe(this._mediaContainer);
790
+ }
791
+
792
+ render() {
793
+ const fullscreenIcon = fullscreenApi.isFullscreen ? 'tier1:smallscreen' : 'tier1:fullscreen';
794
+ const playIcon = this._playing ? 'tier1:pause' : 'tier1:play';
795
+ const volumeIcon = this._muted ? 'tier1:volume-muted' : 'tier1:volume';
796
+
797
+ const fullscreenTooltip = `${fullscreenApi.isFullscreen ? this.localize('components:mediaPlayer:exitFullscreen') : this.localize('components:mediaPlayer:fullscreen')} (${KEY_BINDINGS.fullscreen})`;
798
+ const playTooltip = `${this._playing ? this.localize('components:mediaPlayer:pause') : this.localize('components:mediaPlayer:play')} (${KEY_BINDINGS.play})`;
799
+ const volumeTooltip = `${this._muted ? this.localize('components:mediaPlayer:unmute') : this.localize('components:mediaPlayer:mute')} (${KEY_BINDINGS.mute})`;
800
+
801
+ const height = this._maintainHeight ? `${this._maintainHeight}px` : (this._heightPixels ? `${this._heightPixels}px` : '100%');
802
+ const mediaContainerStyle = {
803
+ cursor: !this._hidingCustomControls() ? 'auto' : 'none',
804
+ display: 'flex',
805
+ minHeight: this.mediaType === SOURCE_TYPES.audio ? 'min(17rem, 90vh)' : 'auto',
806
+ height,
807
+ ...this._mediaContainerAspectRatio,
808
+ };
809
+ const timeStyle = {
810
+ fontSize: `${this._timeFontSizeRem}rem`,
811
+ margin: `0 ${this._timeFontSizeRem * 0.79}rem`, // At max size, this is 15px.
812
+ lineHeight: `${this._timeFontSizeRem * 1.05263158}rem`, // At max size, this is 1rem.
813
+ };
814
+
815
+ const trackContainerStyle = { bottom: this._hidingCustomControls() ? '12px' : 'calc(1.8rem + 38px)' };
816
+ const trackSpanStyle = { fontSize: `${this._trackFontSizeRem}rem`, lineHeight: `${this._trackFontSizeRem * 1.2}rem` };
817
+
818
+ const mediaContainerClass = { 'd2l-labs-media-player-type-is-audio': this.mediaType === SOURCE_TYPES.audio, 'd2l-labs-media-player-type-is-video': this.mediaType === SOURCE_TYPES.video };
819
+ const mediaControlsClass = { 'd2l-labs-media-player-hidden': this._hidingCustomControls() };
820
+ const theme = this.mediaType === SOURCE_TYPES.video ? 'dark' : undefined;
821
+ const volumeLevelContainerClass = { 'd2l-labs-media-player-hidden': !this._usingVolumeContainer || this._hidingCustomControls() };
822
+ const searchContainerClass = { 'd2l-labs-media-player-search-container-hidden' : !this._searchInstances[this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier)] };
823
+ this._captionsMenuReturnItem?.setAttribute('text', (this.transcriptViewerOn ? this.localize('components:mediaPlayer:language') : this.localize('components:mediaPlayer:captions')));
824
+
825
+ const fullscreenButton = this.mediaType === SOURCE_TYPES.video ? html`<d2l-button-icon
826
+ class="d2l-dropdown-opener"
827
+ icon="${fullscreenIcon}"
828
+ text="${fullscreenTooltip}"
829
+ theme="${ifDefined(theme)}"
830
+ @click="${this._toggleFullscreen}"></d2l-button-icon>` : null;
831
+
832
+ return html`
833
+ <slot @slotchange=${this._onSlotChange}></slot>
834
+
835
+ ${this._getLoadingSpinnerView()}
836
+
837
+ <div id="d2l-labs-media-player-media-container" class=${classMap(mediaContainerClass)} style=${styleMap(mediaContainerStyle)} @mousemove=${this._onVideoContainerMouseMove} @keydown=${this._listenForKeyboard}>
838
+ ${this.transcriptViewerOn ? this._renderTranscriptViewer() : ''}
839
+ ${this._getMediaAreaView()}
840
+
841
+ ${this.isIOSVideo ? null : html`
842
+ ${!this._trackText || this.transcriptViewerOn ? null : html`
843
+ <div id="d2l-labs-media-player-track-container" style=${styleMap(trackContainerStyle)} @click=${this._onTrackContainerClick}>
844
+ <div>
845
+ <span style=${styleMap(trackSpanStyle)} role="status">${this._trackText}</span>
846
+ </div>
847
+ </div>
848
+ `}
849
+
850
+ <div class=${classMap(mediaControlsClass)} id="d2l-labs-media-player-media-controls" @mouseenter=${this._startHoveringControls} @mouseleave=${this._stopHoveringControls}>
851
+ ${this._getTimelinePreview()}
852
+ <div id="d2l-labs-media-player-timeline-markers-container">
853
+ ${this._getSearchResultsView()}
854
+ ${this._getChapterMarkersView()}
855
+ </div>
856
+ ${this.hideSeekBar ? '' : html`
857
+ <d2l-slider-bar
858
+ id="d2l-labs-media-player-seek-bar"
859
+ fullWidth
860
+ solid
861
+ min="0"
862
+ max="${Math.floor(this._getSeekbarValue(this.duration))}"
863
+ value="${this._getSeekbarValue(this._currentTime)}"
864
+ aria-label="${this.localize('components:mediaPlayer:seekSlider')}"
865
+ aria-orientation="horizontal"
866
+ aria-valuemin="0"
867
+ aria-valuemax="${Math.floor(this._getSeekbarValue(this.duration))}"
868
+ aria-valuenow="${this._getSeekbarValue(this._currentTime)}"
869
+ title="${this.localize('components:mediaPlayer:seekSlider')}"
870
+ @drag-start=${this._onDragStartSeek}
871
+ @drag-end=${this._onDragEndSeek}
872
+ @position-change=${this._onPositionChangeSeek}
873
+ @hovering-move=${this._onHoverMove}
874
+ @hovering-start=${this._onHoverStart}
875
+ @hovering-end=${this._onHoverEnd}
876
+ ></d2l-slider-bar>
877
+ `}
878
+ <div id="d2l-labs-media-player-buttons">
879
+ <d2l-button-icon icon="${playIcon}" text="${playTooltip}" @click="${this._togglePlay}" theme="${ifDefined(theme)}"></d2l-button-icon>
880
+
881
+ <div id="d2l-labs-media-player-volume-container" @mouseenter="${this._startUsingVolumeContainer}" @mouseleave="${this._stopUsingVolumeContainer}" ?hidden="${IS_IOS}">
882
+ <d2l-button-icon
883
+ class="d2l-dropdown-opener"
884
+ icon="${volumeIcon}"
885
+ text="${volumeTooltip}"
886
+ theme="${ifDefined(theme)}"
887
+ @blur="${this._stopUsingVolumeContainer}"
888
+ @click="${this._toggleMute}"
889
+ @focus="${this._startUsingVolumeContainer}"
890
+ ></d2l-button-icon>
891
+ <div id="d2l-labs-media-player-volume-level-container" class=${classMap(volumeLevelContainerClass)}>
892
+ <div class="d2l-labs-media-player-rotated" id="d2l-labs-media-player-volume-level-background">
893
+ <div id="d2l-labs-media-player-volume-slider-container">
894
+ <d2l-slider-bar solid
895
+ id="d2l-labs-media-player-volume-slider"
896
+ vertical
897
+ value="${Math.round(this._volume * 100)}"
898
+ aria-label="${this.localize('components:mediaPlayer:volumeSlider')}"
899
+ aria-orientation="vertical" aria-valuemin="0"
900
+ aria-valuemax="100"
901
+ aria-valuenow="${Math.floor(this._volume * 100)}"
902
+ title="${this.localize('components:mediaPlayer:volumeSlider')}"
903
+ @drag-start=${this._onDragStartVolume}
904
+ @focus=${this._startUsingVolumeContainer}
905
+ @focusout=${this._stopUsingVolumeContainer}
906
+ @position-change=${this._onPositionChangeVolume}
907
+ ></d2l-slider-bar>
908
+ </div>
909
+ </div>
910
+ </div>
911
+ </div>
912
+
913
+ <div id="d2l-labs-media-player-time"
914
+ aria-live="off"
915
+ aria-hidden="true"
916
+ tabindex="-1"
917
+ style=${styleMap(timeStyle)}
918
+ @focus=${this._onPlayerTimeFocus}
919
+ @blur=${this._onPlayerTimeBlur}>
920
+ ${MediaPlayer._formatTime(this.currentTime)} / ${MediaPlayer._formatTime(this.duration)}
921
+ </div>
922
+
923
+ <div class="d2l-labs-media-player-flex-filler"></div>
924
+
925
+ <div
926
+ @mouseenter=${this._onSearchContainerHover}
927
+ @mouseleave=${this._onSearchContainerHover}
928
+ class=${classMap(searchContainerClass)}
929
+ id="d2l-labs-media-player-search-container"
930
+ ><d2l-button-icon
931
+ icon="tier1:search"
932
+ id="d2l-labs-media-player-search-button"
933
+ text=${this.localize('components:mediaPlayer:showSearchInput')}
934
+ theme="${ifDefined(theme)}"
935
+ ></d2l-button-icon>
936
+ <input
937
+ @blur=${this._onSearchInputBlur}
938
+ @focus=${this._onSearchInputFocus}
939
+ @input=${this._onSearchInputChanged}
940
+ id="d2l-labs-media-player-search-input"
941
+ placeholder="${this.localize('components:mediaPlayer:searchPlaceholder')}"
942
+ theme="${ifDefined(theme)}"
943
+ type="text"
944
+ ></input>
945
+ </div>
946
+ <d2l-dropdown>
947
+ <d2l-button-icon class="d2l-dropdown-opener" icon="tier1:gear" text="${this.localize('components:mediaPlayer:settings')}" theme="${ifDefined(theme)}"></d2l-button-icon>
948
+ <d2l-dropdown-menu id="d2l-labs-media-player-settings-menu" no-pointer theme="${ifDefined(theme)}">
949
+ <d2l-menu label="${this.localize('components:mediaPlayer:settings')}" theme="${ifDefined(theme)}">
950
+ <d2l-menu-item id="d2l-labs-media-player-playback-speeds" text="${this.localize('components:mediaPlayer:playbackSpeed')}">
951
+ <div slot="supporting">${this._selectedSpeed}</div>
952
+ <d2l-menu @d2l-menu-item-change=${this._onPlaybackSpeedsMenuItemChange} theme="${ifDefined(theme)}">
953
+ ${PLAYBACK_SPEEDS.map(speed => html`
954
+ <d2l-menu-item-radio
955
+ ?selected="${speed === this._selectedSpeed}"
956
+ text="${speed === DEFAULT_SPEED ? `${DEFAULT_SPEED} (${this.localize('components:mediaPlayer:default')})` : speed}"
957
+ value="${speed}"
958
+ ></d2l-menu-item-radio>
959
+ `)}
960
+ </d2l-menu>
961
+ </d2l-menu-item>
962
+ ${this._getTracksMenuView()}
963
+ ${this._getQualityMenuView()}
964
+ ${(this.allowDownload && this._getCurrentSource()) ? this._getDownloadButtonView() : ''}
965
+ <slot name="settings-menu-item"></slot>
966
+ </d2l-menu>
967
+ </d2l-dropdown-menu>
968
+ </d2l-dropdown>
969
+
970
+ ${fullscreenButton}
971
+
972
+ </div>
973
+ </div>`}
974
+ </div>
975
+ `;
976
+ }
977
+
978
+ updated(changedProperties) {
979
+ super.updated(changedProperties);
980
+
981
+ if (changedProperties.has('src') || changedProperties.has('mediaType')) {
982
+ this._reloadSource();
983
+ }
984
+
985
+ if (changedProperties.has('metadata')) {
986
+ this._getMetadata();
987
+ }
988
+
989
+ if (changedProperties.has('thumbnails')) {
990
+ this._getThumbnails();
991
+ }
992
+ }
993
+
994
+ exitFullscreen() {
995
+ if (!fullscreenApi.isFullscreen) return;
996
+
997
+ this._toggleFullscreen();
998
+ }
999
+
1000
+ async getLoadingComplete() {
1001
+ do {
1002
+ await new Promise((resolve) => setTimeout(() => resolve(), 1000));
1003
+ } while (this._loading);
1004
+ }
1005
+
1006
+ load() {
1007
+ if (this._media && this._media.paused) {
1008
+ this._media.load();
1009
+ }
1010
+ }
1011
+
1012
+ pause() {
1013
+ if (this._media && !this._media.paused) {
1014
+ this._togglePlay();
1015
+ }
1016
+ }
1017
+
1018
+ play() {
1019
+ if (this._media && this._media.paused) {
1020
+ this._togglePlay();
1021
+ }
1022
+ }
1023
+
1024
+ requestFullscreen() {
1025
+ if (fullscreenApi.isFullscreen) {
1026
+ return;
1027
+ }
1028
+
1029
+ this._toggleFullscreen();
1030
+ }
1031
+
1032
+ get _media() {
1033
+ if (!this.shadowRoot) return null;
1034
+ switch (this.mediaType) {
1035
+ case SOURCE_TYPES.audio:
1036
+ return this.shadowRoot.getElementById('d2l-labs-media-player-audio');
1037
+ case SOURCE_TYPES.video:
1038
+ return this.shadowRoot.getElementById('d2l-labs-media-player-video');
1039
+ default:
1040
+ return null;
1041
+ }
1042
+ }
1043
+
1044
+ get _selectedTrackLabel() {
1045
+ for (let i = 0; i < this._tracks.length; i++) {
1046
+ const track = this._tracks[i];
1047
+
1048
+ if (track.srclang === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier) &&
1049
+ track.kind === this._getKindFromTrackIdentifier(this._selectedTrackIdentifier)
1050
+ ) {
1051
+ return track.label;
1052
+ }
1053
+ }
1054
+
1055
+ return this.localize('components:mediaPlayer:off');
1056
+ }
1057
+
1058
+ #searchTimeout = null;
1059
+
1060
+ _clearPreference(preferenceKey) {
1061
+ localStorage.removeItem(preferenceKey);
1062
+ }
1063
+
1064
+ _closeTranscript() {
1065
+ this.dispatchEvent(new CustomEvent('close-transcript', { bubbles: true, composed: true }));
1066
+ }
1067
+
1068
+ _disableNativeCaptions() {
1069
+ if (!this._media) return;
1070
+ for (let i = 0; i < this._media.textTracks.length; i++) {
1071
+ const textTrack = this._media.textTracks[i];
1072
+
1073
+ if (this._selectedTrackIdentifier && textTrack.language === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier) && textTrack.kind === this._getKindFromTrackIdentifier(this._selectedTrackIdentifier)) {
1074
+ textTrack.mode = 'hidden';
1075
+ } else {
1076
+ textTrack.mode = 'disabled';
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ _downloadCaptions() {
1082
+ this.dispatchEvent(new CustomEvent('download-captions', { bubbles: true, composed: true }));
1083
+ }
1084
+
1085
+ _downloadTranscript() {
1086
+ this.dispatchEvent(new CustomEvent('download-transcript', { bubbles: true, composed: true }));
1087
+ }
1088
+
1089
+ static _formatTime(totalSeconds) {
1090
+ totalSeconds = Math.floor(totalSeconds);
1091
+
1092
+ const str = [];
1093
+
1094
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
1095
+ const hours = Math.floor(totalSeconds / 3600);
1096
+
1097
+ if (hours > 0) {
1098
+ str.push(`${hours}:`);
1099
+
1100
+ if (minutes < 10) {
1101
+ str.push('0');
1102
+ }
1103
+ }
1104
+
1105
+ str.push(`${minutes}:`);
1106
+
1107
+ const seconds = totalSeconds % 60;
1108
+ if (seconds < 10) {
1109
+ str.push('0');
1110
+ }
1111
+
1112
+ str.push(seconds);
1113
+
1114
+ return str.join('');
1115
+ }
1116
+
1117
+ _getAbsoluteUrl(url) {
1118
+ const a = document.createElement('a');
1119
+ a.setAttribute('href', url);
1120
+
1121
+ return a.href;
1122
+ }
1123
+
1124
+ _getChapterMarkersView() {
1125
+ if (this._chapters.length === 0) return;
1126
+
1127
+ let start, end;
1128
+ if (this._chapters[this._chapters.length - 1].time === Math.floor(this._duration)) this._chapters.pop();
1129
+
1130
+ for (let i = 0; i < this._chapters.length; i++) {
1131
+ if (i === this._chapters.length - 1) {
1132
+ start = this._chapters[i].time;
1133
+ break;
1134
+ }
1135
+ else if (this._hoverTime >= this._chapters[i].time && this._hoverTime < this._chapters[i + 1].time) {
1136
+ start = this._chapters[i].time;
1137
+ end = this._chapters[i + 1].time;
1138
+ break;
1139
+ }
1140
+ }
1141
+
1142
+ return this._chapters.map(chapter => {
1143
+ const highlight = this._hovering && this._hoverTime >= this._chapters[0].time && (chapter.time === start || chapter.time === end);
1144
+ return chapter.time > 0 ? html`
1145
+ <div
1146
+ class=${highlight ? 'd2l-labs-media-player-chapter-marker-highlight' : 'd2l-labs-media-player-chapter-marker'}
1147
+ theme="${ifDefined(this._getTheme())}"
1148
+ style=${styleMap({ left: `${this._getPercentageTime(chapter.time)}%` })}
1149
+ ></div>
1150
+ ` : null;
1151
+ });
1152
+ }
1153
+
1154
+ _getChapterTitle() {
1155
+ if (!(this._chapters.length > 0 && this._hoverTime >= this._chapters[0].time)) return;
1156
+
1157
+ const chapter = this._chapters.find((_chapter, index, chapters) => (
1158
+ index === chapters.length - 1 || (this._hoverTime >= chapters[0].time && this._hoverTime < chapters[index + 1].time)
1159
+ ));
1160
+ const chapterTitle = chapter && chapter.title;
1161
+
1162
+ if (!chapterTitle) return;
1163
+
1164
+ if (typeof chapterTitle === 'string') {
1165
+ return chapterTitle;
1166
+ }
1167
+
1168
+ for (const locale in chapterTitle) {
1169
+ if (locale.split('-')[0] === 'en') {
1170
+ return chapterTitle[locale];
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ _getCurrentSource() {
1176
+ return this.src || this._sources[this._selectedQuality];
1177
+ }
1178
+
1179
+ _getDownloadButtonView() {
1180
+ const linkHref = this._getDownloadLink();
1181
+ return html`
1182
+ <d2l-menu-item-link href="${linkHref}" text="${this.localize('components:mediaPlayer:download')}" download=${this.downloadFilename}></d2l-menu-item-link>
1183
+ `;
1184
+ }
1185
+
1186
+ _getDownloadLink() {
1187
+ const srcUrl = this._getCurrentSource();
1188
+ if (!srcUrl) return '';
1189
+ if (srcUrl.startsWith('blob:')) {
1190
+ return srcUrl;
1191
+ }
1192
+
1193
+ // Due to Ionic rewriter bug we need to use '_' as a first query string parameter
1194
+ const attachmentUrl = `${srcUrl}${srcUrl && srcUrl.indexOf('?') === -1 ? '?_' : ''}`;
1195
+ const url = new Url(this._getAbsoluteUrl(attachmentUrl));
1196
+ url.searchParams.append('attachment', 'true');
1197
+ return url.toString();
1198
+ }
1199
+
1200
+ _getKindFromTrackIdentifier(trackIdentifier) {
1201
+ return !trackIdentifier ? null : trackIdentifier.kind;
1202
+ }
1203
+
1204
+ _getLoadingSpinnerView() {
1205
+ return this._loading ? html`
1206
+ <div class="d2l-labs-media-player-full-area-centered">
1207
+ <d2l-loading-spinner size="100"></d2l-loading-spinner>
1208
+ </div>
1209
+ ` : null;
1210
+ }
1211
+
1212
+ _getMediaAreaView() {
1213
+ const playIcon = `tier3:${this._playing ? 'pause' : 'play'}`;
1214
+ const playTooltip = `${this._playing ? this.localize('components:mediaPlayer:pause') : this.localize('components:mediaPlayer:play')} (${KEY_BINDINGS.play})`;
1215
+
1216
+ switch (this.mediaType) {
1217
+ case SOURCE_TYPES.video:
1218
+ return html`
1219
+ ${this._getPosterView()}
1220
+ <video
1221
+ id="d2l-labs-media-player-video"
1222
+ ?controls="${IS_IOS}"
1223
+ ?autoplay="${this.autoplay}"
1224
+ ?loop="${this.loop}"
1225
+ crossorigin="${ifDefined(this.crossorigin)}"
1226
+ preload="${this.poster ? 'metadata' : 'auto'}"
1227
+ @click=${this._onVideoClick}
1228
+ @contextmenu=${this._onContextMenu}
1229
+ @durationchange=${this._onDurationChange}
1230
+ @ended=${this._onEnded}
1231
+ @error=${this._onError}
1232
+ @loadeddata=${this._onLoadedData}
1233
+ @play=${this._onPlay}
1234
+ @playing=${this._onPlaying}
1235
+ @pause=${this._onPause}
1236
+ @loadedmetadata=${this._onLoadedMetadata}
1237
+ @timeupdate=${this._onTimeUpdate}
1238
+ @volumechange=${this._onVolumeChange}
1239
+ >
1240
+ <source @error=${this._onError}>
1241
+ </video>
1242
+ `;
1243
+ case SOURCE_TYPES.audio:
1244
+ return html`
1245
+ <audio
1246
+ id="d2l-labs-media-player-audio"
1247
+ ?autoplay="${this.autoplay}"
1248
+ ?loop="${this.loop}"
1249
+ crossorigin="${ifDefined(this.crossorigin)}"
1250
+ preload="auto"
1251
+ @contextmenu=${this._onContextMenu}
1252
+ @durationchange=${this._onDurationChange}
1253
+ @ended=${this._onEnded}
1254
+ @error=${this._onError}
1255
+ @loadeddata=${this._onLoadedData}
1256
+ @play=${this._onPlay}
1257
+ @playing=${this._onPlaying}
1258
+ @pause=${this._onPause}
1259
+ @loadedmetadata=${this._onLoadedMetadata}
1260
+ @timeupdate=${this._onTimeUpdate}
1261
+ @volumechange=${this._onVolumeChange}
1262
+ >
1263
+ <source @error=${this._onError}></source>
1264
+ </audio>
1265
+ ${this.transcriptViewerOn ? null : html`
1266
+ <div id="d2l-labs-media-player-audio-bars-container">
1267
+ <div id="d2l-labs-media-player-audio-play-button-container">
1268
+ <button id="d2l-labs-media-player-audio-play-button" title="${playTooltip}" aria-label="${playTooltip}" @click=${this._togglePlay}>
1269
+ <d2l-icon icon="${playIcon}"></d2l-icon>
1270
+ </button>
1271
+ </div>
1272
+
1273
+ <d2l-labs-media-player-audio-bars ?playing="${this._playing}"></d2l-labs-media-player-audio-bars>
1274
+ </div>`}
1275
+ `;
1276
+ default:
1277
+ return null;
1278
+ }
1279
+ }
1280
+
1281
+ _getMetadata() {
1282
+ if (!this.metadata) return;
1283
+
1284
+ const data = (typeof this.metadata === 'string' || this.metadata instanceof String) ? JSON.parse(this.metadata) : this.metadata;
1285
+ if (!(data && data.chapters && data.chapters.length > 0)) return;
1286
+ let chapters = data.chapters.map(({ time, title }) => {
1287
+ return {
1288
+ time: parseInt(time),
1289
+ title
1290
+ };
1291
+ }).sort((a, b) => a.time - b.time);
1292
+
1293
+ if (!data.cuts) {
1294
+ data.cuts = [];
1295
+ }
1296
+
1297
+ // updating the chapter times based on the cuts, loops over all chapters per cut because it can change multiple chapters
1298
+ let cutDiff = 0;
1299
+ for (const cut of data.cuts) {
1300
+ const cutIn = cut.in - cutDiff;
1301
+
1302
+ const newChapters = new Map(); // using map to preserve sort ordering
1303
+
1304
+ if (!cut.out) { // if cut is until the end of the video
1305
+ for (const chapter of chapters) {
1306
+ if (chapter.time < cutIn) {
1307
+ newChapters.set(chapter.time, chapter.title);
1308
+ }
1309
+ }
1310
+ } else {
1311
+ const cutOut = cut.out - cutDiff;
1312
+ const cutLength = cutOut - cutIn;
1313
+
1314
+ for (const chapter of chapters) {
1315
+ let newTime = chapter.time;
1316
+ if (chapter.time > cutIn && chapter.time <= cutOut) {
1317
+ newTime = cutIn;
1318
+ } else if (chapter.time > cutOut) {
1319
+ newTime = chapter.time - cutLength;
1320
+ }
1321
+
1322
+ newChapters.set(newTime, chapter.title);
1323
+ }
1324
+
1325
+ cutDiff += cutLength;
1326
+ }
1327
+
1328
+ chapters = [...newChapters].map(([chapterTime, chapterTitle]) => ({
1329
+ time: chapterTime,
1330
+ title: chapterTitle
1331
+ }));
1332
+ }
1333
+ this._chapters = chapters;
1334
+ }
1335
+
1336
+ _getPercentageTime(time) {
1337
+ if (this._media) return (time / this.duration) * 100;
1338
+ }
1339
+
1340
+ _getPosterView() {
1341
+ if (!this.poster || this.autoplay || !this._posterVisible) return;
1342
+
1343
+ const playIcon = !this._loading ? html`
1344
+ <button id="d2l-labs-media-player-video-poster-play-button" aria-label=${this.localize('components:mediaPlayer:play')} title=${this.localize('components:mediaPlayer:play')} transcript="${ifDefined(this.transcriptViewerOn ? true : undefined)}"
1345
+ @click=${this._onVideoClick}>
1346
+ <d2l-icon icon="tier1:play" theme="${ifDefined(this._getTheme())}"></d2l-icon>
1347
+ </button>
1348
+ ` : null;
1349
+
1350
+ return html`
1351
+ ${playIcon}
1352
+ <img
1353
+ id="d2l-labs-media-player-video-poster"
1354
+ src="${ifDefined(this.poster)}"
1355
+ @click=${this._onVideoClick}
1356
+ />
1357
+ `;
1358
+ }
1359
+
1360
+ _getPreference(preferenceKey) {
1361
+ return localStorage.getItem(preferenceKey);
1362
+ }
1363
+
1364
+ _getQualityFromNode(node) {
1365
+ return node.getAttribute('label');
1366
+ }
1367
+
1368
+ _getQualityMenuView() {
1369
+ return !this.src && this._sources && Object.keys(this._sources).length > 1 && this._selectedQuality ? html`
1370
+ <d2l-menu-item text="${this.localize('components:mediaPlayer:quality')}">
1371
+ <div slot="supporting">${this._selectedQuality}</div>
1372
+ <d2l-menu @d2l-menu-item-change=${this._onQualityMenuItemChange} theme="${ifDefined(this._getTheme())}">
1373
+ ${Object.keys(this._sources).map(quality => html`
1374
+ <d2l-menu-item-radio
1375
+ ?selected=${this._selectedQuality === quality}
1376
+ text=${quality}
1377
+ value=${quality}
1378
+ ></d2l-menu-item-radio>
1379
+ `)}
1380
+ </d2l-menu>
1381
+ </d2l-menu-item>
1382
+ ` : null;
1383
+ }
1384
+
1385
+ _getSearchResultsView() {
1386
+ return this._searchResults.map(result => {
1387
+ return html`
1388
+ <d2l-icon
1389
+ @click=${this._onTimelineMarkerClick(result)}
1390
+ class="d2l-labs-media-player-search-marker"
1391
+ icon="tier1:subscribe-filled"
1392
+ theme="${ifDefined(this._getTheme())}"
1393
+ style=${styleMap({ left: `${this._getPercentageTime(result)}%` })}
1394
+ ></d2l-icon>
1395
+ `;
1396
+ });
1397
+ }
1398
+
1399
+ _getSeekbarResolutionMultipler() {
1400
+ return this._duration < 10 ? 1000 : this._duration < 100 ? 100 : 10;
1401
+ }
1402
+
1403
+ _getSeekbarValue(time) {
1404
+ return time * this._getSeekbarResolutionMultipler();
1405
+ }
1406
+
1407
+ _getSelectedTextTrack() {
1408
+ if (!this._media) return null;
1409
+ const selectedTrackSrcLang = this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
1410
+ for (let i = 0; i < this._media.textTracks.length; i++) {
1411
+ if (this._media.textTracks[i].language === selectedTrackSrcLang) {
1412
+ return this._media.textTracks[i];
1413
+ }
1414
+ }
1415
+ return null;
1416
+ }
1417
+
1418
+ _getSrclangFromTrackIdentifier(trackIdentifier) {
1419
+ return !trackIdentifier ? null : trackIdentifier.srclang;
1420
+ }
1421
+
1422
+ _getTheme() {
1423
+ return this.mediaType === SOURCE_TYPES.video ? 'dark' : undefined;
1424
+ }
1425
+
1426
+ _getThumbnails() {
1427
+ if (!this.thumbnails) return;
1428
+ this._thumbnailsImage = new Image();
1429
+ this._thumbnailsImage.src = this.thumbnails;
1430
+ }
1431
+
1432
+ _getTimelinePreview() {
1433
+ if (!this._hovering) return;
1434
+ const chapterTitleLabel = this._getChapterTitle();
1435
+
1436
+ if (!(this.thumbnails && this._thumbnailsImage))
1437
+ return html`
1438
+ <div id="d2l-labs-media-player-thumbnails-preview-container"
1439
+ style="width: ${DEFAULT_PREVIEW_WIDTH}px; left: clamp(${DEFAULT_PREVIEW_WIDTH / 2}px, ${this._timelinePreviewOffset}%, calc(100% - ${DEFAULT_PREVIEW_WIDTH / 2}px));">
1440
+ <div
1441
+ id="d2l-labs-media-player-thumbnails-preview-image"
1442
+ >
1443
+ <span id="d2l-labs-media-player-thumbnails-preview-time">${MediaPlayer._formatTime(this._hoverTime)}</span>
1444
+ </div>
1445
+ ${chapterTitleLabel &&
1446
+ html`<span class="d2l-label-text" id="d2l-labs-media-player-thumbnails-preview-chapter" style="bottom: ${DEFAULT_PREVIEW_HEIGHT - 60}px">${chapterTitleLabel}</span>`}
1447
+ </div>
1448
+ `;
1449
+
1450
+ // format of the thumbnail is [url]/th<height>w<height>i<interval>-<hash>.[png|jpg]
1451
+ const matches = this.thumbnails.match(/th(\d+)w(\d+)i(\d+)[^/]*$/i);
1452
+ if (matches && matches.length !== 4) return; // no matches
1453
+ const [ , thumbHeight, thumbWidth, interval] = matches;
1454
+
1455
+ const width = this._thumbnailsImage.width;
1456
+ const height = this._thumbnailsImage.height;
1457
+
1458
+ const rows = height / thumbHeight;
1459
+ const columns = width / thumbWidth;
1460
+
1461
+ let thumbNum = Math.floor(this._hoverTime / interval);
1462
+ if (thumbNum >= rows * columns) thumbNum = rows * columns - 1;
1463
+
1464
+ const row = Math.floor(thumbNum / columns);
1465
+ const column = thumbNum % columns;
1466
+
1467
+ return html`
1468
+ <div id="d2l-labs-media-player-thumbnails-preview-container"
1469
+ style="width: ${thumbWidth}px; left: clamp(${thumbWidth / 2}px, ${this._timelinePreviewOffset}%, calc(100% - ${thumbWidth / 2}px));">
1470
+ <div
1471
+ id="d2l-labs-media-player-thumbnails-preview-image"
1472
+ style="height: ${thumbHeight}px; background: url(${this._thumbnailsImage.src}) ${-column * thumbWidth}px ${-row * thumbHeight}px / ${width}px ${height}px;"
1473
+ >
1474
+ <span id="d2l-labs-media-player-thumbnails-preview-time">${MediaPlayer._formatTime(this._hoverTime)}</span>
1475
+ </div>
1476
+ ${chapterTitleLabel &&
1477
+ html`<span class="d2l-label-text" id="d2l-labs-media-player-thumbnails-preview-chapter" style="bottom: ${thumbHeight}px">${chapterTitleLabel}</span>`}
1478
+ </div>
1479
+ `;
1480
+ }
1481
+
1482
+ _getTrackIdentifier(srclang, kind) {
1483
+ return JSON.stringify({
1484
+ kind,
1485
+ srclang
1486
+ });
1487
+ }
1488
+
1489
+ _getTracksMenuView() {
1490
+ const isTrackSelected = (track) => (
1491
+ track.srclang === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier) &&
1492
+ track.kind === this._getKindFromTrackIdentifier(this._selectedTrackIdentifier)
1493
+ );
1494
+
1495
+ return this._tracks.length > 0 && !this.hideCaptionsSelection ? html`
1496
+ <d2l-menu-item text="${this.transcriptViewerOn ? this.localize('components:mediaPlayer:language') : this.localize('components:mediaPlayer:captions')}">
1497
+ <div slot="supporting">${this._selectedTrackLabel}</div>
1498
+ <d2l-menu id="d2l-labs-media-player-captions-menu"
1499
+ @d2l-menu-item-change=${this._onTracksMenuItemChange} theme="${ifDefined(this._getTheme())}">
1500
+ ${this.transcriptViewerOn ? '' : html`
1501
+ <d2l-menu-item-radio text="${this.localize('components:mediaPlayer:off')}" ?selected="${!this._selectedTrackIdentifier}"></d2l-menu-item-radio>`}
1502
+ ${this._tracks.map(track => html`
1503
+ <d2l-menu-item-radio
1504
+ ?selected="${isTrackSelected(track)}"
1505
+ text="${`${track.label}${track.kind === TRACK_KINDS.captions ? ` (${this.localize('components:mediaPlayer:closedCaptionsAcronym')})` : ''}`}"
1506
+ value="${this._getTrackIdentifier(track.srclang, track.kind)}"
1507
+ ></d2l-menu-item-radio>
1508
+ `)}
1509
+ </d2l-menu>
1510
+ </d2l-menu-item>
1511
+ ` : null;
1512
+ }
1513
+
1514
+ _hidingCustomControls() {
1515
+ const settingsMenuOpened = this._settingsMenu && this._settingsMenu.opened;
1516
+ return this.isIOSVideo || (this._playing && !this._recentlyShowedCustomControls && !this._hoveringMediaControls && !settingsMenuOpened && !this._usingVolumeContainer && this.mediaType === SOURCE_TYPES.video);
1517
+ }
1518
+
1519
+ _listenForKeyboard(e) {
1520
+ if (this._searchInputFocused) {
1521
+ return;
1522
+ }
1523
+ this._showControls(true);
1524
+ switch (e.key) {
1525
+ case KEY_BINDINGS.play:
1526
+ this._togglePlay();
1527
+ break;
1528
+ case KEY_BINDINGS.mute:
1529
+ this._toggleMute();
1530
+ break;
1531
+ case KEY_BINDINGS.fullscreen:
1532
+ this._toggleFullscreen();
1533
+ break;
1534
+ }
1535
+ }
1536
+
1537
+ _loadVisibilityObserver({ target }) {
1538
+ if (this._observer) return;
1539
+ this._observer = new IntersectionObserver(([ioEvent]) => {
1540
+ if (!ioEvent.isIntersecting && this._playing) {
1541
+ this.pause();
1542
+ this._observer.unobserve(target);
1543
+ this._observer.disconnect();
1544
+ delete this._observer;
1545
+ }
1546
+ }, {
1547
+ root: null, // entire viewport
1548
+ rootMargin: '0px',
1549
+ threshold: 0.05, // if 5% of the target is in the viewport
1550
+ });
1551
+ this._observer.observe(target);
1552
+ }
1553
+
1554
+ _onContextMenu(e) {
1555
+ if (!this.allowDownload) e.preventDefault();
1556
+ }
1557
+
1558
+ async _onCueChange() {
1559
+ if (this.transcriptViewerOn) {
1560
+ if (!this._transcriptViewer) {
1561
+ this._transcriptViewer = this.shadowRoot.getElementById('video-transcript-viewer')
1562
+ || this.shadowRoot.getElementById('audio-transcript-viewer');
1563
+ }
1564
+ this._updateTranscriptViewerCues();
1565
+ await this.requestUpdate();
1566
+ this._scrollTranscriptViewer();
1567
+ }
1568
+ for (let i = 0; i < this._media.textTracks.length; i++) {
1569
+ if (this._media.textTracks[i].mode === 'hidden') {
1570
+ if (this._media.textTracks[i].activeCues?.length > 0) {
1571
+ this._trackText = this._sanitizeText(this._media.textTracks[i].activeCues[0].text);
1572
+ } else this._trackText = null;
1573
+
1574
+ this.dispatchEvent(new CustomEvent('cuechange'));
1575
+ }
1576
+ }
1577
+ }
1578
+
1579
+ _onDownloadButtonPress() {
1580
+ const linkHref = this._getDownloadLink();
1581
+
1582
+ const anchor = document.createElement('a');
1583
+ anchor.href = linkHref;
1584
+ anchor.download = '';
1585
+ anchor.click();
1586
+ anchor.remove();
1587
+ }
1588
+
1589
+ _onDragEndSeek() {
1590
+ // _onDragEndSeek() is called once before firstUpdated()
1591
+ if (this._seekBar) {
1592
+ this._updateCurrentTimeFromSeekbarProgress();
1593
+
1594
+ if (this._pausedForSeekDrag) {
1595
+ this._media.play();
1596
+ }
1597
+ this._dragging = false;
1598
+ }
1599
+
1600
+ this.dispatchEvent(new CustomEvent('seeked'));
1601
+ }
1602
+
1603
+ _onDragStartSeek() {
1604
+ if (this._playing) {
1605
+ this._media.pause();
1606
+ this._pausedForSeekDrag = true;
1607
+ }
1608
+
1609
+ this._dragging = true;
1610
+ setTimeout(() => {
1611
+ this._updateCurrentTimeFromSeekbarProgress();
1612
+ }, 0);
1613
+
1614
+ this.dispatchEvent(new CustomEvent('seeking'));
1615
+ }
1616
+
1617
+ _onDragStartVolume() {
1618
+ setTimeout(() => {
1619
+ this._onPositionChangeVolume();
1620
+ }, 0);
1621
+ }
1622
+
1623
+ _onDurationChange(e) {
1624
+ const newDuration = e.target.duration;
1625
+ const newDurationIsValid = isFinite(newDuration) && !isNaN(newDuration);
1626
+ const hintIsValid = isFinite(this.durationHint) && !isNaN(this.durationHint);
1627
+ this._duration = newDurationIsValid || !hintIsValid
1628
+ ? newDuration
1629
+ : this.durationHint;
1630
+ this.dispatchEvent(new CustomEvent('durationchange'));
1631
+ }
1632
+
1633
+ _onEnded() {
1634
+ this._playRequested = false;
1635
+ this.dispatchEvent(new CustomEvent('ended'));
1636
+ }
1637
+
1638
+ _onError() {
1639
+ this.dispatchEvent(new CustomEvent('error'));
1640
+ }
1641
+
1642
+ _onHoverEnd() {
1643
+ this._hovering = false;
1644
+ }
1645
+
1646
+ _onHoverMove() {
1647
+ if (this._hovering && this._seekBar) {
1648
+ this._hoverTime = this._seekBar.hoverValue / this._getSeekbarResolutionMultipler();
1649
+ this._timelinePreviewOffset = (this._hoverTime / this._duration) * 100;
1650
+ }
1651
+ }
1652
+
1653
+ _onHoverStart() {
1654
+ this._hovering = true;
1655
+ }
1656
+
1657
+ _onLoadedData() {
1658
+ const media = this._media;
1659
+ const width = media.videoWidth;
1660
+ const height = media.videoHeight;
1661
+ const aspectRatio = width / height;
1662
+ this._mediaContainerAspectRatio = { 'aspect-ratio': Number.isNaN(aspectRatio) ? 'auto' : aspectRatio.toString() };
1663
+ this._disableNativeCaptions();
1664
+ this.dispatchEvent(new CustomEvent('loadeddata'));
1665
+ }
1666
+
1667
+ _onLoadedMetadata() {
1668
+ if (this._stateBeforeLoad) {
1669
+ this.currentTime = this._stateBeforeLoad.currentTime;
1670
+ this._media.autoplay = this._stateBeforeLoad.autoplay;
1671
+
1672
+ if (!this._stateBeforeLoad.paused) {
1673
+ this.play();
1674
+ }
1675
+
1676
+ this._stateBeforeLoad = null;
1677
+ }
1678
+
1679
+ this._maintainHeight = null;
1680
+ this._loading = false;
1681
+
1682
+ const speed = this._getPreference(PREFERENCES_SPEED_KEY) || DEFAULT_SPEED;
1683
+ this._onPlaybackSpeedsMenuItemChange({
1684
+ target: {
1685
+ value: speed
1686
+ }
1687
+ });
1688
+
1689
+ const volume = this._getPreference(PREFERENCES_VOLUME_KEY) || DEFAULT_VOLUME;
1690
+ this.volume = volume;
1691
+
1692
+ this.dispatchEvent(new CustomEvent('loadedmetadata'));
1693
+ }
1694
+
1695
+ _onPause() {
1696
+ this._playing = false;
1697
+ this.dispatchEvent(new CustomEvent('pause'));
1698
+ }
1699
+
1700
+ _onPlay() {
1701
+ this.dispatchEvent(new CustomEvent('play'));
1702
+ }
1703
+
1704
+ _onPlaybackSpeedsMenuItemChange(e) {
1705
+ const speed = e.target.value;
1706
+ this._media.playbackRate = speed;
1707
+ this._selectedSpeed = speed;
1708
+ this._setPreference(PREFERENCES_SPEED_KEY, speed);
1709
+ }
1710
+
1711
+ _onPlayerTimeBlur(event) {
1712
+ if (event && event.target) {
1713
+ event.target.setAttribute('aria-live', 'off');
1714
+ event.target.setAttribute('aria-hidden', 'true');
1715
+ }
1716
+ }
1717
+
1718
+ _onPlayerTimeFocus(event) {
1719
+ if (event && event.target) {
1720
+ event.target.setAttribute('aria-live', 'polite');
1721
+ event.target.removeAttribute('aria-hidden');
1722
+ }
1723
+ }
1724
+
1725
+ _onPlaying() {
1726
+ this._playing = true;
1727
+ this._pausedForSeekDrag = false;
1728
+ }
1729
+
1730
+ _onPositionChangeSeek() {
1731
+ this._updateCurrentTimeFromSeekbarProgress();
1732
+ this._showControls(true);
1733
+ }
1734
+
1735
+ _onPositionChangeVolume() {
1736
+ this.volume = this._volumeSlider.immediateValue / 100;
1737
+ }
1738
+
1739
+ _onQualityMenuItemChange(e) {
1740
+ if (
1741
+ !this._sources ||
1742
+ !Object.keys(this._sources) > 0 ||
1743
+ e.target.value === this._selectedQuality ||
1744
+ !(e.target.value in this._sources)
1745
+ ) return;
1746
+
1747
+ this._selectedQuality = e.target.value;
1748
+ this._reloadSource();
1749
+ }
1750
+
1751
+ _onRetryButtonPress() {
1752
+ this._loading = true;
1753
+ }
1754
+
1755
+ _onSearchButtonPress() {
1756
+ if (this._searchContainer.classList.includes((SEARCH_CONTAINER_HOVER_CLASS))) {
1757
+ this._searchContainer.classList.remove(SEARCH_CONTAINER_HOVER_CLASS);
1758
+ } else {
1759
+ this._onSearchContainerHover(true);
1760
+ }
1761
+ }
1762
+
1763
+ _onSearchContainerHover() {
1764
+ if (this._searchInput.value === '') {
1765
+ this._searchContainer.classList.remove(SEARCH_CONTAINER_HOVER_CLASS);
1766
+ } else {
1767
+ this._searchContainer.classList.add(SEARCH_CONTAINER_HOVER_CLASS);
1768
+ }
1769
+ }
1770
+
1771
+ _onSearchInputBlur() {
1772
+ this._searchInputFocused = false;
1773
+ }
1774
+
1775
+ _onSearchInputChanged() {
1776
+ if (this.#searchTimeout) {
1777
+ clearTimeout(this.#searchTimeout);
1778
+ }
1779
+ this.#searchTimeout = setTimeout(() => {
1780
+ this._onSearchContainerHover();
1781
+ if (this._searchInput.value.length < 2) {
1782
+ this._searchResults = [];
1783
+ return;
1784
+ }
1785
+ const srclang = this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
1786
+ const searcher = this._searchInstances[srclang];
1787
+ this._searchResults = searcher.search(this._searchInput.value)
1788
+ .map(result => (isNaN(result.item.startTime) ? result.item.start : result.item.startTime));
1789
+ }, 500);
1790
+ }
1791
+
1792
+ _onSearchInputFocus() {
1793
+ this._searchInputFocused = true;
1794
+ }
1795
+
1796
+ async _onSlotChange(e) {
1797
+ this._tracks = [];
1798
+ const nodes = e.target.assignedNodes();
1799
+ let defaultTrack;
1800
+
1801
+ // this.src case is handled in updated() event
1802
+ if (!this.src) {
1803
+ // The onSlotChange event does not monitor changes to slot children, so we need
1804
+ // to detect the change to the <source> element via a MutationObserver
1805
+ const sourceNodes = nodes.filter(node => node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SOURCE');
1806
+ const observer = new MutationObserver(mutationList => {
1807
+ mutationList.forEach(mutation => {
1808
+ this._parseSourceNode(mutation.target);
1809
+ });
1810
+
1811
+ this._reloadSource();
1812
+ });
1813
+ sourceNodes.map(node => {
1814
+ observer.observe(node, { attributes: true });
1815
+ });
1816
+ this._updateSources(sourceNodes);
1817
+ this._reloadSource();
1818
+ }
1819
+
1820
+ for (let i = 0; i < nodes.length; i++) {
1821
+ const node = nodes[i];
1822
+
1823
+ if (node.nodeType !== Node.ELEMENT_NODE || node.nodeName !== 'TRACK') continue;
1824
+
1825
+ if (!(node.kind in TRACK_KINDS)) {
1826
+ console.warn(`d2l-labs-media-player component requires 'kind' text on track to be one of ${Object.keys(TRACK_KINDS)}`);
1827
+ continue;
1828
+ }
1829
+
1830
+ if (!node.label) {
1831
+ console.warn("d2l-labs-media-player component requires 'label' text on track");
1832
+ continue;
1833
+ }
1834
+
1835
+ if (!node.src) {
1836
+ console.warn("d2l-labs-media-player component requires 'src' text on track");
1837
+ continue;
1838
+ }
1839
+
1840
+ if (!node.srclang) {
1841
+ console.warn("d2l-labs-media-player component requires 'srclang' text on track");
1842
+ continue;
1843
+ }
1844
+
1845
+ const res = await fetch(node.src);
1846
+ if (res.status !== 200) {
1847
+ console.warn(`d2l-labs-media-player component could not load track from '${node.src}'`);
1848
+ this.dispatchEvent(new CustomEvent('trackloadfailed'));
1849
+ continue;
1850
+ }
1851
+
1852
+ const text = await res.text();
1853
+
1854
+ try {
1855
+ node.cues = parseSRT(text);
1856
+ node.srt = true;
1857
+ } catch {
1858
+ node.srt = false;
1859
+ }
1860
+
1861
+ this._tracks.push({
1862
+ cues: node.cues,
1863
+ kind: node.kind,
1864
+ label: node.label,
1865
+ src: node.src,
1866
+ srclang: node.srclang,
1867
+ srt: node.srt,
1868
+ default: node.default || node['default-ignore-preferences'],
1869
+ text
1870
+ });
1871
+
1872
+ const defaultIgnorePreferences = node.attributes['default-ignore-preferences'];
1873
+ if (node.default || defaultIgnorePreferences) {
1874
+ // Stringified to be parsed in initializeTracks
1875
+ defaultTrack = {
1876
+ srclang: node.srclang,
1877
+ kind: node.kind,
1878
+ ignorePreferences: !!defaultIgnorePreferences,
1879
+ };
1880
+ }
1881
+ }
1882
+
1883
+ await new Promise(resolve => {
1884
+ const interval = setInterval(() => {
1885
+ if (!this._media) return;
1886
+
1887
+ clearInterval(interval);
1888
+
1889
+ resolve();
1890
+ });
1891
+ });
1892
+
1893
+ const oldTracks = this._media.querySelectorAll('track');
1894
+ oldTracks.forEach(track => this._media.removeChild(track));
1895
+
1896
+ this._tracks.forEach(track => {
1897
+ if (track.srt) {
1898
+ const trackElement = this._media.addTextTrack(track.kind, track.label, track.srclang);
1899
+ trackElement.oncuechange = this._onCueChange.bind(this);
1900
+
1901
+ track.cues.forEach(cue => {
1902
+ trackElement.addCue(new VTTCue(cue.start, cue.end, cue.text));
1903
+ });
1904
+ this.dispatchEvent(new CustomEvent('trackloaded'));
1905
+ if (track.srclang === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier)) {
1906
+ this._syncDisplayedTrackTextToSelectedTrack();
1907
+ }
1908
+
1909
+ this._searchInstances[track.srclang] = new Fuse(track.cues, FUSE_OPTIONS({
1910
+ sortFn: (a, b) => a.start - b.start
1911
+ }));
1912
+ } else {
1913
+ const trackElement = document.createElement('track');
1914
+ trackElement.src = track.src;
1915
+ trackElement.label = track.label;
1916
+ trackElement.kind = track.kind;
1917
+ trackElement.srclang = track.srclang;
1918
+ trackElement.default = track.default ? '' : undefined;
1919
+ trackElement.oncuechange = this._onCueChange.bind(this);
1920
+ this._media.appendChild(trackElement);
1921
+ trackElement.addEventListener('load', () => {
1922
+ this.dispatchEvent(new CustomEvent('trackloaded'));
1923
+ if (track.srclang === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier)) {
1924
+ this._syncDisplayedTrackTextToSelectedTrack();
1925
+ }
1926
+ });
1927
+
1928
+ const { cues } = this._webVTTParser.parse(track.text, 'metadata');
1929
+ this._searchInstances[track.srclang] = new Fuse(cues, FUSE_OPTIONS({
1930
+ sortFn: (a, b) => a.startTime - b.startTime,
1931
+ }));
1932
+ }
1933
+ });
1934
+
1935
+ // Safari sets the mode of text tracks itself, which have to be overwritten here
1936
+ // It has been observed to happen synchronously, or during the next event loop
1937
+ // Changing the mode in this event loop and the next catches both scenarios
1938
+ // Needs to be caught right away, since the cuechange event can be emitted immediately
1939
+ // Set default track to 'hidden'
1940
+ const initializeTracks = (() => {
1941
+ if (defaultTrack && defaultTrack.ignorePreferences) {
1942
+ this._selectedTrackIdentifier = defaultTrack;
1943
+ } else {
1944
+ const trackPreference = this._getPreference(PREFERENCES_TRACK_IDENTIFIER_KEY);
1945
+ this._selectedTrackIdentifier = trackPreference ? JSON.parse(trackPreference) : defaultTrack;
1946
+ }
1947
+
1948
+ this._disableNativeCaptions();
1949
+ }).bind(this);
1950
+
1951
+ initializeTracks();
1952
+ setTimeout(initializeTracks, 0);
1953
+ }
1954
+
1955
+ _onTimelineMarkerClick(time) {
1956
+ return () => this.currentTime = time;
1957
+ }
1958
+
1959
+ _onTimeUpdate() {
1960
+ this.dispatchEvent(new CustomEvent('timeupdate'));
1961
+ }
1962
+
1963
+ _onTrackContainerClick() {
1964
+ if (this.mediaType === SOURCE_TYPES.video) {
1965
+ this._onVideoClick();
1966
+ }
1967
+ }
1968
+
1969
+ _onTracksMenuItemChange(e) {
1970
+ setTimeout(() => {
1971
+ this.dispatchEvent(new CustomEvent('tracksmenuitemchanged'));
1972
+ }, 0);
1973
+ this._trackText = null;
1974
+
1975
+ this._selectedTrackIdentifier = e.target.value;
1976
+
1977
+ if (this._selectedTrackIdentifier) {
1978
+ this._setPreference(PREFERENCES_TRACK_IDENTIFIER_KEY, this._selectedTrackIdentifier);
1979
+ this._selectedTrackIdentifier = JSON.parse(this._selectedTrackIdentifier);
1980
+ } else {
1981
+ this._clearPreference(PREFERENCES_TRACK_IDENTIFIER_KEY);
1982
+ }
1983
+
1984
+ for (let i = 0; i < this._media.textTracks.length; i++) {
1985
+ const track = this._media.textTracks[i];
1986
+
1987
+ if (track.language === this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier) &&
1988
+ track.kind === this._getKindFromTrackIdentifier(this._selectedTrackIdentifier)
1989
+ ) {
1990
+ this._media.textTracks[i].mode = 'hidden';
1991
+ } else {
1992
+ this._media.textTracks[i].mode = 'disabled';
1993
+ }
1994
+ }
1995
+ if (this.transcriptViewerOn) {
1996
+ this._updateTranscriptViewerCues();
1997
+ }
1998
+ this._onSearchInputChanged();
1999
+ }
2000
+
2001
+ _onVideoClick() {
2002
+ // Given that we are currently not rendering custom controls on the iOS video player,
2003
+ // we let the native controls/player handle the play/pause toggling
2004
+ if (IS_IOS) {
2005
+ if (this._posterVisible) this._togglePlay();
2006
+ return;
2007
+ }
2008
+
2009
+ this._togglePlay();
2010
+ this._showControls(true);
2011
+
2012
+ if (this._videoClicked) {
2013
+ this._toggleFullscreen();
2014
+ } else {
2015
+ setTimeout(() => {
2016
+ if (this._videoClicked) {
2017
+ this._videoClicked = false;
2018
+ }
2019
+ }, TIMEOUT_FOR_DOUBLE_CLICK_MS);
2020
+ }
2021
+
2022
+ this._videoClicked = !this._videoClicked;
2023
+ }
2024
+
2025
+ _onVideoContainerMouseMove() {
2026
+ this._showControls(true);
2027
+ }
2028
+
2029
+ _onVolumeChange() {
2030
+ this._volume = this.volume;
2031
+ this._muted = this._volume === 0;
2032
+ }
2033
+
2034
+ _parseSourceNode(node) {
2035
+ const quality = this._getQualityFromNode(node);
2036
+ if (!quality) {
2037
+ console.warn("d2l-labs-media-player component requires 'label' text on source");
2038
+ return;
2039
+ }
2040
+ if (!node.src) {
2041
+ console.warn("d2l-labs-media-player component requires 'src' text on source");
2042
+ return;
2043
+ }
2044
+
2045
+ this._sources[quality] = node.src;
2046
+ return quality;
2047
+ }
2048
+
2049
+ _reloadSource() {
2050
+ if (this._media) {
2051
+ const oldSourceNode = this._media.getElementsByTagName('source')[0];
2052
+ const updatedSource = this._getCurrentSource();
2053
+ if (oldSourceNode.getAttribute('src') !== updatedSource) {
2054
+ this._loading = true;
2055
+
2056
+ oldSourceNode.setAttribute('src', updatedSource);
2057
+
2058
+ // Note: Safari doesn't emit an error if a URL expires during playback. To work
2059
+ // around this, we manually fire an error slightly before the URL is expected
2060
+ // to expire. Note that this only currently works with URLs that include the
2061
+ // "Expires" query parameter (e.g. CloudFront signed URLs). It's also not a
2062
+ // good long-term solution since it depends on a somewhat accurate client
2063
+ // system time.
2064
+ if (isSafari() && updatedSource !== undefined) {
2065
+ const expires = tryParseUrlExpiry(updatedSource);
2066
+ if (expires) {
2067
+ const timeToExpiry = (expires * 1000) - Date.now() - SAFARI_EXPIRY_EARLY_SWAP_SECONDS;
2068
+ const timeoutPeriod = Math.max(timeToExpiry, SAFARI_EXPIRY_MIN_ERROR_EMIT_SECONDS);
2069
+ setTimeout(() => this._onError(), timeoutPeriod);
2070
+ }
2071
+ }
2072
+
2073
+ // Maintain the height while loading the new source to prevent
2074
+ // the video object from resizing temporarily
2075
+ this._maintainHeight = this._media.clientHeight;
2076
+
2077
+ if (!this._stateBeforeLoad) {
2078
+ this._stateBeforeLoad = {
2079
+ paused: !this._pausedForSeekDrag && this.paused && !this._playRequested,
2080
+ autoplay: this._media.autoplay,
2081
+ currentTime: this.currentTime
2082
+ };
2083
+ }
2084
+
2085
+ this.pause();
2086
+ this.load();
2087
+ }
2088
+ }
2089
+ }
2090
+
2091
+ _renderTranscriptViewer() {
2092
+ if (!this._media) {
2093
+ return;
2094
+ }
2095
+ const captionsMenu = this.shadowRoot.getElementById('d2l-labs-media-player-captions-menu');
2096
+ if (captionsMenu) {
2097
+ this._captionsMenuReturnItem = captionsMenu.shadowRoot.querySelector('d2l-menu-item-return');
2098
+ this._captionsMenuReturnItem?.setAttribute('text', this.localize('components:mediaPlayer:language'));
2099
+ }
2100
+
2101
+ if (!this._transcriptViewer) {
2102
+ this._onCueChange();
2103
+ }
2104
+
2105
+ const isVideo = this.mediaType === SOURCE_TYPES.video;
2106
+ const captionsToHtml = (item) => {
2107
+ const updateTime = async() => {
2108
+ this.currentTime = item.startTime;
2109
+ this._media.currentTime = item.startTime;
2110
+ };
2111
+ return html`
2112
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'}
2113
+ @click=${updateTime}>
2114
+ ${item.text}<br>
2115
+ </div>`;
2116
+ };
2117
+
2118
+ return html`
2119
+ <span id="close-transcript"
2120
+ @click=${this._closeTranscript}>
2121
+ <d2l-icon class="d2l-button-icon"
2122
+ id=${isVideo ? 'video-close-transcript-icon' : 'audio-close-transcript-icon'}
2123
+ icon="tier1:close-small"></d2l-icon>
2124
+ </span>
2125
+ <div
2126
+ id=${isVideo ? 'video-transcript-viewer' : 'audio-transcript-viewer'}
2127
+ >
2128
+ <div class="transcript-cue-container">
2129
+ ${this.beforeCaptions.map(captionsToHtml)}
2130
+ <div class=${isVideo ? 'video-transcript-cue' : 'audio-transcript-cue'} active
2131
+ id="transcript-viewer-active-cue">
2132
+ ${this.transcriptActiveCue?.text}
2133
+ </div>
2134
+ ${this.afterCaptions.map(captionsToHtml)}
2135
+ </div>
2136
+ </div>
2137
+ <d2l-dropdown-button-subtle
2138
+ id=${isVideo ? 'video-transcript-download-button' : 'audio-transcript-download-button'}
2139
+ text="${this.localize('components:mediaPlayer:download')}">
2140
+ <d2l-dropdown-menu id=${isVideo ? 'video-transcript-download-menu' : 'audio-transcript-download-menu'}>
2141
+ <d2l-menu>
2142
+ <d2l-menu-item @click=${this._downloadTranscript} text="${this.localize('components:mediaPlayer:transcriptTxt')}"></d2l-menu-item>
2143
+ <d2l-menu-item @click=${this._downloadCaptions} text="${this.localize('components:mediaPlayer:captionsVtt')}"></d2l-menu-item>
2144
+ </d2l-menu>
2145
+ </d2l-dropdown-menu>
2146
+ </d2l-dropdown-button-subtle>
2147
+ `;
2148
+ }
2149
+
2150
+ _sanitizeText(text) {
2151
+ return text.replace(/<br \/>/g, '\n');
2152
+ }
2153
+
2154
+ _scrollTranscriptViewer() {
2155
+ const cue = this.shadowRoot.getElementById('transcript-viewer-active-cue');
2156
+ const cueRect = cue?.getBoundingClientRect();
2157
+ const transcriptRect = this._transcriptViewer?.getBoundingClientRect();
2158
+ if (cue && cueRect && transcriptRect) {
2159
+ if (cueRect.bottom > transcriptRect.bottom && cueRect.height <= transcriptRect.height) {
2160
+ this._transcriptViewer.scrollBy({ top: cueRect.bottom - transcriptRect.bottom + transcriptRect.height - cueRect.height, left: 0, behavior: 'smooth' });
2161
+ } else if (cueRect.top < transcriptRect.top) {
2162
+ this._transcriptViewer.scrollBy({ top: cueRect.top - transcriptRect.top, left: 0, behavior: 'smooth' });
2163
+ }
2164
+ }
2165
+ }
2166
+
2167
+ _setPreference(preferenceKey, value) {
2168
+ if (!this.disableSetPreferences) {
2169
+ localStorage.setItem(preferenceKey, value);
2170
+ }
2171
+ }
2172
+
2173
+ _showControls(temporarily) {
2174
+ this._recentlyShowedCustomControls = true;
2175
+ clearTimeout(this._showControlsTimeout);
2176
+
2177
+ if (temporarily && !(this._searchInput && this._searchInput.value)) {
2178
+ this._showControlsTimeout = setTimeout(() => {
2179
+ this._recentlyShowedCustomControls = false;
2180
+ }, HIDE_DELAY_MS);
2181
+ }
2182
+ }
2183
+
2184
+ _startHoveringControls() {
2185
+ this._hoveringMediaControls = true;
2186
+ this._showControls(false);
2187
+ }
2188
+
2189
+ _startUpdatingCurrentTime() {
2190
+ setInterval(() => {
2191
+ if (this._media && !this._dragging) {
2192
+ this._currentTime = this._media.currentTime;
2193
+ }
2194
+ }, SEEK_BAR_UPDATE_PERIOD_MS);
2195
+ }
2196
+
2197
+ _startUsingVolumeContainer() {
2198
+ setTimeout(() => {
2199
+ this._usingVolumeContainer = true;
2200
+ }, 0);
2201
+ }
2202
+
2203
+ _stopHoveringControls() {
2204
+ this._hoveringMediaControls = false;
2205
+ this._showControls(true);
2206
+ }
2207
+
2208
+ _stopUsingVolumeContainer() {
2209
+ setTimeout(() => {
2210
+ this._usingVolumeContainer = false;
2211
+ }, 0);
2212
+ }
2213
+
2214
+ _syncDisplayedTrackTextToSelectedTrack() {
2215
+ this._trackText = null;
2216
+ const selectedTextTrack = this._getSelectedTextTrack();
2217
+
2218
+ if (!selectedTextTrack || !selectedTextTrack.cues) return;
2219
+
2220
+ for (let i = 0; i < selectedTextTrack.cues.length; i++) {
2221
+ const cue = selectedTextTrack.cues[i];
2222
+ if (
2223
+ (cue.startTime <= this.currentTime) &&
2224
+ (cue.endTime >= this.currentTime)
2225
+ ) {
2226
+ this._trackText = this._sanitizeText(cue.text);
2227
+ this.dispatchEvent(new CustomEvent('cuechange'));
2228
+ }
2229
+ }
2230
+ }
2231
+
2232
+ _toggleFullscreen() {
2233
+ if (!FULLSCREEN_ENABLED) return;
2234
+
2235
+ if (this.mediaType !== SOURCE_TYPES.video) return;
2236
+
2237
+ if (fullscreenApi.isFullscreen) {
2238
+ fullscreenApi.exit();
2239
+ } else {
2240
+ fullscreenApi.request(this._mediaContainer);
2241
+ }
2242
+ }
2243
+
2244
+ _toggleMute() {
2245
+ if (this._muted) {
2246
+ this.volume = this.preMuteVolume || 1;
2247
+ } else {
2248
+ this.preMuteVolume = this.volume;
2249
+ this.volume = 0;
2250
+ }
2251
+
2252
+ this._muted = !this._muted;
2253
+ }
2254
+
2255
+ _togglePlay() {
2256
+ this._posterVisible = false;
2257
+ if (this._media.paused) {
2258
+ if (this.playInView) {
2259
+ this._loadVisibilityObserver({ target: this._mediaContainer });
2260
+ }
2261
+ this._playRequested = true;
2262
+ this._media.play();
2263
+ } else {
2264
+ this._playRequested = false;
2265
+ this._media.pause();
2266
+ }
2267
+ }
2268
+
2269
+ _updateCurrentTimeFromSeekbarProgress() {
2270
+ this.currentTime = this._seekBar.immediateValue / this._getSeekbarResolutionMultipler();
2271
+ }
2272
+
2273
+ _updateSources(nodes) {
2274
+ this._selectedQuality = null;
2275
+ nodes.forEach((node, index) => {
2276
+ if (node.nodeType === Node.ELEMENT_NODE && node.nodeName === 'SOURCE') {
2277
+ const quality = this._parseSourceNode(node);
2278
+ if (quality && ((index === 0) || node.hasAttribute('default'))) {
2279
+ this._selectedQuality = quality;
2280
+ }
2281
+ }
2282
+ });
2283
+ }
2284
+
2285
+ _updateTranscriptViewerCues() {
2286
+ let cues = null;
2287
+ const lang = this._getSrclangFromTrackIdentifier(this._selectedTrackIdentifier);
2288
+ for (let i = 0; i < this._media.textTracks.length; i += 1) {
2289
+ const currTrack = this._media.textTracks[i];
2290
+ if (currTrack?.cues) {
2291
+ const activeCues = currTrack.activeCues;
2292
+ if (lang === currTrack.language) {
2293
+ this.transcriptActiveCue = activeCues?.[activeCues?.length - 1];
2294
+ cues = currTrack.cues;
2295
+ break;
2296
+ }
2297
+ }
2298
+ }
2299
+ if (!cues) {
2300
+ let defaultTrack;
2301
+ for (let i = 0; i < this._media.textTracks.length; i++) {
2302
+ if (this._media.textTracks[i].default) {
2303
+ defaultTrack = this._media.textTracks[i];
2304
+ break;
2305
+ }
2306
+ }
2307
+ defaultTrack = defaultTrack || this._media.textTracks[0];
2308
+ if (defaultTrack) defaultTrack.mode = 'hidden';
2309
+ this._selectedTrackIdentifier = { kind: defaultTrack?.kind, srclang: defaultTrack?.language };
2310
+ this.requestUpdate();
2311
+ return;
2312
+ }
2313
+
2314
+ this.beforeCaptions = [];
2315
+ this.afterCaptions = [];
2316
+ for (let i = 0; i < cues.length; i += 1) {
2317
+ const currCue = cues[i];
2318
+ const currTime = this._media?.currentTime;
2319
+ const before = currCue !== this.transcriptActiveCue && (currCue.endTime < currTime || currCue.endTime <= this.transcriptActiveCue?.endTime);
2320
+ if (before) {
2321
+ this.beforeCaptions.push(currCue);
2322
+ } else if (currCue !== this.transcriptActiveCue) {
2323
+ this.afterCaptions.push(currCue);
2324
+ }
2325
+ }
2326
+ }
2327
+ }
2328
+
2329
+ customElements.define('d2l-labs-media-player', MediaPlayer);