@gcorevideo/player 2.28.30 → 2.28.36

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.
Files changed (37) hide show
  1. package/README.md +22 -1
  2. package/assets/{subtitles → cc}/style.scss +5 -0
  3. package/dist/core.js +17 -23
  4. package/dist/index.css +241 -237
  5. package/dist/index.embed.js +79 -59
  6. package/dist/index.js +137 -119
  7. package/docs/api/player.closedcaptionspluginsettings.md +1 -0
  8. package/docs/api/player.md +9 -0
  9. package/docs/api/player.mediacontrol.md +16 -0
  10. package/docs/api/player.thumbnails.md +1 -1
  11. package/lib/Player.d.ts.map +1 -1
  12. package/lib/playback/BasePlayback.d.ts +1 -0
  13. package/lib/playback/BasePlayback.d.ts.map +1 -1
  14. package/lib/playback/BasePlayback.js +3 -0
  15. package/lib/playback/dash-playback/DashPlayback.d.ts +1 -0
  16. package/lib/playback/dash-playback/DashPlayback.d.ts.map +1 -1
  17. package/lib/playback/dash-playback/DashPlayback.js +9 -22
  18. package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
  19. package/lib/playback/hls-playback/HlsPlayback.js +4 -0
  20. package/lib/plugins/subtitles/ClosedCaptions.d.ts +7 -3
  21. package/lib/plugins/subtitles/ClosedCaptions.d.ts.map +1 -1
  22. package/lib/plugins/subtitles/ClosedCaptions.js +66 -42
  23. package/lib/testUtils.d.ts +1 -0
  24. package/lib/testUtils.d.ts.map +1 -1
  25. package/lib/testUtils.js +3 -0
  26. package/package.json +4 -1
  27. package/src/Player.ts +12 -12
  28. package/src/playback/BasePlayback.ts +4 -0
  29. package/src/playback/dash-playback/DashPlayback.ts +10 -27
  30. package/src/playback/hls-playback/HlsPlayback.ts +4 -0
  31. package/src/plugins/subtitles/ClosedCaptions.ts +75 -47
  32. package/src/plugins/subtitles/__tests__/ClosedCaptions.test.ts +277 -29
  33. package/src/plugins/subtitles/__tests__/__snapshots__/ClosedCaptions.test.ts.snap +3 -3
  34. package/src/testUtils.ts +3 -0
  35. package/tsconfig.tsbuildinfo +1 -1
  36. /package/assets/{subtitles → cc}/combobox.ejs +0 -0
  37. /package/assets/{subtitles → cc}/string.ejs +0 -0
@@ -1,7 +1,7 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest'
1
+ import { beforeEach, describe, expect, it, vi, afterEach } from 'vitest'
2
2
 
3
3
  import { ClosedCaptions } from '../ClosedCaptions.js'
4
- import { createMockCore, createMockMediaControl } from '../../../testUtils.js'
4
+ import { createMockContainer, createMockCore, createMockMediaControl } from '../../../testUtils.js'
5
5
  import { ExtendedEvents } from '../../media-control/MediaControl.js'
6
6
 
7
7
  import { Events } from '@clappr/core'
@@ -16,6 +16,7 @@ describe('ClosedCaptions', () => {
16
16
  let mediaControl: any
17
17
  let cc: ClosedCaptions
18
18
  beforeEach(() => {
19
+ vi.useFakeTimers()
19
20
  core = createMockCore()
20
21
  mediaControl = createMockMediaControl(core)
21
22
  mediaControl.getAvailablePopupHeight = vi.fn().mockReturnValue(211)
@@ -27,8 +28,12 @@ describe('ClosedCaptions', () => {
27
28
  })
28
29
  cc = new ClosedCaptions(core)
29
30
  })
31
+ afterEach(() => {
32
+ vi.useRealTimers()
33
+ })
30
34
  describe('basically', () => {
31
35
  beforeEach(() => {
36
+ core.options.cc = { language: 'none' }
32
37
  core.emit(Events.CORE_READY)
33
38
  core.activePlayback.el = document.createElement('video')
34
39
  core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
@@ -53,6 +58,7 @@ describe('ClosedCaptions', () => {
53
58
  describe("when subtitle tracks are available", () => {
54
59
  beforeEach(() => {
55
60
  emitSubtitleAvailable(core)
61
+ vi.advanceTimersByTime(1)
56
62
  })
57
63
  it('should render', () => {
58
64
  expect(cc.el.innerHTML).toMatchSnapshot()
@@ -64,7 +70,6 @@ describe('ClosedCaptions', () => {
64
70
  width: 320,
65
71
  height: 260,
66
72
  })
67
- // core.emit(Events.CORE_RESIZE, { width: 320, height: 260 })
68
73
  })
69
74
  it('should clamp popup height', () => {
70
75
  expect(cc.$el.find('#gplayer-cc-menu').css('max-height')).toBe('197px')
@@ -131,9 +136,9 @@ describe('ClosedCaptions', () => {
131
136
  })
132
137
  })
133
138
  describe('when subtitle is changed', () => {
134
- beforeEach(async () => {
139
+ beforeEach(() => {
135
140
  cc.$el.find('#gplayer-cc-button').click()
136
- await new Promise((resolve) => setTimeout(resolve, 100))
141
+ vi.advanceTimersByTime(100)
137
142
  core.activePlayback.getCurrentTime = vi.fn().mockReturnValue(7)
138
143
  core.activeContainer.getCurrentTime = vi.fn().mockReturnValue(7)
139
144
  core.activePlayback.closedCaptionsTracks[1].track.cues = [
@@ -169,10 +174,10 @@ describe('ClosedCaptions', () => {
169
174
  },
170
175
  ]
171
176
  cc.$el.find('#gplayer-cc-menu li:nth-child(2) a').click()
172
- await new Promise((resolve) => setTimeout(resolve, 100))
177
+ vi.advanceTimersByTime(100)
173
178
  // TODO test explicitly that PLAYBACK_SUBTITLE_CHANGED event does not cause track switch
174
179
  core.activePlayback.emit(Events.PLAYBACK_SUBTITLE_CHANGED, { id: 2 })
175
- await new Promise((resolve) => setTimeout(resolve, 100))
180
+ vi.advanceTimersByTime(100)
176
181
  })
177
182
  it('should activate selected track', () => {
178
183
  expect(core.activePlayback.closedCaptionsTracks[1].track.mode).toBe(
@@ -193,7 +198,8 @@ describe('ClosedCaptions', () => {
193
198
  cc.$el.find('#gplayer-cc-menu li:nth-child(2) a').click()
194
199
  })
195
200
  it('should activate native subtitles track', () => {
196
- expect(core.activePlayback.closedCaptionsTrackId).toEqual(2)
201
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(1)
202
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(1)
197
203
  })
198
204
  it('should highlight selected menu item', () => {
199
205
  expect(
@@ -254,34 +260,276 @@ describe('ClosedCaptions', () => {
254
260
  })
255
261
  })
256
262
  })
263
+ describe('when language is configured', () => {
264
+ describe("and is set to 'none'", () => {
265
+ beforeEach(() => {
266
+ core.options.cc = { language: 'none' }
267
+ emitSubtitleAvailable(core)
268
+ vi.advanceTimersByTime(1)
269
+ })
270
+ it('should disable subtitles', () => {
271
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(-1)
272
+ expect(
273
+ core.activeContainer.$el.find('#gplayer-cc-line').text().trim(),
274
+ ).toBe('')
275
+ })
276
+ it('should have all tracks disabled', () => {
277
+ expect(core.activePlayback.closedCaptionsTracks[0].track.mode).toBe(
278
+ 'disabled',
279
+ )
280
+ expect(core.activePlayback.closedCaptionsTracks[1].track.mode).toBe(
281
+ 'disabled',
282
+ )
283
+ })
284
+ it('should hide subtitles', () => {
285
+ expect(cc.$el.find('#gplayer-cc-menu').css('display')).toBe('none')
286
+ })
287
+ })
288
+ describe('when language is undefined', () => {
289
+ beforeEach(() => {
290
+ core.options.cc = {}
291
+ core.activePlayback.closedCaptionsTrackId = undefined
292
+ emitSubtitleAvailable(core, 1)
293
+ vi.advanceTimersByTime(1)
294
+ })
295
+ // The native engine will decide
296
+ it('should not automatically select any track', () => {
297
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(undefined)
298
+ expect(core.activePlayback.setTextTrack).not.toHaveBeenCalled()
299
+ })
300
+ it('should not touch any tracks', () => {
301
+ expect(core.activePlayback.el.textTracks[0].mode).toBe('hidden')
302
+ // Track #1 is set active (as if selected by the engine), @see {emitSubtitleAvailable}()
303
+ expect(core.activePlayback.el.textTracks[1].mode).toBe('showing')
304
+ })
305
+ })
306
+ describe('when language matches a track', () => {
307
+ beforeEach(() => {
308
+ core.options.cc = { language: 'en' }
309
+ emitSubtitleAvailable(core)
310
+ vi.advanceTimersByTime(1)
311
+ })
312
+ it('should activate the matching track', () => {
313
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(0)
314
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(0)
315
+ })
316
+ // TODO with options.cc.mode='native' the selected track's mode should be 'showing'
317
+ it('should set the matching track mode appropriately', () => {
318
+ expect(core.activePlayback.el.textTracks[0].mode).toBe('hidden')
319
+ expect(core.activePlayback.el.textTracks[1].mode).toBe('disabled')
320
+ })
321
+ it('should highlight the matching track in menu', () => {
322
+ expect(
323
+ cc.$el.find('#gplayer-cc-menu li:nth-child(1)').hasClass('current'),
324
+ ).toBe(true)
325
+ })
326
+ })
327
+ describe('when language does not match any track', () => {
328
+ beforeEach(() => {
329
+ core.options.cc = { language: 'fr' }
330
+ core.emit(Events.CORE_READY)
331
+ core.activePlayback.el = document.createElement('video')
332
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
333
+ emitSubtitleAvailable(core)
334
+ vi.advanceTimersByTime(1)
335
+ })
336
+ it('should disable subtitles', () => {
337
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(-1)
338
+ })
339
+ it('should have all tracks disabled', () => {
340
+ expect(core.activePlayback.closedCaptionsTracks[0].track.mode).toBe(
341
+ 'disabled',
342
+ )
343
+ expect(core.activePlayback.closedCaptionsTracks[1].track.mode).toBe(
344
+ 'disabled',
345
+ )
346
+ })
347
+ it('should hide subtitle text', () => {
348
+ expect(
349
+ core.activeContainer.$el.find('#gplayer-cc-line').text().trim(),
350
+ ).toBe('')
351
+ })
352
+ })
353
+ })
354
+ describe('when subtitle available event occurs after user selection', () => {
355
+ describe('if user selected track before event', () => {
356
+ beforeEach(() => {
357
+ core.options.cc = { language: 'en' }
358
+ // First subtitle available event
359
+ emitSubtitleAvailable(core)
360
+ vi.advanceTimersByTime(1)
361
+ cc.$el.find('#gplayer-cc-button').click()
362
+ vi.advanceTimersByTime(1)
363
+ // User manually selects Spanish track
364
+ cc.$el.find('#gplayer-cc-menu li:nth-child(2) a').click()
365
+ vi.advanceTimersByTime(1)
366
+
367
+ core.activeContainer = createMockContainer()
368
+ // Reset isPreselectedApplied by emitting container changed
369
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
370
+ // Second subtitle available event (e.g., after source switch)
371
+ emitSubtitleAvailable(core)
372
+ vi.advanceTimersByTime(1)
373
+ })
374
+ it('should run preselected language matching algorithm', () => {
375
+ // Should activate track selected by user
376
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(1)
377
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(1)
378
+ })
379
+ it('should select track based on configured language', () => {
380
+ expect(
381
+ cc.$el.find('#gplayer-cc-menu li:nth-child(2)').hasClass('current'),
382
+ ).toBe(true)
383
+ })
384
+ it('should activate the track matching the selected', () => {
385
+ expect(core.activePlayback.closedCaptionsTracks[0].track.mode).toBe(
386
+ 'disabled',
387
+ )
388
+ // not 'showing' because the plugin manages the subtitles rendition
389
+ expect(core.activePlayback.closedCaptionsTracks[1].track.mode).toBe(
390
+ 'hidden',
391
+ )
392
+ })
393
+ })
394
+ describe('when multiple subtitle available events occur', () => {
395
+ beforeEach(() => {
396
+ core.options.cc = { language: 'en' }
397
+ // First subtitle available event
398
+ emitSubtitleAvailable(core)
399
+ vi.advanceTimersByTime(1)
400
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
401
+ vi.advanceTimersByTime(1)
402
+ core.activePlayback.closedCaptionsTrackId = undefined
403
+ // Second subtitle available event
404
+ emitSubtitleAvailable(core)
405
+ vi.advanceTimersByTime(1)
406
+ })
407
+ it('should reapply preselected language matching and activate the matching track', () => {
408
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(0)
409
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(0)
410
+ expect(
411
+ cc.$el.find('#gplayer-cc-menu li:nth-child(1)').hasClass('current'),
412
+ ).toBe(true)
413
+ })
414
+ it('should activate the matching track', () => {
415
+ expect(core.activePlayback.el.textTracks[0].mode).toBe(
416
+ 'hidden',
417
+ )
418
+ expect(core.activePlayback.el.textTracks[1].mode).toBe(
419
+ 'disabled',
420
+ )
421
+ })
422
+ })
423
+ })
424
+ describe.each([['html5_video', false], ['dash', false], ['hls', true]] as const)(
425
+ 'when playback engine is %s',
426
+ (playbackEngine, isManaged) => {
427
+ beforeEach(() => {
428
+ core.activePlayback.el = document.createElement('video')
429
+ core.activePlayback.name = playbackEngine
430
+ })
431
+ describe('when language is configured and matches a track', () => {
432
+ beforeEach(() => {
433
+ core.options.cc = { language: 'en' }
434
+ emitSubtitleAvailable(core)
435
+ vi.advanceTimersByTime(1)
436
+ })
437
+ it('should call setTextTrack with matching track id', () => {
438
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(0)
439
+ })
440
+ it('should set closedCaptionsTrackId to matching track id', () => {
441
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(0)
442
+ })
443
+ if (isManaged) {
444
+ it('should not touch native tracks', () => {
445
+ expect(core.activePlayback.el.textTracks[0].mode).toBe('hidden')
446
+ expect(core.activePlayback.el.textTracks[1].mode).toBe('hidden')
447
+ })
448
+ } else {
449
+ it('should disable inactive native track', () => {
450
+ expect(core.activePlayback.el.textTracks[1].mode).toBe('disabled')
451
+ })
452
+ it('should keep active native track hidden', () => {
453
+ expect(core.activePlayback.el.textTracks[0].mode).toBe('hidden')
454
+ })
455
+ }
456
+ })
457
+ describe('when language is configured but does not match any track', () => {
458
+ beforeEach(() => {
459
+ core.options.cc = { language: 'fr' }
460
+ core.emit(Events.CORE_READY)
461
+ core.activePlayback.el = document.createElement('video')
462
+ core.activePlayback.name = playbackEngine
463
+ core.activePlayback.setTextTrack = vi.fn()
464
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
465
+ emitSubtitleAvailable(core)
466
+ vi.advanceTimersByTime(1)
467
+ })
468
+ it('should disable subtitles', () => {
469
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(-1)
470
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(-1)
471
+ })
472
+ })
473
+ describe('when user selects a track from menu', () => {
474
+ beforeEach(() => {
475
+ core.activePlayback.setTextTrack = vi.fn()
476
+ emitSubtitleAvailable(core)
477
+ vi.advanceTimersByTime(1)
478
+ cc.$el.find('#gplayer-cc-menu li:nth-child(2) a').click()
479
+ vi.advanceTimersByTime(1)
480
+ })
481
+ it('should call setTextTrack with selected track id', () => {
482
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(1)
483
+ })
484
+ it('should update closedCaptionsTrackId', () => {
485
+ expect(core.activePlayback.closedCaptionsTrackId).toEqual(1)
486
+ })
487
+ })
488
+ describe('when language is set to none', () => {
489
+ beforeEach(() => {
490
+ core.options.cc = { language: 'none' }
491
+ core.emit(Events.CORE_READY)
492
+ core.activePlayback.el = document.createElement('video')
493
+ core.activePlayback.name = playbackEngine
494
+ core.activePlayback.setTextTrack = vi.fn()
495
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
496
+ emitSubtitleAvailable(core)
497
+ vi.advanceTimersByTime(1)
498
+ })
499
+ it('should disable subtitles', () => {
500
+ expect(core.activePlayback.setTextTrack).toHaveBeenCalledWith(-1)
501
+ })
502
+ })
503
+ },
504
+ )
257
505
  })
258
506
  })
259
507
 
260
- function emitSubtitleAvailable(core: any) {
261
- core.activePlayback.closedCaptionsTracks = [
508
+ function emitSubtitleAvailable(core: any, selectedTrackId?: number) {
509
+ const domTracks = [
262
510
  {
263
- id: 1,
264
- name: 'English',
265
- track: {
266
- language: 'en',
267
- kind: 'subtitles',
268
- label: 'English',
269
- mode: 'hidden',
270
- cues: [],
271
- },
511
+ language: 'en',
512
+ kind: 'subtitles',
513
+ label: 'English',
514
+ mode: selectedTrackId === 0 ? 'showing' : 'hidden',
515
+ cues: [],
516
+ id: "01en"
272
517
  },
273
518
  {
274
- id: 2,
275
- name: 'Spanish',
276
- track: {
277
- language: 'es',
278
- kind: 'subtitles',
279
- label: 'Spanish',
280
- mode: 'hidden',
281
- cues: [],
282
- },
283
- },
519
+ language: 'es',
520
+ kind: 'subtitles',
521
+ label: 'Español',
522
+ mode: selectedTrackId === 1 ? 'showing' : 'hidden',
523
+ cues: [],
524
+ id: "02es"
525
+ }
284
526
  ]
527
+ core.activePlayback.closedCaptionsTracks = domTracks.map((t, index) => ({
528
+ id: index,
529
+ name: t.label,
530
+ track: t,
531
+ }))
532
+ vi.spyOn(core.activePlayback.el, 'textTracks', 'get').mockReturnValue(domTracks)
285
533
  core.activePlayback.emit(Events.PLAYBACK_SUBTITLE_AVAILABLE)
286
534
  core.activeContainer.emit(Events.CONTAINER_SUBTITLE_AVAILABLE)
287
535
  }
@@ -6,14 +6,14 @@ exports[`ClosedCaptions > basically > when subtitle tracks are available > shoul
6
6
  <ul class="gcore-skin-bg-color media-control-dd__popup" id="gplayer-cc-menu" role="menu" style="display: none; max-height: 211px;">
7
7
 
8
8
  <li class="">
9
- <a href="#" class="gcore-skin-text-color" data-item="1" role="menuitemradio" aria-checked="false">
9
+ <a href="#" class="gcore-skin-text-color" data-item="0" role="menuitemradio" aria-checked="false">
10
10
  English
11
11
  </a>
12
12
  </li>
13
13
 
14
14
  <li class="">
15
- <a href="#" class="gcore-skin-text-color" data-item="2" role="menuitemradio" aria-checked="false">
16
- Spanish
15
+ <a href="#" class="gcore-skin-text-color" data-item="1" role="menuitemradio" aria-checked="false">
16
+ Español
17
17
  </a>
18
18
  </li>
19
19
 
package/src/testUtils.ts CHANGED
@@ -81,6 +81,7 @@ export function createMockPlayback(
81
81
  canAutoPlay: vi.fn().mockImplementation(() => true),
82
82
  onResize: vi.fn().mockImplementation(() => true),
83
83
  setPlaybackRate: vi.fn(),
84
+ setTextTrack: vi.fn(),
84
85
  switchAudioTrack: vi.fn(),
85
86
  trigger: emitter.emit,
86
87
  })
@@ -139,6 +140,8 @@ export function createMockMediaControl(core: any) {
139
140
  // @ts-ignore
140
141
  mediaControl.getAvailablePopupHeight = vi.fn().mockReturnValue(286)
141
142
  // @ts-ignore
143
+ mediaControl.isVisible = vi.fn().mockReturnValue(true)
144
+ // @ts-ignore
142
145
  mediaControl.toggleElement = vi.fn()
143
146
  // @ts-ignore
144
147
  mediaControl.setKeepVisible = vi.fn()