@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.
- package/assets/audio-tracks/template.ejs +1 -1
- package/dist/core.js +65 -21
- package/dist/index.css +265 -265
- package/dist/index.embed.js +99 -34
- package/dist/index.js +125 -53
- package/docs/api/player.md +37 -0
- package/docs/api/player.player.getplugin.md +59 -0
- package/docs/api/player.player.md +14 -0
- package/docs/api/player.tokenrefreshoptions.gettoken.md +13 -0
- package/docs/api/player.tokenrefreshoptions.ipbound.md +13 -0
- package/docs/api/player.tokenrefreshoptions.md +115 -0
- package/docs/api/player.tokenrefreshoptions.ontokenrefreshed.md +13 -0
- package/docs/api/player.tokenrefreshoptions.refreshleadseconds.md +13 -0
- package/docs/api/player.tokenrefreshplugin.md +50 -0
- package/docs/api/player.tokenresponse.client_ip.md +13 -0
- package/docs/api/player.tokenresponse.expires.md +13 -0
- package/docs/api/player.tokenresponse.md +153 -0
- package/docs/api/player.tokenresponse.token.md +13 -0
- package/docs/api/player.tokenresponse.token_ip.md +13 -0
- package/docs/api/player.tokenresponse.url.md +13 -0
- package/docs/api/player.tokenresponse.url_ip.md +13 -0
- package/lib/playback/hls-playback/HlsPlayback.d.ts +1 -1
- package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
- package/lib/playback/hls-playback/HlsPlayback.js +23 -20
- package/lib/playback/hls-playback/RangesList.d.ts +7 -0
- package/lib/playback/hls-playback/RangesList.d.ts.map +1 -0
- package/lib/playback/hls-playback/RangesList.js +41 -0
- package/lib/plugins/audio-selector/AudioTracks.d.ts +4 -0
- package/lib/plugins/audio-selector/AudioTracks.d.ts.map +1 -1
- package/lib/plugins/audio-selector/AudioTracks.js +42 -12
- package/lib/plugins/subtitles/ClosedCaptions.d.ts.map +1 -1
- package/lib/plugins/subtitles/ClosedCaptions.js +0 -2
- package/package.json +1 -1
- package/src/playback/hls-playback/HlsPlayback.ts +40 -37
- package/src/playback/hls-playback/RangesList.ts +45 -0
- package/src/playback/hls-playback/__tests__/RangesList.test.ts +60 -0
- package/src/plugins/audio-selector/AudioTracks.ts +51 -16
- package/src/plugins/audio-selector/__tests__/__snapshots__/AudioTracks.test.ts.snap +9 -9
- package/src/plugins/subtitles/ClosedCaptions.ts +0 -5
- 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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
786
|
+
this._playableRegionStartTime,
|
|
781
787
|
),
|
|
782
788
|
end: Math.max(
|
|
783
789
|
0,
|
|
784
790
|
(this.el as HTMLMediaElement).buffered.end(i) -
|
|
785
|
-
|
|
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
|
-
|
|
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
|
-
;
|
|
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
|
-
|
|
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.
|
|
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
|
|
1154
|
-
|
|
1155
|
-
name: t.name,
|
|
1156
|
-
track: {
|
|
1154
|
+
return (
|
|
1155
|
+
this._hls?.subtitleTracks.map((t: MediaPlaylist) => ({
|
|
1157
1156
|
id: t.id,
|
|
1158
|
-
|
|
1159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
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="
|
|
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"
|
|
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="
|
|
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"
|
|
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="
|
|
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()
|