@gcorevideo/player 2.30.0 → 2.30.2

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 (40) hide show
  1. package/assets/audio-tracks/template.ejs +1 -1
  2. package/dist/core.js +65 -21
  3. package/dist/index.css +265 -265
  4. package/dist/index.embed.js +99 -34
  5. package/dist/index.js +125 -53
  6. package/docs/api/player.md +37 -0
  7. package/docs/api/player.player.getplugin.md +59 -0
  8. package/docs/api/player.player.md +14 -0
  9. package/docs/api/player.tokenrefreshoptions.gettoken.md +13 -0
  10. package/docs/api/player.tokenrefreshoptions.ipbound.md +13 -0
  11. package/docs/api/player.tokenrefreshoptions.md +115 -0
  12. package/docs/api/player.tokenrefreshoptions.ontokenrefreshed.md +13 -0
  13. package/docs/api/player.tokenrefreshoptions.refreshleadseconds.md +13 -0
  14. package/docs/api/player.tokenrefreshplugin.md +50 -0
  15. package/docs/api/player.tokenresponse.client_ip.md +13 -0
  16. package/docs/api/player.tokenresponse.expires.md +13 -0
  17. package/docs/api/player.tokenresponse.md +153 -0
  18. package/docs/api/player.tokenresponse.token.md +13 -0
  19. package/docs/api/player.tokenresponse.token_ip.md +13 -0
  20. package/docs/api/player.tokenresponse.url.md +13 -0
  21. package/docs/api/player.tokenresponse.url_ip.md +13 -0
  22. package/lib/playback/hls-playback/HlsPlayback.d.ts +1 -1
  23. package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
  24. package/lib/playback/hls-playback/HlsPlayback.js +23 -20
  25. package/lib/playback/hls-playback/RangesList.d.ts +7 -0
  26. package/lib/playback/hls-playback/RangesList.d.ts.map +1 -0
  27. package/lib/playback/hls-playback/RangesList.js +41 -0
  28. package/lib/plugins/audio-selector/AudioTracks.d.ts +4 -0
  29. package/lib/plugins/audio-selector/AudioTracks.d.ts.map +1 -1
  30. package/lib/plugins/audio-selector/AudioTracks.js +42 -12
  31. package/lib/plugins/subtitles/ClosedCaptions.d.ts.map +1 -1
  32. package/lib/plugins/subtitles/ClosedCaptions.js +0 -2
  33. package/package.json +1 -1
  34. package/src/playback/hls-playback/HlsPlayback.ts +40 -37
  35. package/src/playback/hls-playback/RangesList.ts +45 -0
  36. package/src/playback/hls-playback/__tests__/RangesList.test.ts +60 -0
  37. package/src/plugins/audio-selector/AudioTracks.ts +51 -16
  38. package/src/plugins/audio-selector/__tests__/__snapshots__/AudioTracks.test.ts.snap +9 -9
  39. package/src/plugins/subtitles/ClosedCaptions.ts +0 -5
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -1,13 +1,13 @@
1
1
  import { Events, UICorePlugin, template } from '@clappr/core';
2
2
  import assert from 'assert';
3
- // import { trace } from '@gcorevideo/utils'
3
+ import { trace } from '@gcorevideo/utils';
4
4
  import { CLAPPR_VERSION } from '../../build.js';
5
5
  import pluginHtml from '../../../assets/audio-tracks/template.ejs';
6
6
  import audioArrow from '../../../assets/icons/old/quality-arrow.svg';
7
7
  import { ExtendedEvents } from '../media-control/MediaControl.js';
8
8
  import { mediaControlClickaway } from '../../utils/clickaway.js';
9
9
  const VERSION = '2.22.4';
10
- // const T = 'plugins.audiotracks'
10
+ const T = 'plugins.audiotracks';
11
11
  /**
12
12
  * `PLUGIN` that makes possible to switch audio tracks via the media control UI.
13
13
  * @public
@@ -22,6 +22,7 @@ export class AudioTracks extends UICorePlugin {
22
22
  currentTrack = null;
23
23
  open = false;
24
24
  tracks = [];
25
+ autoUpdateTimerId = null;
25
26
  /**
26
27
  * @internal
27
28
  */
@@ -82,19 +83,21 @@ export class AudioTracks extends UICorePlugin {
82
83
  onActiveContainerChanged() {
83
84
  this.currentTrack = null;
84
85
  this.listenTo(this.core.activeContainer, Events.CONTAINER_AUDIO_AVAILABLE, (tracks) => {
86
+ trace(`${T} on Events.CONTAINER_AUDIO_AVAILABLE`, {
87
+ tracks,
88
+ });
89
+ const currentTrackId = this.core.activeContainer.currentAudioTrack?.id ??
90
+ this.core.activePlayback?.currentAudioTrack?.id;
85
91
  this.currentTrack =
86
- tracks.find((track) => track.kind === 'main') ?? null; // TODO test
92
+ tracks.find((track) => track.id === currentTrackId) ??
93
+ tracks.find((track) => track.kind === 'main') ??
94
+ tracks[0] ??
95
+ null;
87
96
  this.tracks = tracks;
88
97
  this.render();
89
98
  this.mount();
90
99
  });
91
- this.listenTo(this.core.activeContainer, Events.CONTAINER_AUDIO_CHANGED, (track) => {
92
- this.currentTrack = track;
93
- this.highlightCurrentTrack();
94
- this.buttonElement().removeClass('changing');
95
- this.updateText();
96
- });
97
- // TODO test
100
+ this.bindContainerAudioChanged();
98
101
  this.listenTo(this.core.activeContainer, Events.CONTAINER_CLICK, () => {
99
102
  this.hideMenu();
100
103
  });
@@ -102,6 +105,24 @@ export class AudioTracks extends UICorePlugin {
102
105
  this.clickaway(null);
103
106
  });
104
107
  }
108
+ bindContainerAudioChanged() {
109
+ this.listenTo(this.core.activeContainer, Events.CONTAINER_AUDIO_CHANGED, (track) => {
110
+ trace(`${T} on Events.CONTAINER_AUDIO_CHANGED`, {
111
+ track,
112
+ });
113
+ this.setCurrentTrack(track);
114
+ });
115
+ }
116
+ setCurrentTrack(track) {
117
+ if (this.autoUpdateTimerId) {
118
+ clearTimeout(this.autoUpdateTimerId);
119
+ this.autoUpdateTimerId = null;
120
+ }
121
+ this.currentTrack = track;
122
+ this.highlightCurrentTrack();
123
+ this.buttonElement().removeClass('changing');
124
+ this.updateText();
125
+ }
105
126
  shouldRender() {
106
127
  // Render is called from the parent class constructor so tracks aren't available
107
128
  // Only care if we have at least 2 to choose from
@@ -134,7 +155,7 @@ export class AudioTracks extends UICorePlugin {
134
155
  selectAudioTrack(id) {
135
156
  this.startTrackSwitching();
136
157
  this.core.activeContainer.switchAudioTrack(id);
137
- this.updateText();
158
+ this.autoUpdateSelected(id);
138
159
  }
139
160
  hideMenu() {
140
161
  this.open = false;
@@ -185,7 +206,7 @@ export class AudioTracks extends UICorePlugin {
185
206
  if (!this.currentTrack) {
186
207
  return;
187
208
  }
188
- this.buttonElementText().text(this.currentTrack.label);
209
+ this.buttonElementText().text(this.getTitle());
189
210
  }
190
211
  highlightCurrentTrack() {
191
212
  this.trackElement().removeClass('current');
@@ -206,5 +227,14 @@ export class AudioTracks extends UICorePlugin {
206
227
  this.core.getPlugin('media_control')?.slot('audiotracks', this.$el);
207
228
  }
208
229
  }
230
+ autoUpdateSelected(id) {
231
+ if (this.autoUpdateTimerId) {
232
+ clearTimeout(this.autoUpdateTimerId);
233
+ }
234
+ this.autoUpdateTimerId = setTimeout(() => {
235
+ const track = this.tracks.find(t => t.id === id) ?? null;
236
+ this.setCurrentTrack(track);
237
+ }, 500);
238
+ }
209
239
  clickaway = mediaControlClickaway(() => this.hideMenu());
210
240
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ClosedCaptions.d.ts","sourceRoot":"","sources":["../../../src/plugins/subtitles/ClosedCaptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,YAAY,EAAwB,MAAM,cAAc,CAAA;AAOzE,OAAO,+BAA+B,CAAA;AAmBtC;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAC3B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,iBAAiB,CAAQ;IAEjC,OAAO,CAAC,MAAM,CAAQ;IAEtB,OAAO,CAAC,IAAI,CAAQ;IAEpB,OAAO,CAAC,kBAAkB,CAAa;IAEvC,OAAO,CAAC,KAAK,CAA6B;IAE1C,OAAO,CAAC,MAAM,CAAsB;IAEpC,OAAO,CAAC,KAAK,CAA2B;IAExC;;OAEG;IACH,IAAI,IAAI,WAEP;IAED;;OAEG;IACH,IAAI,gBAAgB;;MAEnB;IAED;;OAEG;IACH,MAAM,KAAK,OAAO,WAEjB;IAED,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAyB;IAEhE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAuB;IAE3D;;OAEG;IACH,IAAa,UAAU;;MAItB;IAED;;OAEG;IACH,IAAa,MAAM;;;MAKlB;IAED,OAAO,KAAK,mBAAmB,GAI9B;IAED,OAAO,CAAC,qBAAqB;IAO7B;;OAEG;IACM,UAAU;IAWnB,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,kBAAkB;IAiD1B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,iBAAiB;IAqBzB;;OAEG;IACH,IAAI;IAcJ;;OAEG;IACH,IAAI;IAgBJ,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACM,MAAM;IAmCf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,YAAY;IAepB,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,yBAAyB;IAiBjC,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,KAAK;IAOb,OAAO,KAAK,kBAAkB,GAO7B;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,YAAY,QAEvB;IAED,OAAO,KAAK,aAAa,QAMxB;IAED,OAAO,CAAC,SAAS,CAA+C;CACjE"}
1
+ {"version":3,"file":"ClosedCaptions.d.ts","sourceRoot":"","sources":["../../../src/plugins/subtitles/ClosedCaptions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,YAAY,EAAwB,MAAM,cAAc,CAAA;AAOzE,OAAO,+BAA+B,CAAA;AAgBtC;;;GAGG;AACH,MAAM,MAAM,4BAA4B,GAAG;IACzC;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAC3B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,OAAO,CAAC,iBAAiB,CAAQ;IAEjC,OAAO,CAAC,MAAM,CAAQ;IAEtB,OAAO,CAAC,IAAI,CAAQ;IAEpB,OAAO,CAAC,kBAAkB,CAAa;IAEvC,OAAO,CAAC,KAAK,CAA6B;IAE1C,OAAO,CAAC,MAAM,CAAsB;IAEpC,OAAO,CAAC,KAAK,CAA2B;IAExC;;OAEG;IACH,IAAI,IAAI,WAEP;IAED;;OAEG;IACH,IAAI,gBAAgB;;MAEnB;IAED;;OAEG;IACH,MAAM,KAAK,OAAO,WAEjB;IAED,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAyB;IAEhE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAuB;IAE3D;;OAEG;IACH,IAAa,UAAU;;MAItB;IAED;;OAEG;IACH,IAAa,MAAM;;;MAKlB;IAED,OAAO,KAAK,mBAAmB,GAI9B;IAED,OAAO,CAAC,qBAAqB;IAO7B;;OAEG;IACM,UAAU;IAWnB,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,kBAAkB;IAiD1B,OAAO,CAAC,eAAe;IASvB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,iBAAiB;IAqBzB;;OAEG;IACH,IAAI;IAcJ;;OAEG;IACH,IAAI;IAgBJ,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACM,MAAM;IAmCf,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,UAAU;IAWlB,OAAO,CAAC,YAAY;IAapB,OAAO,CAAC,sBAAsB;IAwB9B,OAAO,CAAC,QAAQ;IAOhB,OAAO,CAAC,UAAU;IAclB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,eAAe;IAUvB,OAAO,CAAC,yBAAyB;IAiBjC,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,UAAU;IAOlB,OAAO,CAAC,KAAK;IAOb,OAAO,KAAK,kBAAkB,GAO7B;IAED,OAAO,KAAK,aAAa,GAExB;IAED,OAAO,KAAK,YAAY,GAEvB;IAED,OAAO,KAAK,YAAY,QAEvB;IAED,OAAO,KAAK,aAAa,QAMxB;IAED,OAAO,CAAC,SAAS,CAA+C;CACjE"}
@@ -331,8 +331,6 @@ export class ClosedCaptions extends UICorePlugin {
331
331
  // event.target does not exist for some reason in tests
332
332
  const id = Number((event.target ?? event.currentTarget).dataset?.item ??
333
333
  '-1');
334
- // TODO review, make configurable, and emit event in addition
335
- // localStorage.setItem(LOCAL_STORAGE_CC_ID, id) // TODO store language instead?
336
334
  this.userSelectedItemId = id;
337
335
  this.selectItem(this.findById(id));
338
336
  this.hideMenu();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcorevideo/player",
3
- "version": "2.30.0",
3
+ "version": "2.30.2",
4
4
  "description": "Gcore JavaScript video player",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -35,6 +35,7 @@ import { BasePlayback } from '../BasePlayback.js'
35
35
  import { CLAPPR_VERSION } from '../../build.js'
36
36
  import { AudioTrack } from '@clappr/core/types/base/playback/playback.js'
37
37
  import { VTTCueInfo } from '../types.js'
38
+ import { RangesList } from './RangesList.js'
38
39
 
39
40
  const { now } = Utils
40
41
 
@@ -70,8 +71,6 @@ type CustomListener = {
70
71
  }
71
72
 
72
73
  export default class HlsPlayback extends BasePlayback {
73
- private _ccTracksUpdated = false
74
-
75
74
  private _currentFragment: Fragment | null = null
76
75
 
77
76
  private _currentLevel: number | null = null
@@ -120,7 +119,9 @@ export default class HlsPlayback extends BasePlayback {
120
119
 
121
120
  oncueexit: ((e: { id: string }) => void) | null = null
122
121
 
123
- private cues: VTTCueInfo[] = [] // TODO check the list size and use BST if needed
122
+ private cues: RangesList<VTTCueInfo> | null = null
123
+
124
+ private cuesByTrack: Record<number, RangesList<VTTCueInfo>> = {}
124
125
 
125
126
  private currentCueId: string | null = null
126
127
 
@@ -310,7 +311,7 @@ export default class HlsPlayback extends BasePlayback {
310
311
  // added/removed every 5.
311
312
  this._extrapolatedWindowNumSegments =
312
313
  !this.options.playback ||
313
- typeof this.options.playback.extrapolatedWindowNumSegments === 'undefined'
314
+ typeof this.options.playback.extrapolatedWindowNumSegments === 'undefined'
314
315
  ? 2
315
316
  : this.options.playback.extrapolatedWindowNumSegments
316
317
 
@@ -362,7 +363,6 @@ export default class HlsPlayback extends BasePlayback {
362
363
  }
363
364
  this._manifestParsed = false
364
365
  // this._ccIsSetup = false
365
- this._ccTracksUpdated = false
366
366
  this._setInitialState()
367
367
  this._hls.destroy()
368
368
  this._hls = null
@@ -436,9 +436,7 @@ export default class HlsPlayback extends BasePlayback {
436
436
  (evt: HlsEvents.LEVEL_SWITCHED, data: { level: number }) =>
437
437
  this._onLevelSwitched(evt, data),
438
438
  )
439
- this._hls.on(HlsEvents.ERROR, (evt, data) =>
440
- this._onHLSJSError(evt, data),
441
- )
439
+ this._hls.on(HlsEvents.ERROR, (evt, data) => this._onHLSJSError(evt, data))
442
440
  this._hls.on(HlsEvents.AUDIO_TRACKS_UPDATED, (evt, data) =>
443
441
  this._onAudioTracksUpdated(evt, data),
444
442
  )
@@ -447,12 +445,20 @@ export default class HlsPlayback extends BasePlayback {
447
445
  )
448
446
  this._hls.on(HlsEvents.CUES_PARSED, (evt, data) => {
449
447
  data.cues?.forEach((cue: any) => {
450
- this.cues.push({
448
+ if (!this.cues) {
449
+ const trackId = this._hls!.subtitleTrack
450
+ if (!this.cuesByTrack[trackId]) {
451
+ this.cuesByTrack[trackId] = new RangesList<VTTCueInfo>()
452
+ }
453
+ this.cues = this.cuesByTrack[trackId]
454
+ }
455
+ const cueInfo = {
451
456
  id: cue.id,
452
457
  start: cue.startTime,
453
458
  end: cue.endTime,
454
459
  text: cue.text,
455
- } as VTTCueInfo)
460
+ } as VTTCueInfo
461
+ this.cues.insert(cue.startTime, cue.endTime, cueInfo)
456
462
  })
457
463
  })
458
464
  this.bindCustomListeners()
@@ -514,7 +520,7 @@ export default class HlsPlayback extends BasePlayback {
514
520
  }
515
521
 
516
522
  // this playback manages the src on the video element itself
517
- protected override _setupSrc(srcUrl: string) { } // eslint-disable-line no-unused-vars
523
+ protected override _setupSrc(srcUrl: string) {} // eslint-disable-line no-unused-vars
518
524
 
519
525
  private _startTimeUpdateTimer() {
520
526
  if (this._timeUpdateTimer) {
@@ -579,7 +585,7 @@ export default class HlsPlayback extends BasePlayback {
579
585
  // assume live if time within 3 seconds of end of stream
580
586
  this.dvrEnabled && this._updateDvr(time < this.getDuration() - 3)
581
587
  time += this._startTime
582
- ; (this.el as HTMLMediaElement).currentTime = time
588
+ ;(this.el as HTMLMediaElement).currentTime = time
583
589
 
584
590
  this.triggerCues()
585
591
  }
@@ -719,7 +725,7 @@ export default class HlsPlayback extends BasePlayback {
719
725
  }
720
726
 
721
727
  private reload() {
722
- this.cues = []
728
+ this.cues = null
723
729
  this.currentCueId = null
724
730
  this._hls?.startLoad(-1)
725
731
  }
@@ -777,12 +783,12 @@ export default class HlsPlayback extends BasePlayback {
777
783
  start: Math.max(
778
784
  0,
779
785
  (this.el as HTMLMediaElement).buffered.start(i) -
780
- this._playableRegionStartTime,
786
+ this._playableRegionStartTime,
781
787
  ),
782
788
  end: Math.max(
783
789
  0,
784
790
  (this.el as HTMLMediaElement).buffered.end(i) -
785
- this._playableRegionStartTime,
791
+ this._playableRegionStartTime,
786
792
  ),
787
793
  },
788
794
  ]
@@ -804,9 +810,7 @@ export default class HlsPlayback extends BasePlayback {
804
810
 
805
811
  private triggerCues() {
806
812
  const currentTime = this.getCurrentTime()
807
- // const cues = Object.values(this.cues)
808
- // TODO build a search tree
809
- const cue = this.cues.find((cue: VTTCueInfo) => currentTime >= cue.start && currentTime <= cue.end)
813
+ const cue = this.cues?.find(currentTime)
810
814
  if (cue) {
811
815
  this.currentCueId = cue.id
812
816
  this.oncueenter?.(cue)
@@ -836,7 +840,7 @@ export default class HlsPlayback extends BasePlayback {
836
840
  if (!this._hls) {
837
841
  return
838
842
  }
839
- ; (this.el as HTMLMediaElement).pause()
843
+ ;(this.el as HTMLMediaElement).pause()
840
844
  if (this.dvrEnabled) {
841
845
  this._updateDvr(true)
842
846
  }
@@ -853,6 +857,8 @@ export default class HlsPlayback extends BasePlayback {
853
857
  destroy() {
854
858
  this._stopTimeUpdateTimer()
855
859
  this._destroyHLSInstance()
860
+ this.cues = null
861
+ this.cuesByTrack = {}
856
862
  return super.destroy()
857
863
  }
858
864
 
@@ -865,14 +871,6 @@ export default class HlsPlayback extends BasePlayback {
865
871
  data.details.live ? Playback.LIVE : Playback.VOD
866
872
  ) as PlaybackType
867
873
  this._onLevelUpdated(evt, data)
868
- // Live stream subtitle tracks detection hack (may not immediately available)
869
- // if (
870
- // this._ccTracksUpdated &&
871
- // this._playbackType === Playback.LIVE &&
872
- // this.hasClosedCaptionsTracks
873
- // ) {
874
- // this._onSubtitleLoaded()
875
- // }
876
874
  if (prevPlaybackType !== this._playbackType) {
877
875
  this._updateSettings()
878
876
  }
@@ -952,7 +950,7 @@ export default class HlsPlayback extends BasePlayback {
952
950
  Math.max(
953
951
  fragments[0].start,
954
952
  previousPlayableRegionStartTime +
955
- this._extrapolatedWindowDuration,
953
+ this._extrapolatedWindowDuration,
956
954
  ) * 1000,
957
955
  }
958
956
  }
@@ -1139,7 +1137,10 @@ export default class HlsPlayback extends BasePlayback {
1139
1137
  return
1140
1138
  }
1141
1139
  this._hls!.subtitleTrack = id
1142
- this.cues = []
1140
+ if (!this.cuesByTrack[id]) {
1141
+ this.cuesByTrack[id] = new RangesList<VTTCueInfo>()
1142
+ }
1143
+ this.cues = this.cuesByTrack[id]
1143
1144
  }
1144
1145
 
1145
1146
  /**
@@ -1150,15 +1151,17 @@ export default class HlsPlayback extends BasePlayback {
1150
1151
  }
1151
1152
 
1152
1153
  getTextTracks() {
1153
- return this._hls?.subtitleTracks.map((t: MediaPlaylist) => ({
1154
- id: t.id,
1155
- name: t.name,
1156
- track: {
1154
+ return (
1155
+ this._hls?.subtitleTracks.map((t: MediaPlaylist) => ({
1157
1156
  id: t.id,
1158
- label: t.name,
1159
- language: t.lang,
1160
- },
1161
- })) || []
1157
+ name: t.name,
1158
+ track: {
1159
+ id: t.id,
1160
+ label: t.name,
1161
+ language: t.lang,
1162
+ },
1163
+ })) || []
1164
+ )
1162
1165
  }
1163
1166
  }
1164
1167
 
@@ -0,0 +1,45 @@
1
+
2
+ export class RangesList<T> {
3
+ // TODO write an efficient implementation
4
+ private items: Array<[number, number, T]> = [];
5
+
6
+ insert(start: number, end: number, value: T) {
7
+ const index = this.findIndex((start + end) / 2);
8
+ this.items.splice(index, 0, [start, end, value]);
9
+ }
10
+
11
+ find(position: number): T | null {
12
+ const index = this.findIndex(position);
13
+ const item = this.items[index];
14
+ if (!item || item[0] > position || item[1] < position) {
15
+ return null;
16
+ }
17
+ return item[2];
18
+ }
19
+
20
+ private findIndex(position: number): number {
21
+ let low = 0;
22
+ let high = this.items.length;
23
+ let index = 0;
24
+ while (low < high) {
25
+ index = low + Math.floor((high - low) / 2);
26
+ const item = this.items[index];
27
+ if (item[0] > position) {
28
+ if (index === low) {
29
+ return index
30
+ }
31
+ high = index;
32
+ continue;
33
+ }
34
+ if (item[1] <= position) {
35
+ if (index === high - 1) {
36
+ return index + 1
37
+ }
38
+ low = index + 1;
39
+ continue;
40
+ }
41
+ break;
42
+ }
43
+ return index;
44
+ }
45
+ }
@@ -0,0 +1,60 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+ import { RangesList } from '../RangesList'
3
+
4
+ describe('RangesList', () => {
5
+ let rs: RangesList<number>
6
+ beforeEach(() => {
7
+ rs = new RangesList()
8
+
9
+ })
10
+ describe("sequential order", () => {
11
+ beforeEach(() => {
12
+ rs.insert(0, 0.5, 1)
13
+ rs.insert(0.5, 1, 2)
14
+ rs.insert(1, 2.5, 3)
15
+ rs.insert(2.5, 3, 4)
16
+ rs.insert(4, 10, 5)
17
+ rs.insert(10, 11, 6)
18
+ })
19
+ it.each([
20
+ [-1, null],
21
+ [0, 1],
22
+ [0.25, 1],
23
+ [0.5, 2],
24
+ [1, 3],
25
+ [2.5, 4],
26
+ [4, 5],
27
+ [10, 6],
28
+ [11, null],
29
+ [-100, null],
30
+ [1020, null],
31
+ ])('%s -> %s', (pos, expVal) => {
32
+ expect(rs.find(pos)).toBe(expVal)
33
+ })
34
+ })
35
+ describe("arbitrary order", () => {
36
+ beforeEach(() => {
37
+ rs.insert(10, 11, 6)
38
+ rs.insert(2.5, 3, 4)
39
+ rs.insert(1, 2.5, 3)
40
+ rs.insert(0.5, 1, 2)
41
+ rs.insert(4, 10, 5)
42
+ rs.insert(0, 0.5, 1)
43
+ })
44
+ it.each([
45
+ [-1, null],
46
+ [0, 1],
47
+ [0.25, 1],
48
+ [0.5, 2],
49
+ [1, 3],
50
+ [2.5, 4],
51
+ [4, 5],
52
+ [10, 6],
53
+ [11, null],
54
+ [-100, null],
55
+ [1020, null],
56
+ ])('%s -> %s', (pos, expVal) => {
57
+ expect(rs.find(pos)).toBe(expVal)
58
+ })
59
+ })
60
+ })
@@ -1,7 +1,7 @@
1
1
  import { Events, UICorePlugin, template } from '@clappr/core'
2
2
  import { AudioTrack } from '@clappr/core/types/base/playback/playback.js'
3
3
  import assert from 'assert'
4
- // import { trace } from '@gcorevideo/utils'
4
+ import { trace } from '@gcorevideo/utils'
5
5
 
6
6
  import { CLAPPR_VERSION } from '../../build.js'
7
7
 
@@ -13,7 +13,7 @@ import { mediaControlClickaway } from '../../utils/clickaway.js'
13
13
 
14
14
  const VERSION: string = '2.22.4'
15
15
 
16
- // const T = 'plugins.audiotracks'
16
+ const T = 'plugins.audiotracks'
17
17
 
18
18
  /**
19
19
  * `PLUGIN` that makes possible to switch audio tracks via the media control UI.
@@ -32,6 +32,8 @@ export class AudioTracks extends UICorePlugin {
32
32
 
33
33
  private tracks: AudioTrack[] = []
34
34
 
35
+ private autoUpdateTimerId: ReturnType<typeof setTimeout> | null = null
36
+
35
37
  /**
36
38
  * @internal
37
39
  */
@@ -112,24 +114,23 @@ export class AudioTracks extends UICorePlugin {
112
114
  this.core.activeContainer,
113
115
  Events.CONTAINER_AUDIO_AVAILABLE,
114
116
  (tracks: AudioTrack[]) => {
117
+ trace(`${T} on Events.CONTAINER_AUDIO_AVAILABLE`, {
118
+ tracks,
119
+ })
120
+ const currentTrackId =
121
+ this.core.activeContainer.currentAudioTrack?.id ??
122
+ this.core.activePlayback?.currentAudioTrack?.id
115
123
  this.currentTrack =
116
- tracks.find((track) => track.kind === 'main') ?? null // TODO test
124
+ tracks.find((track) => track.id === currentTrackId) ??
125
+ tracks.find((track) => track.kind === 'main') ??
126
+ tracks[0] ??
127
+ null
117
128
  this.tracks = tracks
118
129
  this.render()
119
130
  this.mount()
120
131
  },
121
132
  )
122
- this.listenTo(
123
- this.core.activeContainer,
124
- Events.CONTAINER_AUDIO_CHANGED,
125
- (track: AudioTrack) => {
126
- this.currentTrack = track
127
- this.highlightCurrentTrack()
128
- this.buttonElement().removeClass('changing')
129
- this.updateText()
130
- },
131
- )
132
- // TODO test
133
+ this.bindContainerAudioChanged()
133
134
  this.listenTo(this.core.activeContainer, Events.CONTAINER_CLICK, () => {
134
135
  this.hideMenu()
135
136
  })
@@ -138,6 +139,30 @@ export class AudioTracks extends UICorePlugin {
138
139
  })
139
140
  }
140
141
 
142
+ private bindContainerAudioChanged() {
143
+ this.listenTo(
144
+ this.core.activeContainer,
145
+ Events.CONTAINER_AUDIO_CHANGED,
146
+ (track: AudioTrack) => {
147
+ trace(`${T} on Events.CONTAINER_AUDIO_CHANGED`, {
148
+ track,
149
+ })
150
+ this.setCurrentTrack(track)
151
+ }
152
+ )
153
+ }
154
+
155
+ private setCurrentTrack(track: AudioTrack | null) {
156
+ if (this.autoUpdateTimerId) {
157
+ clearTimeout(this.autoUpdateTimerId)
158
+ this.autoUpdateTimerId = null
159
+ }
160
+ this.currentTrack = track
161
+ this.highlightCurrentTrack()
162
+ this.buttonElement().removeClass('changing')
163
+ this.updateText()
164
+ }
165
+
141
166
  private shouldRender() {
142
167
  // Render is called from the parent class constructor so tracks aren't available
143
168
  // Only care if we have at least 2 to choose from
@@ -176,7 +201,7 @@ export class AudioTracks extends UICorePlugin {
176
201
  private selectAudioTrack(id: string) {
177
202
  this.startTrackSwitching()
178
203
  this.core.activeContainer.switchAudioTrack(id)
179
- this.updateText()
204
+ this.autoUpdateSelected(id)
180
205
  }
181
206
 
182
207
  private hideMenu() {
@@ -241,7 +266,7 @@ export class AudioTracks extends UICorePlugin {
241
266
  if (!this.currentTrack) {
242
267
  return
243
268
  }
244
- this.buttonElementText().text(this.currentTrack.label)
269
+ this.buttonElementText().text(this.getTitle())
245
270
  }
246
271
 
247
272
  private highlightCurrentTrack() {
@@ -266,5 +291,15 @@ export class AudioTracks extends UICorePlugin {
266
291
  }
267
292
  }
268
293
 
294
+ private autoUpdateSelected(id: string) {
295
+ if (this.autoUpdateTimerId) {
296
+ clearTimeout(this.autoUpdateTimerId)
297
+ }
298
+ this.autoUpdateTimerId = setTimeout(() => {
299
+ const track = this.tracks.find(t => t.id === id) ?? null
300
+ this.setCurrentTrack(track)
301
+ }, 500)
302
+ }
303
+
269
304
  private clickaway = mediaControlClickaway(() => this.hideMenu())
270
305
  }
@@ -2,13 +2,13 @@
2
2
 
3
3
  exports[`AudioTracks > when audio tracks are available > should render menu 1`] = `
4
4
  "<button class="gcore-skin-button-color media-control-dd" id="gplayer-audiotracks-button" aria-haspopup="menu" aria-expanded="false">
5
- <span class="media-control-dd__text" id="gplayer-audiotracks-button-text"></span>
5
+ <span class="media-control-dd__text" id="gplayer-audiotracks-button-text">English</span>
6
6
  <span class="media-control-dd__arrow">/assets/icons/old/quality-arrow.svg</span>
7
7
  </button>
8
8
  <ul class="gcore-skin-bg-color menu media-control-dd__popup" id="gplayer-audiotracks-menu" role="menu" style="display: none;">
9
9
 
10
- <li class="">
11
- <a href="#" class="gcore-skin-text-color" data-item="1" role="menuitemradio" aria-checked="false">
10
+ <li class="current">
11
+ <a href="#" class="gcore-skin-text-color gcore-skin-active" data-item="1" role="menuitemradio" aria-checked="true">
12
12
  English
13
13
  </a>
14
14
  </li>
@@ -25,13 +25,13 @@ exports[`AudioTracks > when audio tracks are available > should render menu 1`]
25
25
 
26
26
  exports[`AudioTracks > when audio tracks are available > when button is clicked > should show menu 1`] = `
27
27
  "<button class="gcore-skin-button-color media-control-dd" id="gplayer-audiotracks-button" aria-haspopup="menu" aria-expanded="true">
28
- <span class="media-control-dd__text" id="gplayer-audiotracks-button-text"></span>
28
+ <span class="media-control-dd__text" id="gplayer-audiotracks-button-text">English</span>
29
29
  <span class="media-control-dd__arrow">/assets/icons/old/quality-arrow.svg</span>
30
30
  </button>
31
31
  <ul class="gcore-skin-bg-color menu media-control-dd__popup" id="gplayer-audiotracks-menu" role="menu">
32
32
 
33
- <li class="">
34
- <a href="#" class="gcore-skin-text-color" data-item="1" role="menuitemradio" aria-checked="false">
33
+ <li class="current">
34
+ <a href="#" class="gcore-skin-text-color gcore-skin-active" data-item="1" role="menuitemradio" aria-checked="true">
35
35
  English
36
36
  </a>
37
37
  </li>
@@ -48,13 +48,13 @@ exports[`AudioTracks > when audio tracks are available > when button is clicked
48
48
 
49
49
  exports[`AudioTracks > when audio tracks are available > when button is clicked > when audio track is selected > should hide the menu 1`] = `
50
50
  "<button class="gcore-skin-button-color media-control-dd changing" id="gplayer-audiotracks-button" aria-haspopup="menu" aria-expanded="false">
51
- <span class="media-control-dd__text" id="gplayer-audiotracks-button-text"></span>
51
+ <span class="media-control-dd__text" id="gplayer-audiotracks-button-text">English</span>
52
52
  <span class="media-control-dd__arrow">/assets/icons/old/quality-arrow.svg</span>
53
53
  </button>
54
54
  <ul class="gcore-skin-bg-color menu media-control-dd__popup" id="gplayer-audiotracks-menu" role="menu" style="display: none;">
55
55
 
56
- <li class="">
57
- <a href="#" class="gcore-skin-text-color" data-item="1" role="menuitemradio" aria-checked="false">
56
+ <li class="current">
57
+ <a href="#" class="gcore-skin-text-color gcore-skin-active" data-item="1" role="menuitemradio" aria-checked="true">
58
58
  English
59
59
  </a>
60
60
  </li>
@@ -19,9 +19,6 @@ import { VTTCueInfo } from '../../playback/types.js'
19
19
 
20
20
  const VERSION: string = '2.19.14'
21
21
 
22
- // TODO review
23
- // const LOCAL_STORAGE_CC_ID = 'gplayer.plugins.cc.selected'
24
-
25
22
  // const T = 'plugins.cc'
26
23
 
27
24
  /**
@@ -441,8 +438,6 @@ export class ClosedCaptions extends UICorePlugin {
441
438
  '-1',
442
439
  )
443
440
 
444
- // TODO review, make configurable, and emit event in addition
445
- // localStorage.setItem(LOCAL_STORAGE_CC_ID, id) // TODO store language instead?
446
441
  this.userSelectedItemId = id
447
442
  this.selectItem(this.findById(id))
448
443
  this.hideMenu()