@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.
package/package.json CHANGED
@@ -14,6 +14,7 @@
14
14
  "./components/community-link.js": "./src/components/community/community-link.js",
15
15
  "./components/file-uploader.js": "./src/components/file-uploader/file-uploader.js",
16
16
  "./components/community-url-factory.js": "./src/components/community/community-url-factory.js",
17
+ "./components/media-player.js": "./src/components/media-player/media-player.js",
17
18
  "./components/navigation/navigation-band.js": "./src/components/navigation/navigation-band.js",
18
19
  "./components/navigation/navigation-button-icon.js": "./src/components/navigation/navigation-button-icon.js",
19
20
  "./components/navigation/navigation-dropdown-button-custom.js": "./src/components/navigation/navigation-dropdown-button-custom.js",
@@ -73,7 +74,7 @@
73
74
  "eslint": "^9",
74
75
  "eslint-config-brightspace": "^2",
75
76
  "messageformat-validator": "^2",
76
- "sinon": "^19",
77
+ "sinon": "^20",
77
78
  "stylelint": "^16"
78
79
  },
79
80
  "files": [
@@ -88,9 +89,14 @@
88
89
  "@brightspace-ui/core": "^3",
89
90
  "@brightspace-ui/intl": "^3",
90
91
  "@lit/context": "^1.1.3",
92
+ "fuse.js": "^6.4.6",
91
93
  "lit": "^3",
94
+ "lodash-es": "^4.17.21",
92
95
  "mobx": "^5",
93
- "page": "^1"
96
+ "page": "^1",
97
+ "parse-srt": "^1.0.0-alpha",
98
+ "resize-observer-polyfill": "^1",
99
+ "webvtt-parser": "^2.1.2"
94
100
  },
95
- "version": "2.19.1"
101
+ "version": "2.20.1"
96
102
  }
@@ -0,0 +1,216 @@
1
+ # media-player
2
+
3
+ A Lit element based media player component, designed for similarity across browsers. Capable of playing video and audio contents.
4
+
5
+
6
+ ## d2l-labs-media-player
7
+
8
+ ```html
9
+ <script type="module">
10
+ import '@brightspace-ui/labs/components/media-player.js';
11
+ </script>
12
+ <d2l-labs-media-player src="/video.webm"></d2l-labs-media-player>
13
+ ```
14
+
15
+ **Attributes:**
16
+
17
+ | Attribute | Type | Default | Description |
18
+ |--|--|--|--|
19
+ | allow-download| Boolean | false | If set, will allow the media to be downloaded.
20
+ | autoplay | Boolean | false | If set, will play the media as soon as it has been loaded. |
21
+ | crossorigin | String | null | If set, will set the `crossorigin` attribute on the underlying media element to the set value.
22
+ | download-filename | String | null | If set along with `allow-download`, will use the provided value as the base of the filename (the extension will be automatically appended)
23
+ | duration-hint | Number | 1 | Measured in seconds. If set and the duration cannot be determined automatically, this value will be used instead.
24
+ | hide-captions-selection | Boolean | false | If set, the menu item to configure captions is hidden. |
25
+ | hide-seek-bar | Boolean | false | If set, the seek bar will not be shown. |
26
+ | loop | Boolean | false | If set, once the media has finished playing it will replay from the beginning. |
27
+ | media-type | ["video", "audio"] | null | Whether the video or audio player should be rendered. If not set, a loading indicator will be displayed until set.
28
+ | metadata | JSON | false | Metadata JSON of the video, contains chapters and cuts data. |
29
+ | poster | String | null | URL of the image to display in place of the media before it has loaded. |
30
+ | src | String | | URL of the media to play. If multiple sources are desired, use `<source>` tags instead (see below). |
31
+ | thumbnails | String | | If set, will show thumbnails on preview. See below for required format. |
32
+ | play-in-view | Boolean | false | If set, will stop the media playback if not in view
33
+
34
+ ```
35
+ <!-- Render a media player with a source file and loop the media when it reaches the end -->
36
+
37
+ <d2l-labs-media-player loop src="./local-video.mp4"></d2l-labs-media-player>
38
+ ```
39
+
40
+ **Properties:**
41
+
42
+ | Property | Type | Get/Set | Description |
43
+ |--|--|--|--|
44
+ | currentTime | Number | Get & Set | Current time playback time of the media in seconds. |
45
+ | activeCue | Object | Get | VTTCue instance for the currently-displayed captions cue. If no cue is currently displayed, the value is null. |
46
+ | duration | Number | Get | Total duration of the media in seconds. |
47
+ | ended | Boolean | Get | Whether or not the video has ended. |
48
+ | paused | Boolean | Get | Whether or not the video is currently paused. |
49
+ | sourceType | ["audio", "video", "unknown"] | Get | The source type of the media.
50
+ | textTracks | [TextTrack] | Get | The TextTracks, for handling WebVTT. (See [MDN link](https://developer.mozilla.org/en-US/docs/Web/API/TextTrack))
51
+
52
+ ```
53
+ // Programatically determine the current playback time of the media player
54
+
55
+ console.log(`Current playback time of the media player = ${this.document.querySelector('d2l-labs-media-player').currentTime} sec`);
56
+ ```
57
+
58
+ **Methods:**
59
+
60
+ | Method | Type | Description |
61
+ |--|--|--|
62
+ | exitFullscreen() | void | Requests to exit fullscreen mode from the browser. Ignored if it is not playing video content, or if the video is not in fullscreen mode.
63
+ | play() | void | Begins playing the media. Ignored if the media is already playing.
64
+ | pause() | void | Pauses the media. Ignored if the media is already paused.
65
+ | requestFullscreen() | void | Requests fullscreen mode from the browser. Ignored if it is not playing video content, or the video is already in fullscreen mode.
66
+
67
+ ```
68
+ // Programatically pause the media player
69
+
70
+ this.document.querySelector('d2l-labs-media-player').pause();
71
+ ```
72
+
73
+ **Events:**
74
+
75
+ | Event | Description |
76
+ |--|--|
77
+ | cuechange | Dispatched when the currently-displayed captions cue changes. |
78
+ | durationchange | Dispatched when the video or media displayed has changed its duration |
79
+ | ended | Dispatched when the media has reached the end of its duration. |
80
+ | error | Dispatched when the media failed to load. |
81
+ | loadeddata | Dispatched when the media at the current playback position has finished loading. Often the first frame. |
82
+ | loadedmetadata | Dispatched when the metadata for the media has finished loading. |
83
+ | play | Dispatched when the media begins playing. |
84
+ | pause | Dispatched when the media is paused. |
85
+ | seeked | Dispatched when the user finishes navigating through the media's timeline using the seek bar. |
86
+ | seeking | Dispatched when the user starts navigating through the media's timeline using the seek bar. |
87
+ | timeupdate | Dispatched when the currentTime of the media player has been updated. |
88
+ | trackloaded | Dispatched when a track element has loaded. |
89
+ | trackloadfailed | Dispatched when a track element could not be loaded from the provided src attribute. |
90
+ | tracksmenuitemchanged | Dispatched when the tracks menu item has changed. |
91
+ ```
92
+ // Listen for the loadeddata event
93
+
94
+ this.document.querySelector('d2l-labs-media-player').addEventListener('loadeddata', () => {
95
+ console.log('loadeddata event has been dispatched');
96
+ });
97
+ ```
98
+
99
+ ## Multiple qualities using `<source>`
100
+ The media player supports switching to different qualities. If multiple `<source>` tags are present, a quality selector will be available in the menu. In this case, do not set `src` on `d2l-labs-media-player`.
101
+
102
+ ```
103
+ <d2l-labs-media-player>
104
+ <source src="sample-video-144p.mp4" label="SD">
105
+ <source src="sample-video.mp4" label="HD" default>
106
+ </d2l-labs-media-player
107
+ ```
108
+
109
+ **Attributes**
110
+
111
+ | Attribute | Type | Default | Description |
112
+ |--|--|--|--|
113
+ | label | String, required | | The label for the track, displayed to the user for selection.
114
+ | src | String, required | | The URL of the source file.
115
+ | default | Boolean | false | The source to be selected by default. If no source has the `default` attribute, then the first `<source>` tag is selected by default. Only one default should be set.
116
+
117
+ ## Showing thumbnails preview with `thumbnails` attribute
118
+
119
+ Provide a url to the thumbnails sprite image. This sprite is a grid of images taken from the video, at a set interval.
120
+
121
+ The thumbnail file name must use the following format:
122
+ `th<height>w<width>i<interval>-<hash>.[png|jpg]`
123
+
124
+ - `width` and `height` are the width/height px of each individual thumbnail
125
+ - `interval` indicates how many seconds apart each thumbnail is
126
+
127
+ For example, a sprite image named `th90w160i5-<hash>png` has the thumbnails 5 seconds apart, with width 160px and height 90px.
128
+
129
+ | Attribute | Required | Default | Description |
130
+ |--|--|--|--|
131
+ | hash | optional | |
132
+ | height | optional | 90px | Height px of each individual thumbnail in the sprite.
133
+ | interval | required | | Interval in seconds between each thumbnail.
134
+ | width | optional | 160px | Width px of each individual thumbnail in the sprite.
135
+
136
+ ## Chapters with `metadata` attribute
137
+
138
+ Provide metadata JSON e.g. [getMetadata endpoint](http://d2l-content-service-docs.s3-website-us-east-1.amazonaws.com/#operation/getMetadata)
139
+
140
+ Example format:
141
+ ```
142
+ {
143
+ "cuts": [
144
+ "in": 20,
145
+ "out": 30
146
+ ],
147
+ "chapters": [
148
+ {
149
+ "time": 0,
150
+ "title": {
151
+ "en": "Chapter One",
152
+ "fr": "Chapitre Un"
153
+ }
154
+ },
155
+ {
156
+ "time": 30,
157
+ "title": {
158
+ "en": "Chapter Two",
159
+ "fr": "Chapitre Deux"
160
+ }
161
+ },
162
+ {
163
+ "time": 70,
164
+ "title": {
165
+ "en": "Chapter Three",
166
+ "fr": "Chapitre Trois"
167
+ }
168
+ }
169
+ ]
170
+ }
171
+ ```
172
+
173
+ ## Captions and Subtitles Using `<track>`
174
+
175
+ The media player supports captions and subtitles, provided as `.srt` or `.vtt` files. If any valid tracks are present, a captions menu will be presented in the settings menu with an item for each track.
176
+
177
+ ```html
178
+ <script type="module">
179
+ import '@brightspace-ui/labs/components/media-player.js';
180
+ </script>
181
+ <d2l-labs-media-player src="/video.webm">
182
+ <track src="/english-captions.srt" srclang="en" label="English" kind="captions">
183
+ <track src="/french-captions.vtt" srclang="fr" label="French" kind="captions">
184
+ </d2l-labs-media-player>
185
+ ```
186
+
187
+ **Attributes**
188
+
189
+ | Attribute | Type | Default | Description |
190
+ |--|--|--|--|
191
+ | kind | ["captions", "subtitles"], required | | The kind of track.
192
+ | label | String, required | | The label for the track, displayed to the user for selection.
193
+ | src | String, required | | The URL of the source file.
194
+ | srclang | String, required | | The language's code.
195
+ | default | Boolean | false | The track to be selected by default. If D2L.MediaPlayer.Preferences.Track is defined in local storage, then it will take precedence over this attribute.
196
+ | default-ignore-preferences | Boolean | false | Same as default, but if D2L.MediaPlayer.Preferences.Track is defined, it will be ignored and this track will be selected instead.
197
+
198
+ ## Local Storage
199
+
200
+ The media player uses local storage to persist the user's playback speed, track selections, and volume.
201
+
202
+ **Items**
203
+
204
+ | Key | Description |
205
+ | -- | -- |
206
+ | D2L.MediaPlayer.Preferences.Speed | Playback speed that was last selected.
207
+ | D2L.MediaPlayer.Preferences.Track | Identifier for the kind and language of the track that was last selected.
208
+ | D2L.MediaPlayer.Preferences.Volume | Volume that was last selected.
209
+
210
+ ## Accessibility
211
+
212
+ The following features are implemented to improve accessibility:
213
+
214
+ - all controls can be accessed using the mouse or keyboard
215
+ - captions can be provided to the media player
216
+ - important events, such as a media source failing to load, are displayed visually and announced by screen readers
@@ -0,0 +1,149 @@
1
+ class FullscreenApi {
2
+ static getNamesMap() {
3
+ const names = [
4
+ [
5
+ 'requestFullscreen',
6
+ 'exitFullscreen',
7
+ 'fullscreenElement',
8
+ 'fullscreenEnabled',
9
+ 'fullscreenchange',
10
+ 'fullscreenerror',
11
+ ],
12
+ // New WebKit
13
+ [
14
+ 'webkitRequestFullscreen',
15
+ 'webkitExitFullscreen',
16
+ 'webkitFullscreenElement',
17
+ 'webkitFullscreenEnabled',
18
+ 'webkitfullscreenchange',
19
+ 'webkitfullscreenerror',
20
+ ],
21
+ // Old WebKit
22
+ [
23
+ 'webkitRequestFullScreen',
24
+ 'webkitCancelFullScreen',
25
+ 'webkitCurrentFullScreenElement',
26
+ 'webkitCancelFullScreen',
27
+ 'webkitfullscreenchange',
28
+ 'webkitfullscreenerror',
29
+ ],
30
+ [
31
+ 'mozRequestFullScreen',
32
+ 'mozCancelFullScreen',
33
+ 'mozFullScreenElement',
34
+ 'mozFullScreenEnabled',
35
+ 'mozfullscreenchange',
36
+ 'mozfullscreenerror',
37
+ ],
38
+ [
39
+ 'msRequestFullscreen',
40
+ 'msExitFullscreen',
41
+ 'msFullscreenElement',
42
+ 'msFullscreenEnabled',
43
+ 'MSFullscreenChange',
44
+ 'MSFullscreenError',
45
+ ]
46
+ ];
47
+
48
+ const map = {};
49
+ for (let i = 0; i < names.length; i++) {
50
+ const val = names[i];
51
+ if (val && val[1] in document) {
52
+ for (i = 0; i < val.length; i++) {
53
+ map[names[0][i]] = val[i];
54
+ }
55
+ return map;
56
+ }
57
+ }
58
+
59
+ return false;
60
+ }
61
+
62
+ constructor() {
63
+ this.namesMap = FullscreenApi.getNamesMap();
64
+ this.eventNamesMap = {
65
+ change: this.namesMap.fullscreenchange,
66
+ error: this.namesMap.fullscreenerror
67
+ };
68
+ }
69
+
70
+ get element() {
71
+ return document[this.namesMap.fullscreenElement];
72
+ }
73
+
74
+ get isEnabled() {
75
+ return Boolean(document[this.namesMap.fullscreenEnabled]);
76
+ }
77
+
78
+ get isFullscreen() {
79
+ return Boolean(document[this.namesMap.fullscreenElement]);
80
+ }
81
+
82
+ exit() {
83
+ return new Promise((resolve, reject) => {
84
+ if (!this.isFullscreen) {
85
+ resolve();
86
+ return;
87
+ }
88
+
89
+ const onFullScreenExit = () => {
90
+ this.off('change', onFullScreenExit);
91
+ resolve();
92
+ };
93
+
94
+ this.on('change', onFullScreenExit);
95
+
96
+ const returnPromise = document[this.namesMap.exitFullscreen]();
97
+ if (returnPromise instanceof Promise) {
98
+ returnPromise.then(onFullScreenExit).catch(reject);
99
+ }
100
+ });
101
+ }
102
+
103
+ off(event, callback) {
104
+ const eventName = this.eventNamesMap[event];
105
+ if (eventName) {
106
+ document.removeEventListener(eventName, callback, false);
107
+ }
108
+ }
109
+
110
+ on(event, callback) {
111
+ const eventName = this.eventNamesMap[event];
112
+ if (eventName) {
113
+ document.addEventListener(eventName, callback, false);
114
+ }
115
+ }
116
+
117
+ onchange(callback) {
118
+ this.on('change', callback);
119
+ }
120
+
121
+ onerror(callback) {
122
+ this.on('error', callback);
123
+ }
124
+
125
+ request(element) {
126
+ return new Promise((resolve, reject) => {
127
+ const onFullScreenEntered = () => {
128
+ this.off('change', onFullScreenEntered);
129
+ resolve();
130
+ };
131
+
132
+ this.on('change', onFullScreenEntered);
133
+
134
+ element = element || document.documentElement;
135
+
136
+ const returnPromise = element[this.namesMap.requestFullscreen]();
137
+
138
+ if (returnPromise instanceof Promise) {
139
+ returnPromise.then(onFullScreenEntered).catch(reject);
140
+ }
141
+ });
142
+ }
143
+
144
+ toggle(element) {
145
+ return this.isFullscreen ? this.exit() : this.request(element);
146
+ }
147
+ }
148
+
149
+ export default new FullscreenApi();
@@ -0,0 +1,274 @@
1
+ import { css, html, LitElement } from 'lit';
2
+ import ResizeObserver from 'resize-observer-polyfill';
3
+ import { styleMap } from 'lit/directives/style-map.js';
4
+
5
+ const AUDIO_BARS_GRADIENTS = [
6
+ {
7
+ from: '29A6FF',
8
+ weight: 9
9
+ },
10
+ {
11
+ from: '00D2ED',
12
+ weight: 9
13
+ },
14
+ {
15
+ from: '2DE2C0',
16
+ weight: 2
17
+ },
18
+ {
19
+ start: true,
20
+ from: '29A6FF',
21
+ weight: 76
22
+ }
23
+ ];
24
+ const { AUDIO_BARS_GRADIENTS_OFFSET, AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT } = AUDIO_BARS_GRADIENTS.reduce((r, gradient) => {
25
+ if (gradient.start || r.AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT > 0) {
26
+ r.AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT += gradient.weight;
27
+ } else {
28
+ r.AUDIO_BARS_GRADIENTS_OFFSET += gradient.weight;
29
+ }
30
+
31
+ return r;
32
+ }, { AUDIO_BARS_GRADIENTS_OFFSET: 0, AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT: 0 });
33
+
34
+ AUDIO_BARS_GRADIENTS.forEach((gradient, i) => {
35
+ gradient.fractionPassedNonInclusive = 0;
36
+ gradient.fractionPassedInclusive = gradient.weight / (AUDIO_BARS_GRADIENTS_OFFSET + AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT);
37
+
38
+ if (i > 0) {
39
+ gradient.fractionPassedNonInclusive = AUDIO_BARS_GRADIENTS[i - 1].fractionPassedInclusive;
40
+ gradient.fractionPassedInclusive += gradient.fractionPassedNonInclusive;
41
+ }
42
+
43
+ if (i < AUDIO_BARS_GRADIENTS.length - 1) {
44
+ gradient.to = AUDIO_BARS_GRADIENTS[i + 1].from;
45
+ } else {
46
+ gradient.to = AUDIO_BARS_GRADIENTS[0].from;
47
+ }
48
+ });
49
+ const AUDIO_BAR_HEIGHTS = [35, 54, 65, 86, 81, 67, 100, 90, 100, 98, 75, 99, 98, 96, 96, 95, 65, 50, 50, 60, 64, 54, 50, 44, 45, 46, 47, 45, 53, 67, 68, 76, 65, 63, 67, 91, 97, 86, 88, 86, 84, 84, 82, 0, 0, 0, 0, 0, 0, 0, 0, 69, 70, 68, 67, 72, 76, 86, 81, 83, 67, 65, 67, 21, 44, 45, 97, 86, 85, 81, 84, 70, 65, 67, 67, 72, 79, 76, 76, 63, 44, 42, 50, 54, 49, 33, 42, 44, 33, 38, 38, 36, 37, 35, 34];
50
+ const AUDIO_BAR_HORIZONTAL_MARGIN_REM = 0.05;
51
+ const AUDIO_BAR_WIDTH_REM = 0.25;
52
+ const FULL_BYTE = 255;
53
+ const GAMMA = 0.43;
54
+ const GAMMA_ADJUSTMENT_EXPONENT = 2.4;
55
+ const LINEAR_OFFSET = 0.055;
56
+ const UNDER_LINEAR_THRESHOLD_FACTOR = 12.92;
57
+ const UPDATE_PERIOD_MS = 50;
58
+
59
+ const FONT_SIZE = getComputedStyle(document.documentElement).fontSize;
60
+ const PX_PER_REM = FONT_SIZE.substr(0, FONT_SIZE.indexOf('px'));
61
+ const AUDIO_BAR_WIDTH_PX = AUDIO_BAR_WIDTH_REM * PX_PER_REM;
62
+ const AUDIO_BAR_HORIZONTAL_MARGIN_PX = AUDIO_BAR_HORIZONTAL_MARGIN_REM * PX_PER_REM;
63
+
64
+ class MediaPlayerAudioBars extends LitElement {
65
+ static get properties() {
66
+ return {
67
+ playing: { type: Boolean },
68
+ _visibleAudioBars: { type: Array, attribute: false },
69
+ };
70
+ }
71
+
72
+ static get styles() {
73
+ return css`
74
+ :host {
75
+ width: 100%;
76
+ }
77
+
78
+ #d2l-labs-media-player-audio-bars-row {
79
+ align-items: center;
80
+ display: flex;
81
+ flex-direction: row;
82
+ height: 100%;
83
+ justify-content: center;
84
+ }
85
+
86
+ #d2l-labs-media-player-audio-bar-container {
87
+ display: flex;
88
+ flex-direction: column;
89
+ height: 100%;
90
+ }
91
+
92
+ .d2l-labs-media-player-audio-bar {
93
+ border-top-left-radius: 0.075rem;
94
+ border-top-right-radius: 0.075rem;
95
+ margin: 0 ${AUDIO_BAR_HORIZONTAL_MARGIN_REM}rem;
96
+ width: ${AUDIO_BAR_WIDTH_REM}rem;
97
+ }
98
+ `;
99
+ }
100
+
101
+ constructor() {
102
+ super();
103
+
104
+ this._audioBarColours = [];
105
+ this._changingAudioBarsInterval = null;
106
+ this._gradientAudioBars = [];
107
+ this._numVisibleAudioBars = 0;
108
+ this._visibleAudioBars = [];
109
+ }
110
+
111
+ firstUpdated(changedProperties) {
112
+ super.firstUpdated(changedProperties);
113
+
114
+ new ResizeObserver((entries) => {
115
+ for (const entry of entries) {
116
+ const { width } = entry.contentRect;
117
+
118
+ const numAudioBarsThatCanFit = Math.floor(width / (AUDIO_BAR_WIDTH_PX + 2 * AUDIO_BAR_HORIZONTAL_MARGIN_PX));
119
+ const numAudioBarsThatCanFitNextOdd = numAudioBarsThatCanFit % 2 === 1 ? numAudioBarsThatCanFit : numAudioBarsThatCanFit - 1;
120
+ this._numVisibleAudioBars = Math.min(numAudioBarsThatCanFitNextOdd, AUDIO_BAR_HEIGHTS.length);
121
+ const numAudioBarColours = Math.floor(this._numVisibleAudioBars * (1 + (AUDIO_BARS_GRADIENTS_OFFSET / AUDIO_BARS_GRADIENTS_DISPLAYED_WEIGHT)));
122
+
123
+ this._audioBarColours = [];
124
+ this._visibleAudioBars = [];
125
+ for (let i = 0; i < this._numVisibleAudioBars; i++) {
126
+
127
+ let height;
128
+
129
+ if (this._numVisibleAudioBars < AUDIO_BAR_HEIGHTS.length) {
130
+ height = AUDIO_BAR_HEIGHTS[i + Math.floor((AUDIO_BAR_HEIGHTS.length - this._numVisibleAudioBars) / 2)];
131
+ } else {
132
+ height = AUDIO_BAR_HEIGHTS[i];
133
+ }
134
+
135
+ this._visibleAudioBars.push({
136
+ height
137
+ });
138
+ }
139
+
140
+ for (let i = 0; i < numAudioBarColours; i++) {
141
+ this._audioBarColours.push(0);
142
+ }
143
+
144
+ for (let i = 0; i < numAudioBarColours; i++) {
145
+ this._audioBarColours[i] = this._getRgbOfAudioBar(i);
146
+ }
147
+
148
+ this._startChangingAudioBars();
149
+ }
150
+ }).observe(this.shadowRoot.getElementById('d2l-labs-media-player-audio-bars-row'));
151
+ }
152
+
153
+ render() {
154
+ return html`
155
+ <div id="d2l-labs-media-player-audio-bars-row">
156
+ ${this._visibleAudioBars.map(audioBar => html`
157
+ <div id="d2l-labs-media-player-audio-bar-container">
158
+ <div style="flex: auto;"></div>
159
+ <div class="d2l-labs-media-player-audio-bar" style=${styleMap({ backgroundColor: `rgba(${audioBar.red}, ${audioBar.green}, ${audioBar.blue}, 1)`, height: `${audioBar.height}%` })}></div>
160
+ </div>
161
+ `)}
162
+ </div>
163
+ `;
164
+ }
165
+
166
+ _changeColoursOfAudioBars() {
167
+ const newVisibleAudioBars = [];
168
+
169
+ this._visibleAudioBars.forEach((visibleAudioBar, i) => {
170
+ const audioBarI = (i + this._visibleAudioBarsOffset) % this._audioBarColours.length;
171
+
172
+ const colour = this._audioBarColours[audioBarI];
173
+
174
+ newVisibleAudioBars.push({
175
+ ...visibleAudioBar,
176
+ ...colour
177
+ });
178
+ });
179
+
180
+ this._visibleAudioBars = newVisibleAudioBars;
181
+
182
+ this._visibleAudioBarsOffset = this._visibleAudioBarsOffset === 0 ? this._audioBarColours.length - 1 : this._visibleAudioBarsOffset - 1;
183
+ }
184
+
185
+ /**
186
+ * Converts a [0..255] SRGB value to a [0..1] linear value.
187
+ * @param {Number} rgb SRGB representation of the colour.
188
+ * @returns {Number} Linear value of the colour.
189
+ */
190
+ static _fromSRGB(rgb) {
191
+ rgb /= FULL_BYTE;
192
+
193
+ return rgb <= 0.04045 ? rgb / UNDER_LINEAR_THRESHOLD_FACTOR : Math.pow((rgb + LINEAR_OFFSET) / (1 + LINEAR_OFFSET), GAMMA_ADJUSTMENT_EXPONENT);
194
+ }
195
+
196
+ static _getGradientFromFraction(fraction) {
197
+ for (let i = 0; i < AUDIO_BARS_GRADIENTS.length; i++) {
198
+ if (fraction < AUDIO_BARS_GRADIENTS[i].fractionPassedInclusive) return AUDIO_BARS_GRADIENTS[i];
199
+ }
200
+ }
201
+
202
+ _getRgbOfAudioBar(i) {
203
+ const fraction = i / this._audioBarColours.length;
204
+
205
+ const { from, to, fractionPassedNonInclusive, fractionPassedInclusive } = MediaPlayerAudioBars._getGradientFromFraction(fraction);
206
+
207
+ const innerFraction = (fraction - fractionPassedNonInclusive) / (fractionPassedInclusive - fractionPassedNonInclusive);
208
+
209
+ const fromRedSRGB = parseInt(from.substr(0, 2), 16);
210
+ const fromRedLinear = MediaPlayerAudioBars._fromSRGB(fromRedSRGB);
211
+ const fromGreenSRGB = parseInt(from.substr(2, 2), 16);
212
+ const fromGreenLinear = MediaPlayerAudioBars._fromSRGB(fromGreenSRGB);
213
+ const fromBlueSRGB = parseInt(from.substr(4, 2), 16);
214
+ const fromBlueLinear = MediaPlayerAudioBars._fromSRGB(fromBlueSRGB);
215
+ const fromBrightness = Math.pow(fromRedLinear + fromGreenLinear + fromBlueLinear, GAMMA);
216
+
217
+ const toRedSRGB = parseInt(to.substr(0, 2), 16);
218
+ const toRedLinear = MediaPlayerAudioBars._fromSRGB(toRedSRGB);
219
+ const toGreenSRGB = parseInt(to.substr(2, 2), 16);
220
+ const toGreenLinear = MediaPlayerAudioBars._fromSRGB(toGreenSRGB);
221
+ const toBlueSRGB = parseInt(to.substr(4, 2), 16);
222
+ const toBlueLinear = MediaPlayerAudioBars._fromSRGB(toBlueSRGB);
223
+ const toBrightness = Math.pow(toRedLinear + toGreenLinear + toBlueLinear, GAMMA);
224
+
225
+ const brightness = Math.pow(MediaPlayerAudioBars._weightedAverage(fromBrightness, toBrightness, innerFraction), 1 / GAMMA);
226
+
227
+ const redWithoutBrightness = MediaPlayerAudioBars._weightedAverage(fromRedLinear, toRedLinear, innerFraction);
228
+ const greenWithoutBrightness = MediaPlayerAudioBars._weightedAverage(fromGreenLinear, toGreenLinear, innerFraction);
229
+ const blueWithoutBrightness = MediaPlayerAudioBars._weightedAverage(fromBlueLinear, toBlueLinear, innerFraction);
230
+
231
+ const sumWithoutBrightness = redWithoutBrightness + greenWithoutBrightness + blueWithoutBrightness;
232
+
233
+ return {
234
+ red: MediaPlayerAudioBars._toSRGB(redWithoutBrightness * brightness / sumWithoutBrightness),
235
+ green: MediaPlayerAudioBars._toSRGB(greenWithoutBrightness * brightness / sumWithoutBrightness),
236
+ blue: MediaPlayerAudioBars._toSRGB(blueWithoutBrightness * brightness / sumWithoutBrightness)
237
+ };
238
+ }
239
+
240
+ _startChangingAudioBars() {
241
+ this._visibleAudioBarsOffset = this._audioBarColours.length - this._visibleAudioBars.length + 1;
242
+
243
+ this._changeColoursOfAudioBars();
244
+
245
+ clearInterval(this._changingAudioBarsInterval);
246
+
247
+ this._changingAudioBarsInterval = setInterval(() => {
248
+ if (!this.playing) return;
249
+
250
+ this._changeColoursOfAudioBars();
251
+ }, UPDATE_PERIOD_MS);
252
+ }
253
+
254
+ /**
255
+ * Converts a [0..1] linear value to a [0..255] SRGB value.
256
+ * @param {Number} rgb Linear representation of the colour.
257
+ * @returns {Number} SRGB value of the colour.
258
+ */
259
+ static _toSRGB(rgb) {
260
+ if (rgb <= 0.0031308) {
261
+ rgb *= UNDER_LINEAR_THRESHOLD_FACTOR;
262
+ } else {
263
+ rgb = ((1 + LINEAR_OFFSET) * (Math.pow(rgb, 1 / GAMMA_ADJUSTMENT_EXPONENT))) - LINEAR_OFFSET;
264
+ }
265
+
266
+ return (FULL_BYTE + 1) * rgb;
267
+ }
268
+
269
+ static _weightedAverage(a, b, weightOfB) {
270
+ return a + (b - a) * weightOfB;
271
+ }
272
+ }
273
+
274
+ customElements.define('d2l-labs-media-player-audio-bars', MediaPlayerAudioBars);