@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.
@@ -1,6 +1,14 @@
1
- import { UICorePlugin, Events, template, $, Container } from '@clappr/core'
2
- import { reportError, trace } from '@gcorevideo/utils'
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 { getPageX } from '../utils.js'
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: number
20
- backdropMaxOpacity: number
21
- backdropMinOpacity: number
22
- spotlightHeight: number
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 Thumb = {
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 _$spotlight: ZeptoResult | null = null
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 _$carousel: ZeptoResult | null = null
72
-
73
- private $textThumbnail: ZeptoResult | null = null
81
+ private spriteSheetHeight: number = 0
74
82
 
75
- private _$backdropCarouselImgs: ZeptoResult[] = []
83
+ private spriteSheetWidth: number = 0
76
84
 
77
- private spriteSheetHeight: number = 0
85
+ private hoverPosition = 0
78
86
 
79
- private _hoverPosition = 0
87
+ private showing = false
80
88
 
81
- private _show = false
89
+ private thumbsLoaded = false
82
90
 
83
- private _thumbsLoaded = false
91
+ private spotlightHeight = 0
84
92
 
85
- private _oldContainer: Container | null = null
93
+ private backdropHeight = 0
86
94
 
87
- private _thumbs: Thumb[] = []
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: this.name,
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
- // buildSpriteConfig(vtt, spriteSheetUrl, numThumbs, thumbWidth, thumbHeight, numColumns, timeInterval, startTime) {
126
- private buildSpriteConfig(vtt: ParsedSRT[], spriteSheetUrl: string): Thumb[] {
127
- const thumbs: Thumb[] = []
128
- // let coor: string[] = [];
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=\d*,\d*,\d*,\d*/g)
147
+ const m = el.match(/(\w+)#xywh=(\d+,\d+,\d+,\d+)/)
138
148
  if (m) {
139
- const coor = m[0].split(',')
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
- url: spriteSheetUrl,
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._onCoreReady)
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 _bindContainerEvents() {
189
- if (this._oldContainer) {
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 _onCoreReady() {
205
- try {
206
- if (
207
- !this.options.thumbnails ||
208
- !this.options.thumbnails.sprite ||
209
- !this.options.thumbnails.vtt
210
- ) {
211
- this.destroy()
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
- // TODO options
221
- const spriteSheet = this.options.thumbnails.sprite
222
- this._thumbs = this.buildSpriteConfig(
223
- parseSRT(this.options.thumbnails.vtt),
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._thumbsLoaded = true
232
- this.core.options.thumbnails.spotlightHeight = this._thumbs[0].h
233
- this._init()
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 new Promise((resolve, reject) => {
239
- const img = new Image()
240
- img.onload = () => {
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 _onMediaControlContainerChanged() {
250
- this._bindContainerEvents()
240
+ private onContainerChanged(container: Container) {
241
+ this.bindContainerEvents(container)
251
242
  }
252
243
 
253
- private _init() {
254
- if (!this._thumbsLoaded) {
255
- // _init() will be called when the thumbs are loaded,
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._$backdropCarouselImgs = []
261
- // create/recreate the dom elements for the plugin
262
- this._createElements()
263
- this._loadBackdrop()
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 _appendElToMediaControl() {
276
- // insert after the background
277
- this.core.mediaControl.$el.find('.seek-time').css('bottom', 56)
278
- this.core.mediaControl.$el.first().after(this.el)
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 _onMouseMove(e: MouseEvent) {
282
- // trace(`${T} _onMouseMove`, {
283
- // e: (e as any).name,
284
- // t: typeof e,
285
- // t2: typeof arguments[1],
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 _calculateHoverPosition(e: MouseEvent) {
298
- const offset =
299
- getPageX(e) - this.core.mediaControl.$seekBarContainer.offset().left
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 provided height
377
- private _buildImg(thumb: Thumb, height: number) {
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
- if (!this.$img) {
381
- this.$img = $('<img />').addClass('thumbnail-img').attr('src', thumb.url)
382
- }
383
-
384
- // the container will contain the image positioned so that the correct sprite
385
- // is visible
386
- if (!this.$container) {
387
- this.$container = $('<div />').addClass('thumbnail-container')
388
- }
389
-
390
- this.$container.css('width', thumb.w * scaleFactor)
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 this.$container
298
+ return $container
402
299
  }
403
300
 
404
- private _loadBackdrop() {
405
- if (!this._getOptions().backdropHeight) {
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._$carousel
308
+ const $carousel = this.$el.find('#thumbnails-carousel')
412
309
 
413
- for (const thumb of this._thumbs) {
414
- const $img = this._buildImg(thumb, this._getOptions().backdropHeight)
415
-
416
- // Keep reference to thumbnail
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
- if (this.core.getPlugin('clips')) {
425
- const txt = this.core.getPlugin('clips').getText(time)
426
-
427
- this.$textThumbnail.text(txt)
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 _updateCarousel() {
434
- trace(`${T} _updateCarousel`, {
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 hoverPosition = this._hoverPosition
443
- const videoDuration = this.core.mediaControl.container.getDuration()
444
- const startTimeOffset =
445
- this.core.mediaControl.container.getStartTimeOffset()
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 backdropWidth = this._$backdrop.width()
449
- const $carousel = this._$carousel
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._getThumbIndexForTime(hoverTime)
462
- const thumb = thumbs[thumbIndex]
463
- let thumbDuration = thumb.duration
464
-
465
- if (!thumbDuration) {
466
- // the last thumbnail duration will be null as it can't be determined
467
- // e.g the duration of the video may increase over time (live stream)
468
- // so calculate the duration now so this last thumbnail lasts till the end
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._getOptions().backdropMaxOpacity || 0.6
485
- const minOpacity = this._getOptions().backdropMinOpacity || 0.08
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._$backdropCarouselImgs[i].css('opacity', opacity)
396
+ this.$backdropCarouselImgs[i].css('opacity', opacity)
506
397
  }
507
398
  }
508
399
 
509
- private _updateSpotlightThumb() {
510
- trace(`${T} _updateSpotlightThumb`, {
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 hoverPosition = this._hoverPosition
519
- const videoDuration = this.core.mediaControl.container.getDuration()
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
- this.core.mediaControl.container.getStartTimeOffset()
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._getThumbIndexForTime(hoverTime)
529
- const thumb = this._thumbs[thumbIndex]
415
+ const thumbIndex = this.getThumbIndexForTime(hoverTime)
416
+ const thumb = this.thumbs[thumbIndex]
530
417
 
531
418
  // update thumbnail
532
- const $spotlight = this._$spotlight
419
+ const $spotlight = this.$el.find('#thumbnails-spotlight')
533
420
 
534
- $spotlight.empty()
535
- $spotlight.append(this._buildImg(thumb, this._getOptions().spotlightHeight))
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(Math.min(spotlightXPos, elWidth - thumbWidth), 0)
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.$textThumbnail.css('left', spotlightXPos)
549
- this.$textThumbnail.css('width', thumbWidth)
550
- this.$textThumbnail.css('bottom', thumbHeight + 1)
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 _getThumbIndexForTime(time: TimeValue) {
556
- const thumbs = this._thumbs
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 _renderPlugin() {
571
- trace(`${T} _renderPlugin`, {
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._show && this._thumbs.length > 0) {
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 _createElements() {
589
- trace(`${T} _createElements`)
590
- this.$el.html(
591
- Thumbnails.template({
592
- backdropHeight: this._getOptions().backdropHeight,
593
- spotlightHeight: this._getOptions().spotlightHeight,
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
- // cache dom references
597
- this._$spotlight = this.$el.find('.spotlight')
598
- this._$backdrop = this.$el.find('.backdrop')
599
- this._$carousel = this._$backdrop.find('.carousel')
600
- this.$textThumbnail = this.$el.find('.thumbnails-text')
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
- this._appendElToMediaControl()
506
+
507
+ return this
603
508
  }
604
509
  }