@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,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
UICorePlugin,
|
|
3
|
+
Events,
|
|
4
|
+
template,
|
|
5
|
+
$,
|
|
6
|
+
Container,
|
|
7
|
+
Core,
|
|
8
|
+
} from '@clappr/core'
|
|
9
|
+
import { trace } from '@gcorevideo/utils'
|
|
3
10
|
import parseSRT, { type ParsedSRT } from 'parse-srt'
|
|
11
|
+
import assert from 'assert'
|
|
4
12
|
|
|
5
13
|
import { TimeValue } from '../../playback.types.js'
|
|
6
14
|
|
|
@@ -9,22 +17,24 @@ import { CLAPPR_VERSION } from '../../build.js'
|
|
|
9
17
|
import pluginHtml from '../../../assets/thumbnails/scrub-thumbnails.ejs'
|
|
10
18
|
import '../../../assets/thumbnails/style.scss'
|
|
11
19
|
import { ZeptoResult } from '../../types.js'
|
|
12
|
-
import {
|
|
20
|
+
import { MediaControl } from '../media-control/MediaControl.js'
|
|
21
|
+
import { Clips } from '../clips/Clips.js'
|
|
22
|
+
import { loadImageDimensions } from './utils.js'
|
|
13
23
|
|
|
14
24
|
/**
|
|
15
25
|
* Plugin configuration options for the thumbnails plugin.
|
|
16
26
|
* @beta
|
|
17
27
|
*/
|
|
18
28
|
export type ThumbnailsPluginSettings = {
|
|
19
|
-
backdropHeight
|
|
20
|
-
backdropMaxOpacity
|
|
21
|
-
backdropMinOpacity
|
|
22
|
-
spotlightHeight
|
|
29
|
+
backdropHeight?: number
|
|
30
|
+
backdropMaxOpacity?: number
|
|
31
|
+
backdropMinOpacity?: number
|
|
32
|
+
spotlightHeight?: number
|
|
23
33
|
sprite: string
|
|
24
34
|
vtt: string
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
type
|
|
37
|
+
type ThumbnailDesc = {
|
|
28
38
|
url: string
|
|
29
39
|
time: number
|
|
30
40
|
w: number
|
|
@@ -40,6 +50,12 @@ const T = 'plugins.thumbnails'
|
|
|
40
50
|
/**
|
|
41
51
|
* `PLUGIN` that displays the thumbnails of the video when available.
|
|
42
52
|
* @beta
|
|
53
|
+
* @remarks
|
|
54
|
+
* The plugin needs specially crafted VTT file with a thumbnail sprite sheet to work.
|
|
55
|
+
* The VTT consist of timestamp records followed by a thumbnail area
|
|
56
|
+
*
|
|
57
|
+
* Configuration options - {@link ThumbnailsPluginSettings}
|
|
58
|
+
*
|
|
43
59
|
* @example
|
|
44
60
|
* ```ts
|
|
45
61
|
* import { Thumbnails } from '@gcorevideo/player'
|
|
@@ -60,31 +76,23 @@ const T = 'plugins.thumbnails'
|
|
|
60
76
|
* ```
|
|
61
77
|
*/
|
|
62
78
|
export class Thumbnails extends UICorePlugin {
|
|
63
|
-
private
|
|
64
|
-
|
|
65
|
-
private _$backdrop: ZeptoResult | null = null
|
|
66
|
-
|
|
67
|
-
private $container: ZeptoResult | null = null
|
|
68
|
-
|
|
69
|
-
private $img: ZeptoResult | null = null
|
|
79
|
+
private $backdropCarouselImgs: ZeptoResult[] = []
|
|
70
80
|
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
private $textThumbnail: ZeptoResult | null = null
|
|
81
|
+
private spriteSheetHeight: number = 0
|
|
74
82
|
|
|
75
|
-
private
|
|
83
|
+
private spriteSheetWidth: number = 0
|
|
76
84
|
|
|
77
|
-
private
|
|
85
|
+
private hoverPosition = 0
|
|
78
86
|
|
|
79
|
-
private
|
|
87
|
+
private showing = false
|
|
80
88
|
|
|
81
|
-
private
|
|
89
|
+
private thumbsLoaded = false
|
|
82
90
|
|
|
83
|
-
private
|
|
91
|
+
private spotlightHeight = 0
|
|
84
92
|
|
|
85
|
-
private
|
|
93
|
+
private backdropHeight = 0
|
|
86
94
|
|
|
87
|
-
private
|
|
95
|
+
private thumbs: ThumbnailDesc[] = []
|
|
88
96
|
|
|
89
97
|
/**
|
|
90
98
|
* @internal
|
|
@@ -105,12 +113,17 @@ export class Thumbnails extends UICorePlugin {
|
|
|
105
113
|
*/
|
|
106
114
|
override get attributes() {
|
|
107
115
|
return {
|
|
108
|
-
class:
|
|
116
|
+
class: 'scrub-thumbnails',
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
|
|
112
120
|
private static readonly template = template(pluginHtml)
|
|
113
121
|
|
|
122
|
+
constructor(core: Core) {
|
|
123
|
+
super(core)
|
|
124
|
+
this.backdropHeight = this.options.thumbnails?.backdropHeight ?? 0
|
|
125
|
+
}
|
|
126
|
+
|
|
114
127
|
/*
|
|
115
128
|
* Helper to build the "thumbs" property for a sprite sheet.
|
|
116
129
|
*
|
|
@@ -122,26 +135,25 @@ export class Thumbnails extends UICorePlugin {
|
|
|
122
135
|
* timeInterval- The interval (in seconds) between the thumbnails.
|
|
123
136
|
* startTime- The time (in seconds) that the first thumbnail represents. (defaults to 0)
|
|
124
137
|
*/
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
138
|
+
private buildSpriteConfig(
|
|
139
|
+
vtt: ParsedSRT[],
|
|
140
|
+
baseUrl: string,
|
|
141
|
+
): ThumbnailDesc[] {
|
|
142
|
+
const thumbs: ThumbnailDesc[] = []
|
|
129
143
|
|
|
130
144
|
for (const vt of vtt) {
|
|
131
145
|
const el = vt.text
|
|
132
|
-
// if (el && el.search(/\d*,\d*,\d*,\d*/g) > -1) {
|
|
133
|
-
// el = el.match(/\d*,\d*,\d*,\d*/g)[0];
|
|
134
|
-
// coor = el.split(',');
|
|
135
|
-
// }
|
|
136
146
|
if (el) {
|
|
137
|
-
const m = el.match(/xywh
|
|
147
|
+
const m = el.match(/(\w+)#xywh=(\d+,\d+,\d+,\d+)/)
|
|
138
148
|
if (m) {
|
|
139
|
-
const coor = m[
|
|
149
|
+
const coor = m[2].split(',')
|
|
140
150
|
const w = parseInt(coor[2], 10)
|
|
141
151
|
const h = parseInt(coor[3], 10)
|
|
142
152
|
if (w > 0 && h > 0) {
|
|
143
153
|
thumbs.push({
|
|
144
|
-
|
|
154
|
+
// TODO handle relative URLs
|
|
155
|
+
// url: new URL(m[0], baseUrl).toString(),
|
|
156
|
+
url: baseUrl,
|
|
145
157
|
time: vt.start,
|
|
146
158
|
w,
|
|
147
159
|
h,
|
|
@@ -156,318 +168,197 @@ export class Thumbnails extends UICorePlugin {
|
|
|
156
168
|
return thumbs
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
// TODO check if seek enabled
|
|
160
|
-
|
|
161
171
|
/**
|
|
162
172
|
* @internal
|
|
163
173
|
*/
|
|
164
174
|
override bindEvents() {
|
|
165
|
-
this.listenToOnce(this.core, Events.CORE_READY, this.
|
|
166
|
-
this.listenTo(
|
|
167
|
-
this.core.mediaControl,
|
|
168
|
-
Events.MEDIACONTROL_MOUSEMOVE_SEEKBAR,
|
|
169
|
-
this._onMouseMove,
|
|
170
|
-
)
|
|
171
|
-
this.listenTo(
|
|
172
|
-
this.core.mediaControl,
|
|
173
|
-
Events.MEDIACONTROL_MOUSELEAVE_SEEKBAR,
|
|
174
|
-
this._onMouseLeave,
|
|
175
|
-
)
|
|
176
|
-
this.listenTo(
|
|
177
|
-
this.core.mediaControl,
|
|
178
|
-
Events.MEDIACONTROL_RENDERED,
|
|
179
|
-
this._init,
|
|
180
|
-
)
|
|
181
|
-
this.listenTo(
|
|
182
|
-
this.core.mediaControl,
|
|
183
|
-
Events.MEDIACONTROL_CONTAINERCHANGED,
|
|
184
|
-
this._onMediaControlContainerChanged,
|
|
185
|
-
)
|
|
175
|
+
this.listenToOnce(this.core, Events.CORE_READY, this.onCoreReady)
|
|
186
176
|
}
|
|
187
177
|
|
|
188
|
-
private
|
|
189
|
-
|
|
190
|
-
this.stopListening(
|
|
191
|
-
this._oldContainer,
|
|
192
|
-
Events.CONTAINER_TIMEUPDATE,
|
|
193
|
-
this._renderPlugin,
|
|
194
|
-
)
|
|
195
|
-
}
|
|
196
|
-
this._oldContainer = this.core.mediaControl.container
|
|
197
|
-
this.listenTo(
|
|
198
|
-
this.core.mediaControl.container,
|
|
199
|
-
Events.CONTAINER_TIMEUPDATE,
|
|
200
|
-
this._renderPlugin,
|
|
201
|
-
)
|
|
178
|
+
private bindContainerEvents(container: Container) {
|
|
179
|
+
this.listenTo(container, Events.CONTAINER_TIMEUPDATE, this.update)
|
|
202
180
|
}
|
|
203
181
|
|
|
204
|
-
private
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
214
|
-
}
|
|
215
|
-
} catch (error) {
|
|
216
|
-
reportError(error)
|
|
182
|
+
private onCoreReady() {
|
|
183
|
+
const mediaControl = this.core.getPlugin('media_control') as
|
|
184
|
+
| MediaControl
|
|
185
|
+
| undefined
|
|
186
|
+
assert(
|
|
187
|
+
mediaControl,
|
|
188
|
+
`MediaControl is required for ${this.name} plugin to work`,
|
|
189
|
+
)
|
|
217
190
|
|
|
191
|
+
if (
|
|
192
|
+
!this.options.thumbnails ||
|
|
193
|
+
!this.options.thumbnails.sprite ||
|
|
194
|
+
!this.options.thumbnails.vtt
|
|
195
|
+
) {
|
|
196
|
+
trace(
|
|
197
|
+
`${T} misconfigured: options.thumbnails.sprite and options.thumbnails.vtt are required`,
|
|
198
|
+
)
|
|
199
|
+
this.destroy()
|
|
218
200
|
return
|
|
219
201
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.
|
|
223
|
-
|
|
224
|
-
spriteSheet,
|
|
225
|
-
)
|
|
226
|
-
if (!this._thumbs.length) {
|
|
202
|
+
const { sprite: spriteSheet, vtt } = this.options.thumbnails
|
|
203
|
+
this.thumbs = this.buildSpriteConfig(parseSRT(vtt), spriteSheet)
|
|
204
|
+
if (!this.thumbs.length) {
|
|
205
|
+
trace(`${T} failed to parse the sprite sheet`)
|
|
227
206
|
this.destroy()
|
|
228
207
|
return
|
|
229
208
|
}
|
|
209
|
+
this.spotlightHeight = this.options.thumbnails?.spotlightHeight ?? 0
|
|
230
210
|
this.loadSpriteSheet(spriteSheet).then(() => {
|
|
231
|
-
this.
|
|
232
|
-
this.
|
|
233
|
-
|
|
211
|
+
this.thumbsLoaded = true
|
|
212
|
+
this.spotlightHeight = this.spotlightHeight
|
|
213
|
+
? Math.min(this.spotlightHeight, this.thumbs[0].h)
|
|
214
|
+
: this.thumbs[0].h
|
|
215
|
+
this.init()
|
|
234
216
|
})
|
|
217
|
+
this.listenTo(
|
|
218
|
+
mediaControl,
|
|
219
|
+
Events.MEDIACONTROL_MOUSEMOVE_SEEKBAR,
|
|
220
|
+
this.onMouseMoveSeekbar,
|
|
221
|
+
)
|
|
222
|
+
this.listenTo(
|
|
223
|
+
mediaControl,
|
|
224
|
+
Events.MEDIACONTROL_MOUSELEAVE_SEEKBAR,
|
|
225
|
+
this.onMouseLeave,
|
|
226
|
+
)
|
|
227
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, this.init)
|
|
228
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_CONTAINERCHANGED, () =>
|
|
229
|
+
this.onContainerChanged(mediaControl.container),
|
|
230
|
+
)
|
|
235
231
|
}
|
|
236
232
|
|
|
237
233
|
private async loadSpriteSheet(spriteSheetUrl: string): Promise<void> {
|
|
238
|
-
return
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
this.spriteSheetHeight = img.height
|
|
242
|
-
resolve()
|
|
243
|
-
}
|
|
244
|
-
img.onerror = reject
|
|
245
|
-
img.src = spriteSheetUrl
|
|
234
|
+
return loadImageDimensions(spriteSheetUrl).then(({ height, width }) => {
|
|
235
|
+
this.spriteSheetHeight = height
|
|
236
|
+
this.spriteSheetWidth = width
|
|
246
237
|
})
|
|
247
238
|
}
|
|
248
239
|
|
|
249
|
-
private
|
|
250
|
-
this.
|
|
240
|
+
private onContainerChanged(container: Container) {
|
|
241
|
+
this.bindContainerEvents(container)
|
|
251
242
|
}
|
|
252
243
|
|
|
253
|
-
private
|
|
254
|
-
if (!this.
|
|
255
|
-
//
|
|
244
|
+
private init() {
|
|
245
|
+
if (!this.thumbsLoaded) {
|
|
246
|
+
// init() will be called when the thumbs are loaded,
|
|
256
247
|
// 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)
|
|
257
248
|
return
|
|
258
249
|
}
|
|
259
250
|
// Init the backdropCarousel as array to keep reference of thumbnail images
|
|
260
|
-
this
|
|
261
|
-
|
|
262
|
-
this.
|
|
263
|
-
this.
|
|
264
|
-
this._renderPlugin()
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private _getOptions(): ThumbnailsPluginSettings {
|
|
268
|
-
if (!('thumbnails' in this.core.options)) {
|
|
269
|
-
throw "'thumbnail property missing from options object."
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return this.core.options.thumbnails
|
|
251
|
+
this.$backdropCarouselImgs = []
|
|
252
|
+
this.fixElements()
|
|
253
|
+
this.loadBackdrop()
|
|
254
|
+
this.update()
|
|
273
255
|
}
|
|
274
256
|
|
|
275
|
-
private
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
257
|
+
private mount() {
|
|
258
|
+
const mediaControl = this.core.getPlugin('media_control') as MediaControl
|
|
259
|
+
mediaControl.$el.find('.seek-time').css('bottom', 56) // TODO check the offset
|
|
260
|
+
mediaControl.$el.first().after(this.$el);
|
|
279
261
|
}
|
|
280
262
|
|
|
281
|
-
private
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
this._calculateHoverPosition(e)
|
|
288
|
-
this._show = true
|
|
289
|
-
this._renderPlugin()
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
private _onMouseLeave() {
|
|
293
|
-
this._show = false
|
|
294
|
-
this._renderPlugin()
|
|
263
|
+
private onMouseMoveSeekbar(_: MouseEvent, pos: number) {
|
|
264
|
+
if (Math.abs(pos - this.hoverPosition) >= 0.01) {
|
|
265
|
+
this.hoverPosition = pos
|
|
266
|
+
this.showing = true
|
|
267
|
+
this.update()
|
|
268
|
+
}
|
|
295
269
|
}
|
|
296
270
|
|
|
297
|
-
private
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
// proportion into the seek bar that the mouse is hovered over 0-1
|
|
302
|
-
this._hoverPosition = Math.min(
|
|
303
|
-
1,
|
|
304
|
-
Math.max(offset / this.core.mediaControl.$seekBarContainer.width(), 0),
|
|
305
|
-
)
|
|
271
|
+
private onMouseLeave() {
|
|
272
|
+
this.showing = false
|
|
273
|
+
this.update()
|
|
306
274
|
}
|
|
307
275
|
|
|
308
|
-
// private _buildThumbsFromOptions() {
|
|
309
|
-
// const thumbs = this._thumbs;
|
|
310
|
-
// const promises = thumbs.map((thumb) => {
|
|
311
|
-
// return this._addThumbFromSrc(thumb);
|
|
312
|
-
// });
|
|
313
|
-
|
|
314
|
-
// return Promise.all(promises);
|
|
315
|
-
// }
|
|
316
|
-
|
|
317
|
-
// private _addThumbFromSrc(thumbSrc) {
|
|
318
|
-
// return new Promise((resolve, reject) => {
|
|
319
|
-
// const img = new Image();
|
|
320
|
-
|
|
321
|
-
// img.onload = () => {
|
|
322
|
-
// resolve(img);
|
|
323
|
-
// };
|
|
324
|
-
// img.onerror = reject;
|
|
325
|
-
// img.src = thumbSrc.url;
|
|
326
|
-
// }).then((img) => {
|
|
327
|
-
// const startTime = thumbSrc.time;
|
|
328
|
-
// // determine the thumb index
|
|
329
|
-
// let index = null;
|
|
330
|
-
|
|
331
|
-
// this._thumbs.some((thumb, i) => {
|
|
332
|
-
// if (startTime < thumb.time) {
|
|
333
|
-
// index = i;
|
|
334
|
-
|
|
335
|
-
// return true;
|
|
336
|
-
// }
|
|
337
|
-
|
|
338
|
-
// return false;
|
|
339
|
-
// });
|
|
340
|
-
// if (index === null) {
|
|
341
|
-
// index = this._thumbs.length;
|
|
342
|
-
// }
|
|
343
|
-
|
|
344
|
-
// const next = index < this._thumbs.length ? this._thumbs[index] : null;
|
|
345
|
-
// const prev = index > 0 ? this._thumbs[index - 1] : null;
|
|
346
|
-
|
|
347
|
-
// if (prev) {
|
|
348
|
-
// // update the duration of the previous thumbnail
|
|
349
|
-
// prev.duration = startTime - prev.time;
|
|
350
|
-
// }
|
|
351
|
-
// // the duration this thumb lasts for
|
|
352
|
-
// // if it is the last thumb then duration will be null
|
|
353
|
-
// const duration = next ? next.time - thumbSrc.time : null;
|
|
354
|
-
// const imageW = img.width;
|
|
355
|
-
// const imageH = img.height;
|
|
356
|
-
// const thumb = {
|
|
357
|
-
// imageW: imageW, // actual width of image
|
|
358
|
-
// imageH: imageH, // actual height of image
|
|
359
|
-
// x: thumbSrc.x || 0, // x coord in image of sprite
|
|
360
|
-
// y: thumbSrc.y || 0, // y coord in image of sprite
|
|
361
|
-
// w: thumbSrc.w || imageW, // width of sprite
|
|
362
|
-
// h: thumbSrc.h || imageH, // height of sprite
|
|
363
|
-
// url: thumbSrc.url,
|
|
364
|
-
// time: startTime, // time this thumb represents
|
|
365
|
-
// duration: duration, // how long (from time) this thumb represents
|
|
366
|
-
// src: thumbSrc
|
|
367
|
-
// };
|
|
368
|
-
|
|
369
|
-
// this._thumbs.splice(index, 0, thumb);
|
|
370
|
-
|
|
371
|
-
// return thumb;
|
|
372
|
-
// });
|
|
373
|
-
// }
|
|
374
|
-
|
|
375
276
|
// builds a dom element which represents the thumbnail
|
|
376
|
-
// scaled to the
|
|
377
|
-
private
|
|
277
|
+
// scaled to the given height
|
|
278
|
+
private buildThumbImage(
|
|
279
|
+
thumb: ThumbnailDesc,
|
|
280
|
+
height: number,
|
|
281
|
+
$ref?: ZeptoResult,
|
|
282
|
+
) {
|
|
378
283
|
const scaleFactor = height / thumb.h
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.$container.css('height', height)
|
|
392
|
-
this.$img.css({
|
|
393
|
-
height: this.spriteSheetHeight * scaleFactor,
|
|
394
|
-
left: -1 * thumb.x * scaleFactor,
|
|
395
|
-
top: -1 * thumb.y * scaleFactor,
|
|
284
|
+
const $container = $ref && $ref.length ? $ref : $('<div />').addClass('thumbnail-container')
|
|
285
|
+
|
|
286
|
+
$container.css('width', thumb.w * scaleFactor)
|
|
287
|
+
$container.css('height', height)
|
|
288
|
+
$container.css({
|
|
289
|
+
backgroundImage: `url(${thumb.url})`,
|
|
290
|
+
backgroundSize: `${Math.floor(
|
|
291
|
+
this.spriteSheetWidth * scaleFactor,
|
|
292
|
+
)}px ${Math.floor(this.spriteSheetHeight * scaleFactor)}px`,
|
|
293
|
+
backgroundPosition: `-${Math.floor(
|
|
294
|
+
thumb.x * scaleFactor,
|
|
295
|
+
)}px -${Math.floor(thumb.y * scaleFactor)}px`,
|
|
396
296
|
})
|
|
397
|
-
if (this.$container.find(this.$img).length === 0) {
|
|
398
|
-
this.$container.append(this.$img)
|
|
399
|
-
}
|
|
400
297
|
|
|
401
|
-
return
|
|
298
|
+
return $container
|
|
402
299
|
}
|
|
403
300
|
|
|
404
|
-
private
|
|
405
|
-
if (!this.
|
|
301
|
+
private loadBackdrop() {
|
|
302
|
+
if (!this.backdropHeight) {
|
|
406
303
|
// disabled
|
|
407
304
|
return
|
|
408
305
|
}
|
|
409
306
|
|
|
410
307
|
// append each of the thumbnails to the backdrop carousel
|
|
411
|
-
const $carousel = this.
|
|
308
|
+
const $carousel = this.$el.find('#thumbnails-carousel')
|
|
412
309
|
|
|
413
|
-
for (const thumb of this.
|
|
414
|
-
const $img = this.
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
this._$backdropCarouselImgs.push($img)
|
|
310
|
+
for (const thumb of this.thumbs) {
|
|
311
|
+
const $img = this.buildThumbImage(thumb, this.backdropHeight)
|
|
312
|
+
// Keep reference to the thumbnail
|
|
313
|
+
this.$backdropCarouselImgs.push($img)
|
|
418
314
|
// Add thumbnail to DOM
|
|
419
315
|
$carousel.append($img)
|
|
420
316
|
}
|
|
421
317
|
}
|
|
422
318
|
|
|
423
319
|
private setText(time: TimeValue) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
this.$
|
|
320
|
+
const clips = this.core.getPlugin('clips') as Clips
|
|
321
|
+
if (clips) {
|
|
322
|
+
const txt = clips.getText(time)
|
|
323
|
+
this.$el.find('#thumbnails-text').text(txt ?? '')
|
|
428
324
|
}
|
|
429
325
|
}
|
|
430
326
|
|
|
431
327
|
// calculate how far along the carousel should currently be slid
|
|
432
328
|
// depending on where the user is hovering on the progress bar
|
|
433
|
-
private
|
|
434
|
-
|
|
435
|
-
backdropHeight: this._getOptions().backdropHeight,
|
|
436
|
-
})
|
|
437
|
-
if (!this._getOptions().backdropHeight) {
|
|
329
|
+
private updateCarousel() {
|
|
330
|
+
if (!this.backdropHeight) {
|
|
438
331
|
// disabled
|
|
439
332
|
return
|
|
440
333
|
}
|
|
441
334
|
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
335
|
+
const mediaControl = this.core.getPlugin('media_control') as MediaControl
|
|
336
|
+
|
|
337
|
+
const videoDuration = mediaControl.container.getDuration()
|
|
338
|
+
const startTimeOffset = mediaControl.container.getStartTimeOffset()
|
|
446
339
|
// the time into the video at the current hover position
|
|
447
|
-
const hoverTime = startTimeOffset + videoDuration * hoverPosition
|
|
448
|
-
const
|
|
449
|
-
const
|
|
340
|
+
const hoverTime = startTimeOffset + videoDuration * this.hoverPosition
|
|
341
|
+
const $backdrop = this.$el.find('#thumbnails-backdrop')
|
|
342
|
+
const backdropWidth = $backdrop.width()
|
|
343
|
+
const $carousel = this.$el.find('#thumbnails-carousel')
|
|
450
344
|
const carouselWidth = $carousel.width()
|
|
451
345
|
|
|
452
346
|
// slide the carousel so that the image on the carousel that is above where the person
|
|
453
347
|
// is hovering maps to that position in time.
|
|
454
348
|
// Thumbnails may not be distributed at even times along the video
|
|
455
|
-
const thumbs = this._thumbs
|
|
456
349
|
|
|
457
350
|
// assuming that each thumbnail has the same width
|
|
458
|
-
const thumbWidth = carouselWidth / thumbs.length
|
|
351
|
+
const thumbWidth = carouselWidth / this.thumbs.length
|
|
459
352
|
|
|
460
353
|
// determine which thumbnail applies to the current time
|
|
461
|
-
const thumbIndex = this.
|
|
462
|
-
const thumb = thumbs[thumbIndex]
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
thumbDuration = Math.max(videoDuration + startTimeOffset - thumb.time, 0)
|
|
470
|
-
}
|
|
354
|
+
const thumbIndex = this.getThumbIndexForTime(hoverTime)
|
|
355
|
+
const thumb = this.thumbs[thumbIndex]
|
|
356
|
+
// the last thumbnail duration will be null as it can't be determined
|
|
357
|
+
// e.g the duration of the video may increase over time (live stream)
|
|
358
|
+
// so calculate the duration now so this last thumbnail lasts till the end
|
|
359
|
+
const thumbDuration =
|
|
360
|
+
thumb.duration ??
|
|
361
|
+
Math.max(videoDuration + startTimeOffset - thumb.time, 0)
|
|
471
362
|
|
|
472
363
|
// determine how far accross that thumbnail we are
|
|
473
364
|
const timeIntoThumb = hoverTime - thumb.time
|
|
@@ -477,15 +368,15 @@ export class Thumbnails extends UICorePlugin {
|
|
|
477
368
|
// now calculate the position along carousel that we want to be above the hover position
|
|
478
369
|
const xCoordInCarousel = thumbIndex * thumbWidth + xCoordInThumb
|
|
479
370
|
// and finally the position of the carousel when the hover position is taken in to consideration
|
|
480
|
-
const carouselXCoord = xCoordInCarousel - hoverPosition * backdropWidth
|
|
371
|
+
const carouselXCoord = xCoordInCarousel - this.hoverPosition * backdropWidth
|
|
481
372
|
|
|
482
|
-
$carousel.css('left', -carouselXCoord)
|
|
373
|
+
$carousel.css('left', -carouselXCoord) // TODO +px
|
|
483
374
|
|
|
484
|
-
const maxOpacity = this.
|
|
485
|
-
const minOpacity = this.
|
|
375
|
+
const maxOpacity = this.options.thumbnails.backdropMaxOpacity ?? 0.6
|
|
376
|
+
const minOpacity = this.options.thumbnails.backdropMinOpacity ?? 0.08
|
|
486
377
|
|
|
487
378
|
// now update the transparencies so that they fade in around the active one
|
|
488
|
-
for (let i = 0; i < thumbs.length; i++) {
|
|
379
|
+
for (let i = 0; i < this.thumbs.length; i++) {
|
|
489
380
|
const thumbXCoord = thumbWidth * i
|
|
490
381
|
let distance = thumbXCoord - xCoordInCarousel
|
|
491
382
|
|
|
@@ -502,61 +393,63 @@ export class Thumbnails extends UICorePlugin {
|
|
|
502
393
|
minOpacity,
|
|
503
394
|
)
|
|
504
395
|
|
|
505
|
-
this
|
|
396
|
+
this.$backdropCarouselImgs[i].css('opacity', opacity)
|
|
506
397
|
}
|
|
507
398
|
}
|
|
508
399
|
|
|
509
|
-
private
|
|
510
|
-
|
|
511
|
-
spotlightHeight: this._getOptions().spotlightHeight,
|
|
512
|
-
})
|
|
513
|
-
if (!this._getOptions().spotlightHeight) {
|
|
400
|
+
private updateSpotlightThumb() {
|
|
401
|
+
if (!this.spotlightHeight) {
|
|
514
402
|
// disabled
|
|
515
403
|
return
|
|
516
404
|
}
|
|
517
405
|
|
|
518
|
-
const
|
|
519
|
-
const videoDuration =
|
|
406
|
+
const mediaControl = this.core.getPlugin('media_control') as MediaControl
|
|
407
|
+
const videoDuration = mediaControl.container.getDuration()
|
|
520
408
|
// the time into the video at the current hover position
|
|
521
|
-
const startTimeOffset =
|
|
522
|
-
|
|
523
|
-
const hoverTime = startTimeOffset + videoDuration * hoverPosition
|
|
409
|
+
const startTimeOffset = mediaControl.container.getStartTimeOffset()
|
|
410
|
+
const hoverTime = startTimeOffset + videoDuration * this.hoverPosition
|
|
524
411
|
|
|
525
412
|
this.setText(hoverTime)
|
|
526
413
|
|
|
527
414
|
// determine which thumbnail applies to the current time
|
|
528
|
-
const thumbIndex = this.
|
|
529
|
-
const thumb = this.
|
|
415
|
+
const thumbIndex = this.getThumbIndexForTime(hoverTime)
|
|
416
|
+
const thumb = this.thumbs[thumbIndex]
|
|
530
417
|
|
|
531
418
|
// update thumbnail
|
|
532
|
-
const $spotlight = this.
|
|
419
|
+
const $spotlight = this.$el.find('#thumbnails-spotlight')
|
|
533
420
|
|
|
534
|
-
|
|
535
|
-
|
|
421
|
+
this.buildThumbImage(
|
|
422
|
+
thumb,
|
|
423
|
+
this.spotlightHeight,
|
|
424
|
+
$spotlight.find('.thumbnail-container'),
|
|
425
|
+
).appendTo($spotlight)
|
|
536
426
|
|
|
537
427
|
const elWidth = this.$el.width()
|
|
538
428
|
const thumbWidth = $spotlight.width()
|
|
539
429
|
const thumbHeight = $spotlight.height()
|
|
540
430
|
|
|
541
|
-
let spotlightXPos = elWidth * hoverPosition - thumbWidth / 2
|
|
542
|
-
|
|
543
431
|
// adjust so the entire thumbnail is always visible
|
|
544
|
-
spotlightXPos = Math.max(
|
|
432
|
+
const spotlightXPos = Math.max(
|
|
433
|
+
Math.min(
|
|
434
|
+
elWidth * this.hoverPosition - thumbWidth / 2,
|
|
435
|
+
elWidth - thumbWidth,
|
|
436
|
+
),
|
|
437
|
+
0,
|
|
438
|
+
)
|
|
545
439
|
|
|
546
440
|
$spotlight.css('left', spotlightXPos)
|
|
547
441
|
|
|
548
|
-
this.$
|
|
549
|
-
|
|
550
|
-
|
|
442
|
+
const $textThumbnail = this.$el.find('#thumbnails-text')
|
|
443
|
+
$textThumbnail.css('left', spotlightXPos)
|
|
444
|
+
$textThumbnail.css('width', thumbWidth)
|
|
445
|
+
$textThumbnail.css('bottom', thumbHeight + 1)
|
|
551
446
|
}
|
|
552
447
|
|
|
553
448
|
// returns the thumbnail which represents a time in the video
|
|
554
449
|
// or null if there is no thumbnail that can represent the time
|
|
555
|
-
private
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
for (let i = thumbs.length - 1; i >= 0; i--) {
|
|
559
|
-
const thumb = thumbs[i]
|
|
450
|
+
private getThumbIndexForTime(time: TimeValue) {
|
|
451
|
+
for (let i = this.thumbs.length - 1; i >= 0; i--) {
|
|
452
|
+
const thumb = this.thumbs[i]
|
|
560
453
|
|
|
561
454
|
if (thumb.time <= time) {
|
|
562
455
|
return i
|
|
@@ -567,38 +460,50 @@ export class Thumbnails extends UICorePlugin {
|
|
|
567
460
|
return 0
|
|
568
461
|
}
|
|
569
462
|
|
|
570
|
-
private
|
|
571
|
-
|
|
572
|
-
show: this._show,
|
|
573
|
-
thumbsLoaded: this._thumbsLoaded,
|
|
574
|
-
thumbs: this._thumbs.length,
|
|
575
|
-
})
|
|
576
|
-
if (!this._thumbsLoaded) {
|
|
463
|
+
private update() {
|
|
464
|
+
if (!this.thumbsLoaded) {
|
|
577
465
|
return
|
|
578
466
|
}
|
|
579
|
-
if (this.
|
|
467
|
+
if (this.showing && this.thumbs.length > 0) {
|
|
468
|
+
this.updateCarousel()
|
|
469
|
+
this.updateSpotlightThumb()
|
|
580
470
|
this.$el.removeClass('hidden')
|
|
581
|
-
this._updateCarousel()
|
|
582
|
-
this._updateSpotlightThumb()
|
|
583
471
|
} else {
|
|
584
472
|
this.$el.addClass('hidden')
|
|
585
473
|
}
|
|
586
474
|
}
|
|
587
475
|
|
|
588
|
-
private
|
|
589
|
-
|
|
590
|
-
this
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
476
|
+
private fixElements() {
|
|
477
|
+
const $spotlight = this.$el.find('#thumbnails-spotlight')
|
|
478
|
+
if (this.spotlightHeight) {
|
|
479
|
+
$spotlight.css('height', this.spotlightHeight)
|
|
480
|
+
} else {
|
|
481
|
+
$spotlight.remove()
|
|
482
|
+
}
|
|
483
|
+
const $backdrop = this.$el.find('#thumbnails-backdrop')
|
|
484
|
+
if (this.backdropHeight) {
|
|
485
|
+
$backdrop.css('height', this.backdropHeight)
|
|
486
|
+
} else {
|
|
487
|
+
$backdrop.remove()
|
|
488
|
+
}
|
|
489
|
+
this.mount()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
private get shouldRender() {
|
|
493
|
+
return (
|
|
494
|
+
this.options.thumbnails &&
|
|
495
|
+
this.options.thumbnails.sprite &&
|
|
496
|
+
this.options.thumbnails.vtt
|
|
595
497
|
)
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
override render() {
|
|
501
|
+
if (!this.shouldRender) {
|
|
502
|
+
return this
|
|
503
|
+
}
|
|
504
|
+
this.$el.html(Thumbnails.template())
|
|
601
505
|
this.$el.addClass('hidden')
|
|
602
|
-
|
|
506
|
+
|
|
507
|
+
return this
|
|
603
508
|
}
|
|
604
509
|
}
|