@gcorevideo/player 2.21.1 → 2.21.4
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-selector/style.scss +1 -1
- package/assets/audio-selector/track-selector.ejs +3 -3
- package/assets/bottom-gear/bottomgear.ejs +2 -2
- package/assets/media-control/container.scss +1 -1
- package/assets/media-control/media-control.ejs +1 -11
- package/assets/media-control/media-control.scss +49 -57
- package/assets/media-control/width270.scss +1 -1
- package/assets/media-control/width370.scss +7 -9
- package/assets/playback-rate/button.ejs +2 -2
- package/assets/playback-rate/list.ejs +4 -4
- package/assets/subtitles/combobox.ejs +10 -12
- package/assets/subtitles/string.ejs +1 -1
- package/assets/subtitles/style.scss +9 -16
- package/dist/core.js +5 -1
- package/dist/index.css +782 -794
- package/dist/index.js +240 -244
- package/dist/player.d.ts +141 -119
- package/dist/plugins/index.css +862 -874
- package/dist/plugins/index.js +222 -238
- package/docs/api/player.bottomgear.getelement.md +2 -2
- package/docs/api/player.bottomgear.md +1 -1
- package/docs/api/{player.subtitles.hide.md → player.closedcaptions.hide.md} +2 -2
- package/docs/api/{player.subtitles.md → player.closedcaptions.md} +11 -11
- package/docs/api/{player.subtitles.show.md → player.closedcaptions.show.md} +2 -2
- package/docs/api/player.closedcaptionspluginsettings.md +13 -0
- package/docs/api/player.gearitemelement.md +6 -4
- package/docs/api/player.gearoptionsitem.md +16 -0
- package/docs/api/player.md +48 -12
- package/docs/api/player.mediacontrol.putelement.md +2 -2
- package/docs/api/player.mediacontrolelement.md +1 -1
- package/docs/api/player.playbackrate.md +1 -1
- package/docs/api/player.subtitlespluginsettings.md +18 -0
- package/docs/api/player.texttrackitem.id.md +11 -0
- package/docs/api/player.texttrackitem.md +87 -0
- package/docs/api/player.texttrackitem.name.md +11 -0
- package/docs/api/player.texttrackitem.track.md +11 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/index.plugins.d.ts +2 -1
- package/lib/index.plugins.d.ts.map +1 -1
- package/lib/index.plugins.js +2 -1
- package/lib/playback/BasePlayback.d.ts +1 -0
- package/lib/playback/BasePlayback.d.ts.map +1 -1
- package/lib/playback/BasePlayback.js +3 -0
- package/lib/playback/dash-playback/DashPlayback.d.ts.map +1 -1
- package/lib/playback/dash-playback/DashPlayback.js +1 -0
- package/lib/playback.types.d.ts +5 -0
- package/lib/playback.types.d.ts.map +1 -1
- package/lib/plugins/audio-selector/AudioSelector.d.ts +2 -3
- package/lib/plugins/audio-selector/AudioSelector.d.ts.map +1 -1
- package/lib/plugins/audio-selector/AudioSelector.js +6 -7
- package/lib/plugins/bottom-gear/BottomGear.d.ts +7 -3
- package/lib/plugins/bottom-gear/BottomGear.d.ts.map +1 -1
- package/lib/plugins/bottom-gear/BottomGear.js +4 -2
- package/lib/plugins/media-control/MediaControl.d.ts +5 -6
- package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
- package/lib/plugins/media-control/MediaControl.js +48 -39
- package/lib/plugins/picture-in-picture/PictureInPicture.d.ts +1 -0
- package/lib/plugins/picture-in-picture/PictureInPicture.d.ts.map +1 -1
- package/lib/plugins/picture-in-picture/PictureInPicture.js +4 -4
- package/lib/plugins/playback-rate/PlaybackRate.d.ts +1 -1
- package/lib/plugins/playback-rate/PlaybackRate.d.ts.map +1 -1
- package/lib/plugins/playback-rate/PlaybackRate.js +24 -14
- package/lib/plugins/subtitles/ClosedCaptions.d.ts +118 -0
- package/lib/plugins/subtitles/ClosedCaptions.d.ts.map +1 -0
- package/lib/plugins/subtitles/ClosedCaptions.js +348 -0
- package/lib/plugins/subtitles/Subtitles.d.ts +31 -26
- package/lib/plugins/subtitles/Subtitles.d.ts.map +1 -1
- package/lib/plugins/subtitles/Subtitles.js +138 -169
- package/lib/testUtils.d.ts +22 -18
- package/lib/testUtils.d.ts.map +1 -1
- package/lib/testUtils.js +22 -36
- package/package.json +1 -1
- package/src/index.plugins.ts +2 -1
- package/src/index.ts +1 -1
- package/src/playback/BasePlayback.ts +4 -0
- package/src/playback/dash-playback/DashPlayback.ts +1 -0
- package/src/playback.types.ts +6 -0
- package/src/plugins/audio-selector/AudioSelector.ts +9 -8
- package/src/plugins/bottom-gear/BottomGear.ts +14 -5
- package/src/plugins/bottom-gear/__tests__/BottomGear.test.ts +1 -1
- package/src/plugins/bottom-gear/__tests__/__snapshots__/BottomGear.test.ts.snap +2 -2
- package/src/plugins/media-control/MediaControl.ts +84 -60
- package/src/plugins/media-control/__tests__/MediaControl.test.ts +43 -0
- package/src/plugins/media-control/__tests__/__snapshots__/MediaControl.test.ts.snap +175 -0
- package/src/plugins/picture-in-picture/PictureInPicture.ts +5 -5
- package/src/plugins/playback-rate/PlaybackRate.ts +143 -100
- package/src/plugins/playback-rate/__tests__/PlaybackRate.test.ts +65 -0
- package/src/plugins/playback-rate/__tests__/__snapshots__/PlaybackRate.test.ts.snap +11 -0
- package/src/plugins/subtitles/ClosedCaptions.ts +469 -0
- package/src/plugins/subtitles/__tests__/ClosedCaptions.test.ts +58 -0
- package/src/plugins/subtitles/__tests__/__snapshots__/ClosedCaptions.test.ts.snap +25 -0
- package/src/testUtils.ts +22 -36
- package/temp/player.api.json +269 -89
- package/tsconfig.tsbuildinfo +1 -1
- package/src/plugins/index.ts +0 -39
- package/src/plugins/subtitles/Subtitles.ts +0 -496
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import { Events, UICorePlugin, Browser, template, $ } from '@clappr/core'
|
|
2
|
+
import { reportError, trace } from '@gcorevideo/utils'
|
|
3
|
+
import assert from 'assert'
|
|
4
|
+
|
|
5
|
+
import { CLAPPR_VERSION } from '../../build.js'
|
|
6
|
+
import type { TextTrackItem } from '../../playback.types.js'
|
|
7
|
+
|
|
8
|
+
import '../../../assets/subtitles/style.scss'
|
|
9
|
+
import subtitlesOffIcon from '../../../assets/icons/new/subtitles-off.svg'
|
|
10
|
+
import subtitlesOnIcon from '../../../assets/icons/new/subtitles-on.svg'
|
|
11
|
+
import comboboxHTML from '../../../assets/subtitles/combobox.ejs'
|
|
12
|
+
import stringHTML from '../../../assets/subtitles/string.ejs'
|
|
13
|
+
|
|
14
|
+
import { isFullscreen } from '../utils.js'
|
|
15
|
+
import type { ZeptoResult } from '../../types.js'
|
|
16
|
+
|
|
17
|
+
const VERSION: string = '2.19.14'
|
|
18
|
+
|
|
19
|
+
const LOCAL_STORAGE_CC_ID = 'gplayer.plugins.cc.selected'
|
|
20
|
+
|
|
21
|
+
const T = 'plugins.cc'
|
|
22
|
+
|
|
23
|
+
export type ClosedCaptionsPluginSettings = {
|
|
24
|
+
/**
|
|
25
|
+
* Initially selected subtitles language
|
|
26
|
+
*/
|
|
27
|
+
language?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated Use {@link ClosedCaptionsPluginSettings} instead.
|
|
32
|
+
*/
|
|
33
|
+
export type SubtitlesPluginSettings = ClosedCaptionsPluginSettings;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `PLUGIN` that provides a UI to select the subtitles when available.
|
|
37
|
+
* @beta
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The plugin is activated when closed captions tracks are provided with the media source.
|
|
41
|
+
* It shows a familiar "CC" button with a dropdown menu to select the subtitles language.
|
|
42
|
+
*
|
|
43
|
+
* Depends on:
|
|
44
|
+
*
|
|
45
|
+
* - {@link MediaControl}
|
|
46
|
+
*
|
|
47
|
+
* Configuration options - {@link ClosedCaptionsPluginSettings}
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { ClosedCaptions } from '@gcorevideo/player'
|
|
51
|
+
*
|
|
52
|
+
* Player.registerPlugin(ClosedCaptions)
|
|
53
|
+
*
|
|
54
|
+
* new Player({
|
|
55
|
+
* ...
|
|
56
|
+
* cc: {
|
|
57
|
+
* language: 'en',
|
|
58
|
+
* },
|
|
59
|
+
* })
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export class ClosedCaptions extends UICorePlugin {
|
|
63
|
+
private isPreselectedApplied = false
|
|
64
|
+
|
|
65
|
+
private isShowing = false
|
|
66
|
+
|
|
67
|
+
private track: TextTrackItem | null = null
|
|
68
|
+
|
|
69
|
+
private tracks: TextTrackItem[] = []
|
|
70
|
+
|
|
71
|
+
private $line: ZeptoResult | null = null
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
get name() {
|
|
77
|
+
return 'cc'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
get supportedVersion() {
|
|
84
|
+
return { min: CLAPPR_VERSION }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @internal
|
|
89
|
+
*/
|
|
90
|
+
static get version() {
|
|
91
|
+
return VERSION
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private static readonly template = template(comboboxHTML)
|
|
95
|
+
|
|
96
|
+
private static readonly templateString = template(stringHTML)
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @internal
|
|
100
|
+
*/
|
|
101
|
+
override get attributes() {
|
|
102
|
+
return {
|
|
103
|
+
class: 'media-control-cc',
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @internal
|
|
109
|
+
*/
|
|
110
|
+
override get events() {
|
|
111
|
+
return {
|
|
112
|
+
'click [data-cc-select]': 'onItemSelect',
|
|
113
|
+
'click [data-cc-button]': 'toggleMenu',
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private get preselectedLanguage(): string {
|
|
118
|
+
return this.core.options.cc?.language ?? this.core.options.subtitles?.language ?? ''
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
override bindEvents() {
|
|
125
|
+
this.listenTo(this.core, Events.CORE_READY, this.onCoreReady)
|
|
126
|
+
this.listenTo(this.core, Events.CORE_RESIZE, this.playerResize)
|
|
127
|
+
this.listenTo(
|
|
128
|
+
this.core,
|
|
129
|
+
Events.CORE_ACTIVE_CONTAINER_CHANGED,
|
|
130
|
+
this.onContainerChanged,
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private onCoreReady() {
|
|
135
|
+
trace(`${T} onCoreReady`)
|
|
136
|
+
const mediaControl = this.core.getPlugin('media_control')
|
|
137
|
+
assert(mediaControl, 'media_control plugin is required')
|
|
138
|
+
this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, this.render)
|
|
139
|
+
this.listenTo(
|
|
140
|
+
mediaControl,
|
|
141
|
+
Events.MEDIACONTROL_HIDE,
|
|
142
|
+
this.hideMenu,
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private onContainerChanged() {
|
|
147
|
+
trace(`${T} onContainerChanged`)
|
|
148
|
+
this.listenTo(
|
|
149
|
+
this.core.activeContainer,
|
|
150
|
+
Events.CONTAINER_FULLSCREEN,
|
|
151
|
+
this.playerResize,
|
|
152
|
+
)
|
|
153
|
+
this.listenTo(
|
|
154
|
+
this.core.activeContainer,
|
|
155
|
+
'container:advertisement:start',
|
|
156
|
+
this.onStartAd,
|
|
157
|
+
)
|
|
158
|
+
this.listenTo(
|
|
159
|
+
this.core.activePlayback,
|
|
160
|
+
Events.PLAYBACK_SUBTITLE_AVAILABLE,
|
|
161
|
+
this.onSubtitleAvailable,
|
|
162
|
+
)
|
|
163
|
+
this.listenTo(
|
|
164
|
+
this.core.activePlayback,
|
|
165
|
+
Events.PLAYBACK_SUBTITLE_CHANGED,
|
|
166
|
+
this.onSubtitleChanged,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// fix for iOS
|
|
170
|
+
const video = this.core.activePlayback.el
|
|
171
|
+
assert(video, 'video element is required')
|
|
172
|
+
|
|
173
|
+
video.addEventListener('webkitbeginfullscreen', () => {
|
|
174
|
+
if (Browser.isiOS) {
|
|
175
|
+
video.classList.add('ios-fullscreen')
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
video.addEventListener('webkitendfullscreen', () => {
|
|
180
|
+
if (Browser.isiOS) {
|
|
181
|
+
video.classList.remove('ios-fullscreen')
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private onSubtitleAvailable() {
|
|
187
|
+
trace(`${T} onSubtitleAvailable`)
|
|
188
|
+
this.applyTracks()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private onSubtitleChanged({ id }: { id: number }) {
|
|
192
|
+
trace(`${T} onSubtitleChanged`, { id })
|
|
193
|
+
if (id === -1) {
|
|
194
|
+
this.clearSubtitleText()
|
|
195
|
+
}
|
|
196
|
+
for (const track of this.tracks) {
|
|
197
|
+
if (track.id === id) {
|
|
198
|
+
track.track.mode = 'showing'
|
|
199
|
+
|
|
200
|
+
this.setSubtitleText(this.getSubtitleText(track.track))
|
|
201
|
+
|
|
202
|
+
track.track.oncuechange = (e) => {
|
|
203
|
+
try {
|
|
204
|
+
if (track.track.activeCues?.length) {
|
|
205
|
+
const html = (track.track.activeCues[0] as VTTCue).getCueAsHTML()
|
|
206
|
+
|
|
207
|
+
this.setSubtitleText(html)
|
|
208
|
+
} else {
|
|
209
|
+
this.clearSubtitleText()
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
reportError(error)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
track.track.oncuechange = null
|
|
217
|
+
track.track.mode = 'hidden'
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private applyTracks() {
|
|
223
|
+
try {
|
|
224
|
+
this.tracks = this.core.activePlayback.closedCaptionsTracks
|
|
225
|
+
this.applyPreselectedSubtitles()
|
|
226
|
+
this.render()
|
|
227
|
+
} catch (error) {
|
|
228
|
+
reportError(error)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private onStartAd() {
|
|
233
|
+
if (this.isShowing && this.core.activeContainer) {
|
|
234
|
+
this.hide()
|
|
235
|
+
this.listenTo(
|
|
236
|
+
this.core.activeContainer,
|
|
237
|
+
'container:advertisement:finish',
|
|
238
|
+
this.onFinishAd,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private onFinishAd() {
|
|
244
|
+
this.show()
|
|
245
|
+
this.stopListening(
|
|
246
|
+
this.core.activeContainer,
|
|
247
|
+
'container:advertisement:finish',
|
|
248
|
+
this.onFinishAd,
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private playerResize() {
|
|
253
|
+
trace(`${T} playerResize`)
|
|
254
|
+
const shouldShow =
|
|
255
|
+
this.core.activeContainer &&
|
|
256
|
+
isFullscreen(this.core.activeContainer.el) &&
|
|
257
|
+
this.track &&
|
|
258
|
+
this.track.track.mode &&
|
|
259
|
+
Browser.isiOS &&
|
|
260
|
+
this.isShowing
|
|
261
|
+
|
|
262
|
+
if (shouldShow) {
|
|
263
|
+
this.show()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
this.resizeFont()
|
|
268
|
+
} catch (error) {
|
|
269
|
+
reportError(error)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Hides the subtitles menu and the subtitles.
|
|
275
|
+
*/
|
|
276
|
+
hide() {
|
|
277
|
+
this.isShowing = false
|
|
278
|
+
this.renderIcon()
|
|
279
|
+
this.$line.hide()
|
|
280
|
+
if (this.tracks) {
|
|
281
|
+
for (const t of this.tracks) {
|
|
282
|
+
t.track.mode = 'hidden'
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Shows the subtitles menu and the subtitles.
|
|
289
|
+
*/
|
|
290
|
+
show() {
|
|
291
|
+
this.isShowing = true
|
|
292
|
+
this.renderIcon()
|
|
293
|
+
if (
|
|
294
|
+
this.core.activeContainer &&
|
|
295
|
+
isFullscreen(this.core.activeContainer.el) &&
|
|
296
|
+
this.track &&
|
|
297
|
+
this.track.track.mode &&
|
|
298
|
+
Browser.isiOS
|
|
299
|
+
) {
|
|
300
|
+
this.$line.hide()
|
|
301
|
+
this.track.track.mode = 'showing'
|
|
302
|
+
} else {
|
|
303
|
+
this.$line.show()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
private shouldRender() {
|
|
308
|
+
return this.tracks?.length > 0
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private resizeFont() {
|
|
312
|
+
if (!this.$line) {
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const skinWidth = this.core.activeContainer.$el.width()
|
|
317
|
+
|
|
318
|
+
this.$line.find('p').css('font-size', skinWidth * 0.03)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @internal
|
|
323
|
+
*/
|
|
324
|
+
override render() {
|
|
325
|
+
if (!this.core.activeContainer) {
|
|
326
|
+
return this
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!this.shouldRender()) {
|
|
330
|
+
return this
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const mediaControl = this.core.getPlugin('media_control')
|
|
334
|
+
|
|
335
|
+
this.$el.html(ClosedCaptions.template({ tracks: this.tracks }))
|
|
336
|
+
this.core.activeContainer.$el.find('#cc-line').remove()
|
|
337
|
+
this.$line = $(ClosedCaptions.templateString())
|
|
338
|
+
this.resizeFont()
|
|
339
|
+
|
|
340
|
+
this.core.activeContainer.$el.append(this.$line)
|
|
341
|
+
mediaControl.putElement('cc', this.el)
|
|
342
|
+
|
|
343
|
+
this.updateSelection()
|
|
344
|
+
|
|
345
|
+
this.renderIcon()
|
|
346
|
+
|
|
347
|
+
return this
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private findById(id: number) {
|
|
351
|
+
return this.tracks.find((track) => track.id === id) ?? null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
private selectItem(item: TextTrackItem | null) {
|
|
355
|
+
this.clearSubtitleText()
|
|
356
|
+
this.track = item
|
|
357
|
+
|
|
358
|
+
this.hideMenu()
|
|
359
|
+
this.updateSelection()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private onItemSelect(event: MouseEvent) {
|
|
363
|
+
const id = (event.target as HTMLElement).dataset.ccSelect ?? '-1'
|
|
364
|
+
|
|
365
|
+
trace(`${T} onItemSelect`, { id })
|
|
366
|
+
|
|
367
|
+
localStorage.setItem(LOCAL_STORAGE_CC_ID, id)
|
|
368
|
+
this.selectItem(this.findById(Number(id)))
|
|
369
|
+
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private applyPreselectedSubtitles() {
|
|
374
|
+
if (!this.isPreselectedApplied) {
|
|
375
|
+
this.isPreselectedApplied = true
|
|
376
|
+
if (!this.preselectedLanguage) {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
this.selectItem(
|
|
381
|
+
this.tracks.find(
|
|
382
|
+
(t) => t.track.language === this.preselectedLanguage,
|
|
383
|
+
) ?? null,
|
|
384
|
+
)
|
|
385
|
+
}, 300) // TODO why delay?
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private hideMenu() {
|
|
390
|
+
;(this.$('[data-cc] ul') as ZeptoResult).hide()
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private toggleMenu() {
|
|
394
|
+
trace(`${T} toggleMenu`)
|
|
395
|
+
;(this.$('[data-cc] ul') as ZeptoResult).toggle()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private itemElement(id: number): ZeptoResult {
|
|
399
|
+
return (
|
|
400
|
+
this.$(`ul li a[data-cc-select="${id}"]`) as ZeptoResult
|
|
401
|
+
).parent()
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private allItemElements(): ZeptoResult {
|
|
405
|
+
return this.$('[data-cc] li')
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private selectSubtitles() {
|
|
409
|
+
const trackId = this.track ? this.track.id : -1
|
|
410
|
+
|
|
411
|
+
this.core.activePlayback.closedCaptionsTrackId = trackId
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private getSubtitleText(track: TextTrack) {
|
|
415
|
+
const currentTime = this.core.activePlayback?.getCurrentTime() ?? 0
|
|
416
|
+
const cues = track.cues
|
|
417
|
+
const lines = []
|
|
418
|
+
|
|
419
|
+
if (cues && cues.length) {
|
|
420
|
+
for (const cue of cues) {
|
|
421
|
+
if (currentTime >= cue.startTime && currentTime <= cue.endTime) {
|
|
422
|
+
lines.push((cue as VTTCue).getCueAsHTML().textContent)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return lines.join('\n')
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private setSubtitleText(text: string | DocumentFragment) {
|
|
431
|
+
this.$line.find('p').html(text)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private clearSubtitleText() {
|
|
435
|
+
this.setSubtitleText('')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private updateSelection() {
|
|
439
|
+
if (!this.track) {
|
|
440
|
+
this.hide()
|
|
441
|
+
} else {
|
|
442
|
+
this.show()
|
|
443
|
+
}
|
|
444
|
+
this.selectSubtitles()
|
|
445
|
+
this.highlightCurrentSubtitles()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private highlightCurrentSubtitles() {
|
|
449
|
+
this.allItemElements()
|
|
450
|
+
.removeClass('current')
|
|
451
|
+
.find('a')
|
|
452
|
+
.removeClass('gcore-skin-active')
|
|
453
|
+
|
|
454
|
+
trace(`${T} highlightCurrentSubtitles`, {
|
|
455
|
+
track: this.track?.id,
|
|
456
|
+
})
|
|
457
|
+
const currentLevelElement = this.itemElement(this.track ? this.track.id : -1)
|
|
458
|
+
currentLevelElement
|
|
459
|
+
.addClass('current')
|
|
460
|
+
.find('a')
|
|
461
|
+
.addClass('gcore-skin-active')
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
private renderIcon() {
|
|
465
|
+
const icon = this.isShowing ? subtitlesOnIcon : subtitlesOffIcon
|
|
466
|
+
|
|
467
|
+
this.$el.find('span.cc-text').html(icon)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ClosedCaptions } from '../ClosedCaptions.js'
|
|
3
|
+
import { createMockCore, createMockMediaControl } from '../../../testUtils.js';
|
|
4
|
+
|
|
5
|
+
describe('ClosedCaptions', () => {
|
|
6
|
+
let core: any;
|
|
7
|
+
let mediaControl: any;
|
|
8
|
+
let cc: ClosedCaptions;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
core = createMockCore()
|
|
11
|
+
mediaControl = createMockMediaControl(core)
|
|
12
|
+
core.getPlugin = vi.fn().mockImplementation((name) => {
|
|
13
|
+
if (name === 'media_control') {
|
|
14
|
+
return mediaControl
|
|
15
|
+
}
|
|
16
|
+
return null
|
|
17
|
+
})
|
|
18
|
+
cc = new ClosedCaptions(core)
|
|
19
|
+
})
|
|
20
|
+
describe('basically', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
core.emit('core:ready')
|
|
23
|
+
core.activePlayback.el = document.createElement('video')
|
|
24
|
+
core.emit('core:active:container:changed', core.activeContainer)
|
|
25
|
+
core.activePlayback.closedCaptionsTracks = [
|
|
26
|
+
{
|
|
27
|
+
id: 1,
|
|
28
|
+
name: 'English',
|
|
29
|
+
track: {
|
|
30
|
+
language: 'en',
|
|
31
|
+
kind: 'subtitles',
|
|
32
|
+
label: 'English',
|
|
33
|
+
mode: 'hidden',
|
|
34
|
+
cues: [],
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 2,
|
|
39
|
+
name: 'Spanish',
|
|
40
|
+
track: {
|
|
41
|
+
language: 'es',
|
|
42
|
+
kind: 'subtitles',
|
|
43
|
+
label: 'Spanish',
|
|
44
|
+
mode: 'hidden',
|
|
45
|
+
cues: [],
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
core.activePlayback.emit('playback:subtitle:available')
|
|
50
|
+
core.activeContainer.emit('container:subtitle:available')
|
|
51
|
+
})
|
|
52
|
+
it('should render', () => {
|
|
53
|
+
expect(cc.el.innerHTML).toMatchSnapshot()
|
|
54
|
+
expect(cc.$el.find('[data-cc-button]').length).toEqual(1)
|
|
55
|
+
expect(mediaControl.putElement).toHaveBeenCalledWith('cc', cc.el)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ClosedCaptions > basically > should render 1`] = `
|
|
4
|
+
"<button data-cc-button="" class="media-control-button media-control-icon gcore-skin-button-color" id="cc-button">
|
|
5
|
+
<span class="cc-text">/assets/icons/new/subtitles-off.svg</span>
|
|
6
|
+
</button>
|
|
7
|
+
|
|
8
|
+
<ul class="gcore-skin-bg-color" id="cc-select">
|
|
9
|
+
|
|
10
|
+
<li>
|
|
11
|
+
<a href="#" class="gcore-skin-text-color" data-cc-select="1">
|
|
12
|
+
English
|
|
13
|
+
</a>
|
|
14
|
+
</li>
|
|
15
|
+
|
|
16
|
+
<li>
|
|
17
|
+
<a href="#" class="gcore-skin-text-color" data-cc-select="2">
|
|
18
|
+
Spanish
|
|
19
|
+
</a>
|
|
20
|
+
</li>
|
|
21
|
+
|
|
22
|
+
<li class="current"><a href="#" class="gcore-skin-text-color gcore-skin-active" data-cc-select="-1">Off</a></li>
|
|
23
|
+
</ul>
|
|
24
|
+
"
|
|
25
|
+
`;
|
package/src/testUtils.ts
CHANGED
|
@@ -126,47 +126,31 @@ export function createMockPlayback(name = 'mock') {
|
|
|
126
126
|
return Object.assign(emitter, {
|
|
127
127
|
name,
|
|
128
128
|
currentLevel: -1,
|
|
129
|
+
dvrEnabled: false,
|
|
129
130
|
levels: [],
|
|
130
131
|
consent() {},
|
|
131
132
|
play() {},
|
|
132
133
|
pause() {},
|
|
133
134
|
stop() {},
|
|
134
|
-
destroy()
|
|
135
|
-
seek()
|
|
136
|
-
seekPercentage()
|
|
137
|
-
getDuration()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
mute() {},
|
|
155
|
-
unmute() {},
|
|
156
|
-
volume() {},
|
|
157
|
-
configure() {},
|
|
158
|
-
attemptAutoPlay() {
|
|
159
|
-
return true
|
|
160
|
-
},
|
|
161
|
-
canAutoPlay() {
|
|
162
|
-
return true
|
|
163
|
-
},
|
|
164
|
-
onResize() {
|
|
165
|
-
return true
|
|
166
|
-
},
|
|
167
|
-
trigger(event: string, ...args: any[]) {
|
|
168
|
-
emitter.emit(event, ...args)
|
|
169
|
-
},
|
|
135
|
+
destroy: vi.fn(),
|
|
136
|
+
seek: vi.fn(),
|
|
137
|
+
seekPercentage: vi.fn(),
|
|
138
|
+
getDuration: vi.fn().mockImplementation(() => 100),
|
|
139
|
+
enterPiP: vi.fn(),
|
|
140
|
+
exitPiP: vi.fn(),
|
|
141
|
+
getPlaybackType: vi.fn().mockImplementation(() => 'live'),
|
|
142
|
+
getStartTimeOffset: vi.fn().mockImplementation(() => 0),
|
|
143
|
+
getCurrentTime: vi.fn().mockImplementation(() => 0),
|
|
144
|
+
isHighDefinitionInUse: vi.fn().mockImplementation(() => false),
|
|
145
|
+
mute: vi.fn(),
|
|
146
|
+
unmute: vi.fn(),
|
|
147
|
+
volume: vi.fn(),
|
|
148
|
+
configure: vi.fn(),
|
|
149
|
+
attemptAutoPlay: vi.fn().mockImplementation(() => true),
|
|
150
|
+
canAutoPlay: vi.fn().mockImplementation(() => true),
|
|
151
|
+
onResize: vi.fn().mockImplementation(() => true),
|
|
152
|
+
setPlaybackRate: vi.fn(),
|
|
153
|
+
trigger: emitter.emit,
|
|
170
154
|
})
|
|
171
155
|
}
|
|
172
156
|
|
|
@@ -179,6 +163,8 @@ export function createMockContainer(playback: any = createMockPlayback()) {
|
|
|
179
163
|
$el: $(el),
|
|
180
164
|
getDuration: vi.fn().mockReturnValue(0),
|
|
181
165
|
getPlugin: vi.fn(),
|
|
166
|
+
getPlaybackType: vi.fn().mockReturnValue('live'),
|
|
167
|
+
isDvrInUse: vi.fn().mockReturnValue(false),
|
|
182
168
|
isPlaying: vi.fn().mockReturnValue(false),
|
|
183
169
|
play: vi.fn(),
|
|
184
170
|
seek: vi.fn(),
|