@gcorevideo/player 2.22.31 → 2.23.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/assets/thumbnails/scrub-thumbnails.ejs +5 -10
- package/assets/thumbnails/style.scss +4 -5
- package/dist/core.js +1 -1
- package/dist/index.css +1178 -1176
- package/dist/index.js +206 -281
- package/lib/plugins/clips/Clips.d.ts +7 -0
- package/lib/plugins/clips/Clips.d.ts.map +1 -1
- package/lib/plugins/clips/Clips.js +8 -0
- package/lib/plugins/media-control/MediaControl.d.ts +1 -7
- package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
- package/lib/plugins/media-control/MediaControl.js +9 -18
- package/lib/plugins/thumbnails/Thumbnails.d.ts +36 -33
- package/lib/plugins/thumbnails/Thumbnails.d.ts.map +1 -1
- package/lib/plugins/thumbnails/Thumbnails.js +174 -260
- package/lib/plugins/thumbnails/utils.d.ts +5 -0
- package/lib/plugins/thumbnails/utils.d.ts.map +1 -0
- package/lib/plugins/thumbnails/utils.js +12 -0
- package/lib/testUtils.js +2 -2
- package/package.json +5 -1
- package/src/plugins/clips/Clips.ts +10 -1
- package/src/plugins/media-control/MediaControl.ts +10 -21
- package/src/plugins/thumbnails/Thumbnails.ts +236 -331
- package/src/plugins/thumbnails/__tests__/Thumbnails.test.ts +72 -0
- package/src/plugins/thumbnails/__tests__/__snapshots__/Thumbnails.test.ts.snap +10 -0
- package/src/plugins/thumbnails/utils.ts +12 -0
- package/src/testUtils.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { UICorePlugin, Events, template,
|
|
2
|
-
import {
|
|
1
|
+
import { UICorePlugin, Events, template, $, } from '@clappr/core';
|
|
2
|
+
import { trace } from '@gcorevideo/utils';
|
|
3
3
|
import parseSRT from 'parse-srt';
|
|
4
|
+
import assert from 'assert';
|
|
4
5
|
import { CLAPPR_VERSION } from '../../build.js';
|
|
5
6
|
import pluginHtml from '../../../assets/thumbnails/scrub-thumbnails.ejs';
|
|
6
7
|
import '../../../assets/thumbnails/style.scss';
|
|
7
|
-
import {
|
|
8
|
+
import { loadImageDimensions } from './utils.js';
|
|
8
9
|
const T = 'plugins.thumbnails';
|
|
9
10
|
/**
|
|
10
11
|
* `PLUGIN` that displays the thumbnails of the video when available.
|
|
11
12
|
* @beta
|
|
13
|
+
* @remarks
|
|
14
|
+
* The plugin needs specially crafted VTT file with a thumbnail sprite sheet to work.
|
|
15
|
+
* The VTT consist of timestamp records followed by a thumbnail area
|
|
16
|
+
*
|
|
17
|
+
* Configuration options - {@link ThumbnailsPluginSettings}
|
|
18
|
+
*
|
|
12
19
|
* @example
|
|
13
20
|
* ```ts
|
|
14
21
|
* import { Thumbnails } from '@gcorevideo/player'
|
|
@@ -29,19 +36,15 @@ const T = 'plugins.thumbnails';
|
|
|
29
36
|
* ```
|
|
30
37
|
*/
|
|
31
38
|
export class Thumbnails extends UICorePlugin {
|
|
32
|
-
|
|
33
|
-
_$backdrop = null;
|
|
34
|
-
$container = null;
|
|
35
|
-
$img = null;
|
|
36
|
-
_$carousel = null;
|
|
37
|
-
$textThumbnail = null;
|
|
38
|
-
_$backdropCarouselImgs = [];
|
|
39
|
+
$backdropCarouselImgs = [];
|
|
39
40
|
spriteSheetHeight = 0;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
spriteSheetWidth = 0;
|
|
42
|
+
hoverPosition = 0;
|
|
43
|
+
showing = false;
|
|
44
|
+
thumbsLoaded = false;
|
|
45
|
+
spotlightHeight = 0;
|
|
46
|
+
backdropHeight = 0;
|
|
47
|
+
thumbs = [];
|
|
45
48
|
/**
|
|
46
49
|
* @internal
|
|
47
50
|
*/
|
|
@@ -59,10 +62,14 @@ export class Thumbnails extends UICorePlugin {
|
|
|
59
62
|
*/
|
|
60
63
|
get attributes() {
|
|
61
64
|
return {
|
|
62
|
-
class:
|
|
65
|
+
class: 'scrub-thumbnails',
|
|
63
66
|
};
|
|
64
67
|
}
|
|
65
68
|
static template = template(pluginHtml);
|
|
69
|
+
constructor(core) {
|
|
70
|
+
super(core);
|
|
71
|
+
this.backdropHeight = this.options.thumbnails?.backdropHeight ?? 0;
|
|
72
|
+
}
|
|
66
73
|
/*
|
|
67
74
|
* Helper to build the "thumbs" property for a sprite sheet.
|
|
68
75
|
*
|
|
@@ -74,25 +81,21 @@ export class Thumbnails extends UICorePlugin {
|
|
|
74
81
|
* timeInterval- The interval (in seconds) between the thumbnails.
|
|
75
82
|
* startTime- The time (in seconds) that the first thumbnail represents. (defaults to 0)
|
|
76
83
|
*/
|
|
77
|
-
|
|
78
|
-
buildSpriteConfig(vtt, spriteSheetUrl) {
|
|
84
|
+
buildSpriteConfig(vtt, baseUrl) {
|
|
79
85
|
const thumbs = [];
|
|
80
|
-
// let coor: string[] = [];
|
|
81
86
|
for (const vt of vtt) {
|
|
82
87
|
const el = vt.text;
|
|
83
|
-
// if (el && el.search(/\d*,\d*,\d*,\d*/g) > -1) {
|
|
84
|
-
// el = el.match(/\d*,\d*,\d*,\d*/g)[0];
|
|
85
|
-
// coor = el.split(',');
|
|
86
|
-
// }
|
|
87
88
|
if (el) {
|
|
88
|
-
const m = el.match(/xywh
|
|
89
|
+
const m = el.match(/(\w+)#xywh=(\d+,\d+,\d+,\d+)/);
|
|
89
90
|
if (m) {
|
|
90
|
-
const coor = m[
|
|
91
|
+
const coor = m[2].split(',');
|
|
91
92
|
const w = parseInt(coor[2], 10);
|
|
92
93
|
const h = parseInt(coor[3], 10);
|
|
93
94
|
if (w > 0 && h > 0) {
|
|
94
95
|
thumbs.push({
|
|
95
|
-
|
|
96
|
+
// TODO handle relative URLs
|
|
97
|
+
// url: new URL(m[0], baseUrl).toString(),
|
|
98
|
+
url: baseUrl,
|
|
96
99
|
time: vt.start,
|
|
97
100
|
w,
|
|
98
101
|
h,
|
|
@@ -105,242 +108,147 @@ export class Thumbnails extends UICorePlugin {
|
|
|
105
108
|
}
|
|
106
109
|
return thumbs;
|
|
107
110
|
}
|
|
108
|
-
// TODO check if seek enabled
|
|
109
111
|
/**
|
|
110
112
|
* @internal
|
|
111
113
|
*/
|
|
112
114
|
bindEvents() {
|
|
113
|
-
this.listenToOnce(this.core, Events.CORE_READY, this.
|
|
114
|
-
this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_MOUSEMOVE_SEEKBAR, this._onMouseMove);
|
|
115
|
-
this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_MOUSELEAVE_SEEKBAR, this._onMouseLeave);
|
|
116
|
-
this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_RENDERED, this._init);
|
|
117
|
-
this.listenTo(this.core.mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, this._onMediaControlContainerChanged);
|
|
115
|
+
this.listenToOnce(this.core, Events.CORE_READY, this.onCoreReady);
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.stopListening(this._oldContainer, Events.CONTAINER_TIMEUPDATE, this._renderPlugin);
|
|
122
|
-
}
|
|
123
|
-
this._oldContainer = this.core.mediaControl.container;
|
|
124
|
-
this.listenTo(this.core.mediaControl.container, Events.CONTAINER_TIMEUPDATE, this._renderPlugin);
|
|
117
|
+
bindContainerEvents(container) {
|
|
118
|
+
this.listenTo(container, Events.CONTAINER_TIMEUPDATE, this.update);
|
|
125
119
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
reportError(error);
|
|
120
|
+
onCoreReady() {
|
|
121
|
+
const mediaControl = this.core.getPlugin('media_control');
|
|
122
|
+
assert(mediaControl, `MediaControl is required for ${this.name} plugin to work`);
|
|
123
|
+
if (!this.options.thumbnails ||
|
|
124
|
+
!this.options.thumbnails.sprite ||
|
|
125
|
+
!this.options.thumbnails.vtt) {
|
|
126
|
+
trace(`${T} misconfigured: options.thumbnails.sprite and options.thumbnails.vtt are required`);
|
|
127
|
+
this.destroy();
|
|
137
128
|
return;
|
|
138
129
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
130
|
+
const { sprite: spriteSheet, vtt } = this.options.thumbnails;
|
|
131
|
+
this.thumbs = this.buildSpriteConfig(parseSRT(vtt), spriteSheet);
|
|
132
|
+
if (!this.thumbs.length) {
|
|
133
|
+
trace(`${T} failed to parse the sprite sheet`);
|
|
143
134
|
this.destroy();
|
|
144
135
|
return;
|
|
145
136
|
}
|
|
137
|
+
this.spotlightHeight = this.options.thumbnails?.spotlightHeight ?? 0;
|
|
146
138
|
this.loadSpriteSheet(spriteSheet).then(() => {
|
|
147
|
-
this.
|
|
148
|
-
this.
|
|
149
|
-
|
|
139
|
+
this.thumbsLoaded = true;
|
|
140
|
+
this.spotlightHeight = this.spotlightHeight
|
|
141
|
+
? Math.min(this.spotlightHeight, this.thumbs[0].h)
|
|
142
|
+
: this.thumbs[0].h;
|
|
143
|
+
this.init();
|
|
150
144
|
});
|
|
145
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_MOUSEMOVE_SEEKBAR, this.onMouseMoveSeekbar);
|
|
146
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_MOUSELEAVE_SEEKBAR, this.onMouseLeave);
|
|
147
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, this.init);
|
|
148
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, () => this.onContainerChanged(mediaControl.container));
|
|
151
149
|
}
|
|
152
150
|
async loadSpriteSheet(spriteSheetUrl) {
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.spriteSheetHeight = img.height;
|
|
157
|
-
resolve();
|
|
158
|
-
};
|
|
159
|
-
img.onerror = reject;
|
|
160
|
-
img.src = spriteSheetUrl;
|
|
151
|
+
return loadImageDimensions(spriteSheetUrl).then(({ height, width }) => {
|
|
152
|
+
this.spriteSheetHeight = height;
|
|
153
|
+
this.spriteSheetWidth = width;
|
|
161
154
|
});
|
|
162
155
|
}
|
|
163
|
-
|
|
164
|
-
this.
|
|
156
|
+
onContainerChanged(container) {
|
|
157
|
+
this.bindContainerEvents(container);
|
|
165
158
|
}
|
|
166
|
-
|
|
167
|
-
if (!this.
|
|
168
|
-
//
|
|
159
|
+
init() {
|
|
160
|
+
if (!this.thumbsLoaded) {
|
|
161
|
+
// init() will be called when the thumbs are loaded,
|
|
169
162
|
// and whenever the media control rendered event is fired as just before this the dom elements get wiped in IE (https://github.com/tjenkinson/clappr-thumbnails-plugin/issues/5)
|
|
170
163
|
return;
|
|
171
164
|
}
|
|
172
165
|
// Init the backdropCarousel as array to keep reference of thumbnail images
|
|
173
|
-
this
|
|
174
|
-
|
|
175
|
-
this.
|
|
176
|
-
this.
|
|
177
|
-
this._renderPlugin();
|
|
166
|
+
this.$backdropCarouselImgs = [];
|
|
167
|
+
this.fixElements();
|
|
168
|
+
this.loadBackdrop();
|
|
169
|
+
this.update();
|
|
178
170
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
return this.core.options.thumbnails;
|
|
184
|
-
}
|
|
185
|
-
_appendElToMediaControl() {
|
|
186
|
-
// insert after the background
|
|
187
|
-
this.core.mediaControl.$el.find('.seek-time').css('bottom', 56);
|
|
188
|
-
this.core.mediaControl.$el.first().after(this.el);
|
|
171
|
+
mount() {
|
|
172
|
+
const mediaControl = this.core.getPlugin('media_control');
|
|
173
|
+
mediaControl.$el.find('.seek-time').css('bottom', 56); // TODO check the offset
|
|
174
|
+
mediaControl.$el.first().after(this.$el);
|
|
189
175
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
this._calculateHoverPosition(e);
|
|
197
|
-
this._show = true;
|
|
198
|
-
this._renderPlugin();
|
|
199
|
-
}
|
|
200
|
-
_onMouseLeave() {
|
|
201
|
-
this._show = false;
|
|
202
|
-
this._renderPlugin();
|
|
176
|
+
onMouseMoveSeekbar(_, pos) {
|
|
177
|
+
if (Math.abs(pos - this.hoverPosition) >= 0.01) {
|
|
178
|
+
this.hoverPosition = pos;
|
|
179
|
+
this.showing = true;
|
|
180
|
+
this.update();
|
|
181
|
+
}
|
|
203
182
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this._hoverPosition = Math.min(1, Math.max(offset / this.core.mediaControl.$seekBarContainer.width(), 0));
|
|
183
|
+
onMouseLeave() {
|
|
184
|
+
this.showing = false;
|
|
185
|
+
this.update();
|
|
208
186
|
}
|
|
209
|
-
// private _buildThumbsFromOptions() {
|
|
210
|
-
// const thumbs = this._thumbs;
|
|
211
|
-
// const promises = thumbs.map((thumb) => {
|
|
212
|
-
// return this._addThumbFromSrc(thumb);
|
|
213
|
-
// });
|
|
214
|
-
// return Promise.all(promises);
|
|
215
|
-
// }
|
|
216
|
-
// private _addThumbFromSrc(thumbSrc) {
|
|
217
|
-
// return new Promise((resolve, reject) => {
|
|
218
|
-
// const img = new Image();
|
|
219
|
-
// img.onload = () => {
|
|
220
|
-
// resolve(img);
|
|
221
|
-
// };
|
|
222
|
-
// img.onerror = reject;
|
|
223
|
-
// img.src = thumbSrc.url;
|
|
224
|
-
// }).then((img) => {
|
|
225
|
-
// const startTime = thumbSrc.time;
|
|
226
|
-
// // determine the thumb index
|
|
227
|
-
// let index = null;
|
|
228
|
-
// this._thumbs.some((thumb, i) => {
|
|
229
|
-
// if (startTime < thumb.time) {
|
|
230
|
-
// index = i;
|
|
231
|
-
// return true;
|
|
232
|
-
// }
|
|
233
|
-
// return false;
|
|
234
|
-
// });
|
|
235
|
-
// if (index === null) {
|
|
236
|
-
// index = this._thumbs.length;
|
|
237
|
-
// }
|
|
238
|
-
// const next = index < this._thumbs.length ? this._thumbs[index] : null;
|
|
239
|
-
// const prev = index > 0 ? this._thumbs[index - 1] : null;
|
|
240
|
-
// if (prev) {
|
|
241
|
-
// // update the duration of the previous thumbnail
|
|
242
|
-
// prev.duration = startTime - prev.time;
|
|
243
|
-
// }
|
|
244
|
-
// // the duration this thumb lasts for
|
|
245
|
-
// // if it is the last thumb then duration will be null
|
|
246
|
-
// const duration = next ? next.time - thumbSrc.time : null;
|
|
247
|
-
// const imageW = img.width;
|
|
248
|
-
// const imageH = img.height;
|
|
249
|
-
// const thumb = {
|
|
250
|
-
// imageW: imageW, // actual width of image
|
|
251
|
-
// imageH: imageH, // actual height of image
|
|
252
|
-
// x: thumbSrc.x || 0, // x coord in image of sprite
|
|
253
|
-
// y: thumbSrc.y || 0, // y coord in image of sprite
|
|
254
|
-
// w: thumbSrc.w || imageW, // width of sprite
|
|
255
|
-
// h: thumbSrc.h || imageH, // height of sprite
|
|
256
|
-
// url: thumbSrc.url,
|
|
257
|
-
// time: startTime, // time this thumb represents
|
|
258
|
-
// duration: duration, // how long (from time) this thumb represents
|
|
259
|
-
// src: thumbSrc
|
|
260
|
-
// };
|
|
261
|
-
// this._thumbs.splice(index, 0, thumb);
|
|
262
|
-
// return thumb;
|
|
263
|
-
// });
|
|
264
|
-
// }
|
|
265
187
|
// builds a dom element which represents the thumbnail
|
|
266
|
-
// scaled to the
|
|
267
|
-
|
|
188
|
+
// scaled to the given height
|
|
189
|
+
buildThumbImage(thumb, height, $ref) {
|
|
268
190
|
const scaleFactor = height / thumb.h;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
this.$container.css('width', thumb.w * scaleFactor);
|
|
278
|
-
this.$container.css('height', height);
|
|
279
|
-
this.$img.css({
|
|
280
|
-
height: this.spriteSheetHeight * scaleFactor,
|
|
281
|
-
left: -1 * thumb.x * scaleFactor,
|
|
282
|
-
top: -1 * thumb.y * scaleFactor,
|
|
191
|
+
const $container = $ref && $ref.length ? $ref : $('<div />').addClass('thumbnail-container');
|
|
192
|
+
$container.css('width', thumb.w * scaleFactor);
|
|
193
|
+
$container.css('height', height);
|
|
194
|
+
$container.css({
|
|
195
|
+
backgroundImage: `url(${thumb.url})`,
|
|
196
|
+
backgroundSize: `${Math.floor(this.spriteSheetWidth * scaleFactor)}px ${Math.floor(this.spriteSheetHeight * scaleFactor)}px`,
|
|
197
|
+
backgroundPosition: `-${Math.floor(thumb.x * scaleFactor)}px -${Math.floor(thumb.y * scaleFactor)}px`,
|
|
283
198
|
});
|
|
284
|
-
|
|
285
|
-
this.$container.append(this.$img);
|
|
286
|
-
}
|
|
287
|
-
return this.$container;
|
|
199
|
+
return $container;
|
|
288
200
|
}
|
|
289
|
-
|
|
290
|
-
if (!this.
|
|
201
|
+
loadBackdrop() {
|
|
202
|
+
if (!this.backdropHeight) {
|
|
291
203
|
// disabled
|
|
292
204
|
return;
|
|
293
205
|
}
|
|
294
206
|
// append each of the thumbnails to the backdrop carousel
|
|
295
|
-
const $carousel = this.
|
|
296
|
-
for (const thumb of this.
|
|
297
|
-
const $img = this.
|
|
298
|
-
// Keep reference to thumbnail
|
|
299
|
-
this
|
|
207
|
+
const $carousel = this.$el.find('#thumbnails-carousel');
|
|
208
|
+
for (const thumb of this.thumbs) {
|
|
209
|
+
const $img = this.buildThumbImage(thumb, this.backdropHeight);
|
|
210
|
+
// Keep reference to the thumbnail
|
|
211
|
+
this.$backdropCarouselImgs.push($img);
|
|
300
212
|
// Add thumbnail to DOM
|
|
301
213
|
$carousel.append($img);
|
|
302
214
|
}
|
|
303
215
|
}
|
|
304
216
|
setText(time) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
217
|
+
const clips = this.core.getPlugin('clips');
|
|
218
|
+
if (clips) {
|
|
219
|
+
const txt = clips.getText(time);
|
|
220
|
+
this.$el.find('#thumbnails-text').text(txt ?? '');
|
|
308
221
|
}
|
|
309
222
|
}
|
|
310
223
|
// calculate how far along the carousel should currently be slid
|
|
311
224
|
// depending on where the user is hovering on the progress bar
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
backdropHeight: this._getOptions().backdropHeight,
|
|
315
|
-
});
|
|
316
|
-
if (!this._getOptions().backdropHeight) {
|
|
225
|
+
updateCarousel() {
|
|
226
|
+
if (!this.backdropHeight) {
|
|
317
227
|
// disabled
|
|
318
228
|
return;
|
|
319
229
|
}
|
|
320
|
-
const
|
|
321
|
-
const videoDuration =
|
|
322
|
-
const startTimeOffset =
|
|
230
|
+
const mediaControl = this.core.getPlugin('media_control');
|
|
231
|
+
const videoDuration = mediaControl.container.getDuration();
|
|
232
|
+
const startTimeOffset = mediaControl.container.getStartTimeOffset();
|
|
323
233
|
// the time into the video at the current hover position
|
|
324
|
-
const hoverTime = startTimeOffset + videoDuration * hoverPosition;
|
|
325
|
-
const
|
|
326
|
-
const
|
|
234
|
+
const hoverTime = startTimeOffset + videoDuration * this.hoverPosition;
|
|
235
|
+
const $backdrop = this.$el.find('#thumbnails-backdrop');
|
|
236
|
+
const backdropWidth = $backdrop.width();
|
|
237
|
+
const $carousel = this.$el.find('#thumbnails-carousel');
|
|
327
238
|
const carouselWidth = $carousel.width();
|
|
328
239
|
// slide the carousel so that the image on the carousel that is above where the person
|
|
329
240
|
// is hovering maps to that position in time.
|
|
330
241
|
// Thumbnails may not be distributed at even times along the video
|
|
331
|
-
const thumbs = this._thumbs;
|
|
332
242
|
// assuming that each thumbnail has the same width
|
|
333
|
-
const thumbWidth = carouselWidth / thumbs.length;
|
|
243
|
+
const thumbWidth = carouselWidth / this.thumbs.length;
|
|
334
244
|
// determine which thumbnail applies to the current time
|
|
335
|
-
const thumbIndex = this.
|
|
336
|
-
const thumb = thumbs[thumbIndex];
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
thumbDuration = Math.max(videoDuration + startTimeOffset - thumb.time, 0);
|
|
343
|
-
}
|
|
245
|
+
const thumbIndex = this.getThumbIndexForTime(hoverTime);
|
|
246
|
+
const thumb = this.thumbs[thumbIndex];
|
|
247
|
+
// the last thumbnail duration will be null as it can't be determined
|
|
248
|
+
// e.g the duration of the video may increase over time (live stream)
|
|
249
|
+
// so calculate the duration now so this last thumbnail lasts till the end
|
|
250
|
+
const thumbDuration = thumb.duration ??
|
|
251
|
+
Math.max(videoDuration + startTimeOffset - thumb.time, 0);
|
|
344
252
|
// determine how far accross that thumbnail we are
|
|
345
253
|
const timeIntoThumb = hoverTime - thumb.time;
|
|
346
254
|
const positionInThumb = timeIntoThumb / thumbDuration;
|
|
@@ -348,12 +256,12 @@ export class Thumbnails extends UICorePlugin {
|
|
|
348
256
|
// now calculate the position along carousel that we want to be above the hover position
|
|
349
257
|
const xCoordInCarousel = thumbIndex * thumbWidth + xCoordInThumb;
|
|
350
258
|
// and finally the position of the carousel when the hover position is taken in to consideration
|
|
351
|
-
const carouselXCoord = xCoordInCarousel - hoverPosition * backdropWidth;
|
|
352
|
-
$carousel.css('left', -carouselXCoord);
|
|
353
|
-
const maxOpacity = this.
|
|
354
|
-
const minOpacity = this.
|
|
259
|
+
const carouselXCoord = xCoordInCarousel - this.hoverPosition * backdropWidth;
|
|
260
|
+
$carousel.css('left', -carouselXCoord); // TODO +px
|
|
261
|
+
const maxOpacity = this.options.thumbnails.backdropMaxOpacity ?? 0.6;
|
|
262
|
+
const minOpacity = this.options.thumbnails.backdropMinOpacity ?? 0.08;
|
|
355
263
|
// now update the transparencies so that they fade in around the active one
|
|
356
|
-
for (let i = 0; i < thumbs.length; i++) {
|
|
264
|
+
for (let i = 0; i < this.thumbs.length; i++) {
|
|
357
265
|
const thumbXCoord = thumbWidth * i;
|
|
358
266
|
let distance = thumbXCoord - xCoordInCarousel;
|
|
359
267
|
if (distance < 0) {
|
|
@@ -365,47 +273,42 @@ export class Thumbnails extends UICorePlugin {
|
|
|
365
273
|
}
|
|
366
274
|
// fade over the width of 2 thumbnails
|
|
367
275
|
const opacity = Math.max(maxOpacity - Math.abs(distance) / (2 * thumbWidth), minOpacity);
|
|
368
|
-
this
|
|
276
|
+
this.$backdropCarouselImgs[i].css('opacity', opacity);
|
|
369
277
|
}
|
|
370
278
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
spotlightHeight: this._getOptions().spotlightHeight,
|
|
374
|
-
});
|
|
375
|
-
if (!this._getOptions().spotlightHeight) {
|
|
279
|
+
updateSpotlightThumb() {
|
|
280
|
+
if (!this.spotlightHeight) {
|
|
376
281
|
// disabled
|
|
377
282
|
return;
|
|
378
283
|
}
|
|
379
|
-
const
|
|
380
|
-
const videoDuration =
|
|
284
|
+
const mediaControl = this.core.getPlugin('media_control');
|
|
285
|
+
const videoDuration = mediaControl.container.getDuration();
|
|
381
286
|
// the time into the video at the current hover position
|
|
382
|
-
const startTimeOffset =
|
|
383
|
-
const hoverTime = startTimeOffset + videoDuration * hoverPosition;
|
|
287
|
+
const startTimeOffset = mediaControl.container.getStartTimeOffset();
|
|
288
|
+
const hoverTime = startTimeOffset + videoDuration * this.hoverPosition;
|
|
384
289
|
this.setText(hoverTime);
|
|
385
290
|
// determine which thumbnail applies to the current time
|
|
386
|
-
const thumbIndex = this.
|
|
387
|
-
const thumb = this.
|
|
291
|
+
const thumbIndex = this.getThumbIndexForTime(hoverTime);
|
|
292
|
+
const thumb = this.thumbs[thumbIndex];
|
|
388
293
|
// update thumbnail
|
|
389
|
-
const $spotlight = this.
|
|
390
|
-
$spotlight.
|
|
391
|
-
$spotlight.append(this._buildImg(thumb, this._getOptions().spotlightHeight));
|
|
294
|
+
const $spotlight = this.$el.find('#thumbnails-spotlight');
|
|
295
|
+
this.buildThumbImage(thumb, this.spotlightHeight, $spotlight.find('.thumbnail-container')).appendTo($spotlight);
|
|
392
296
|
const elWidth = this.$el.width();
|
|
393
297
|
const thumbWidth = $spotlight.width();
|
|
394
298
|
const thumbHeight = $spotlight.height();
|
|
395
|
-
let spotlightXPos = elWidth * hoverPosition - thumbWidth / 2;
|
|
396
299
|
// adjust so the entire thumbnail is always visible
|
|
397
|
-
spotlightXPos = Math.max(Math.min(
|
|
300
|
+
const spotlightXPos = Math.max(Math.min(elWidth * this.hoverPosition - thumbWidth / 2, elWidth - thumbWidth), 0);
|
|
398
301
|
$spotlight.css('left', spotlightXPos);
|
|
399
|
-
this.$
|
|
400
|
-
|
|
401
|
-
|
|
302
|
+
const $textThumbnail = this.$el.find('#thumbnails-text');
|
|
303
|
+
$textThumbnail.css('left', spotlightXPos);
|
|
304
|
+
$textThumbnail.css('width', thumbWidth);
|
|
305
|
+
$textThumbnail.css('bottom', thumbHeight + 1);
|
|
402
306
|
}
|
|
403
307
|
// returns the thumbnail which represents a time in the video
|
|
404
308
|
// or null if there is no thumbnail that can represent the time
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const thumb = thumbs[i];
|
|
309
|
+
getThumbIndexForTime(time) {
|
|
310
|
+
for (let i = this.thumbs.length - 1; i >= 0; i--) {
|
|
311
|
+
const thumb = this.thumbs[i];
|
|
409
312
|
if (thumb.time <= time) {
|
|
410
313
|
return i;
|
|
411
314
|
}
|
|
@@ -413,36 +316,47 @@ export class Thumbnails extends UICorePlugin {
|
|
|
413
316
|
// stretch the first thumbnail back to the start
|
|
414
317
|
return 0;
|
|
415
318
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
show: this._show,
|
|
419
|
-
thumbsLoaded: this._thumbsLoaded,
|
|
420
|
-
thumbs: this._thumbs.length,
|
|
421
|
-
});
|
|
422
|
-
if (!this._thumbsLoaded) {
|
|
319
|
+
update() {
|
|
320
|
+
if (!this.thumbsLoaded) {
|
|
423
321
|
return;
|
|
424
322
|
}
|
|
425
|
-
if (this.
|
|
323
|
+
if (this.showing && this.thumbs.length > 0) {
|
|
324
|
+
this.updateCarousel();
|
|
325
|
+
this.updateSpotlightThumb();
|
|
426
326
|
this.$el.removeClass('hidden');
|
|
427
|
-
this._updateCarousel();
|
|
428
|
-
this._updateSpotlightThumb();
|
|
429
327
|
}
|
|
430
328
|
else {
|
|
431
329
|
this.$el.addClass('hidden');
|
|
432
330
|
}
|
|
433
331
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
332
|
+
fixElements() {
|
|
333
|
+
const $spotlight = this.$el.find('#thumbnails-spotlight');
|
|
334
|
+
if (this.spotlightHeight) {
|
|
335
|
+
$spotlight.css('height', this.spotlightHeight);
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
$spotlight.remove();
|
|
339
|
+
}
|
|
340
|
+
const $backdrop = this.$el.find('#thumbnails-backdrop');
|
|
341
|
+
if (this.backdropHeight) {
|
|
342
|
+
$backdrop.css('height', this.backdropHeight);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
$backdrop.remove();
|
|
346
|
+
}
|
|
347
|
+
this.mount();
|
|
348
|
+
}
|
|
349
|
+
get shouldRender() {
|
|
350
|
+
return (this.options.thumbnails &&
|
|
351
|
+
this.options.thumbnails.sprite &&
|
|
352
|
+
this.options.thumbnails.vtt);
|
|
353
|
+
}
|
|
354
|
+
render() {
|
|
355
|
+
if (!this.shouldRender) {
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
this.$el.html(Thumbnails.template());
|
|
445
359
|
this.$el.addClass('hidden');
|
|
446
|
-
this
|
|
360
|
+
return this;
|
|
447
361
|
}
|
|
448
362
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/plugins/thumbnails/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAW3F"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function loadImageDimensions(url) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const img = new Image();
|
|
4
|
+
img.src = url;
|
|
5
|
+
img.onload = () => {
|
|
6
|
+
resolve({ width: img.width, height: img.height });
|
|
7
|
+
};
|
|
8
|
+
img.onerror = () => {
|
|
9
|
+
reject(new Error('Failed to load image'));
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
}
|
package/lib/testUtils.js
CHANGED
|
@@ -100,16 +100,16 @@ export function createMockContainer(options = {}, playback = createMockPlayback(
|
|
|
100
100
|
}
|
|
101
101
|
export function createMockMediaControl(core) {
|
|
102
102
|
const mediaControl = new UICorePlugin(core);
|
|
103
|
+
// TODO <div class="media-control-layer">
|
|
103
104
|
mediaControl.$el.html(`<div class="media-control-left-panel" data-media-control></div>
|
|
104
105
|
<div class="media-control-right-panel" data-media-control></div>
|
|
105
106
|
<div class="media-control-center-panel" data-media-control></div>`);
|
|
106
107
|
// @ts-ignore
|
|
107
|
-
mediaControl.putElement = vi.fn();
|
|
108
|
-
// @ts-ignore
|
|
109
108
|
mediaControl.mount = vi.fn();
|
|
110
109
|
// @ts-ignore
|
|
111
110
|
mediaControl.toggleElement = vi.fn();
|
|
112
111
|
vi.spyOn(mediaControl, 'trigger');
|
|
112
|
+
core.$el.append(mediaControl.$el);
|
|
113
113
|
return mediaControl;
|
|
114
114
|
}
|
|
115
115
|
export function createMockBottomGear(core) {
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gcorevideo/player",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.1",
|
|
4
4
|
"description": "Gcore JavaScript video player",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"player",
|
|
10
|
+
"video streaming"
|
|
11
|
+
],
|
|
8
12
|
"scripts": {
|
|
9
13
|
"build": "npm run build:ts && npm run build:bundle",
|
|
10
14
|
"build:1": "npm run build:ts && npm run build:bundle && date",
|