@gcorevideo/player 2.22.14 → 2.22.16

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 (80) hide show
  1. package/assets/clips/clips.ejs +1 -0
  2. package/assets/clips/clips.scss +23 -3
  3. package/assets/context-menu/context_menu.ejs +14 -6
  4. package/assets/context-menu/context_menu.scss +18 -4
  5. package/assets/level-selector/list.ejs +9 -3
  6. package/assets/media-control/media-control.ejs +1 -9
  7. package/assets/media-control/media-control.scss +0 -25
  8. package/assets/media-control/width370.scss +4 -4
  9. package/dist/core.js +5 -23
  10. package/dist/index.css +424 -412
  11. package/dist/index.js +294 -286
  12. package/dist/player.d.ts +211 -144
  13. package/dist/plugins/index.css +1513 -1501
  14. package/dist/plugins/index.js +224 -227
  15. package/docs/api/{player.audioselector.md → player.audiotracks.md} +3 -3
  16. package/docs/api/player.contextmenu.md +2 -0
  17. package/docs/api/player.contextmenupluginsettings.md +2 -40
  18. package/docs/api/{player.contextmenupluginsettings.label.md → player.contextmenupluginsettings.options.md} +3 -3
  19. package/docs/api/player.md +78 -23
  20. package/docs/api/player.mediacontrol.md +8 -14
  21. package/docs/api/player.mediacontrolelement.md +4 -2
  22. package/docs/api/{player.contextmenupluginsettings.preventshowcontextmenu.md → player.mediacontrollayerelement.md} +5 -3
  23. package/docs/api/player.mediacontrolleftelement.md +16 -0
  24. package/docs/api/player.mediacontrolrightelement.md +16 -0
  25. package/docs/api/player.mediacontrolsettings.md +23 -0
  26. package/docs/api/{player.contextmenupluginsettings.url.md → player.menuoption.md} +10 -3
  27. package/docs/api/player.playbackrate.md +1 -1
  28. package/docs/api/player.playerconfig.md +1 -1
  29. package/docs/api/player.playerconfig.playbacktype.md +1 -1
  30. package/docs/api/{player.levelselector.events.md → player.qualitylevels.events.md} +2 -2
  31. package/docs/api/{player.levelselector.md → player.qualitylevels.md} +6 -6
  32. package/docs/api/{player.levelselectorpluginsettings.labels.md → player.qualitylevelspluginsettings.labels.md} +2 -2
  33. package/docs/api/{player.levelselectorpluginsettings.md → player.qualitylevelspluginsettings.md} +6 -6
  34. package/docs/api/{player.levelselectorpluginsettings.restrictresolution.md → player.qualitylevelspluginsettings.restrictresolution.md} +2 -2
  35. package/lib/Player.d.ts.map +1 -1
  36. package/lib/Player.js +4 -1
  37. package/lib/playback/hls-playback/HlsPlayback.d.ts.map +1 -1
  38. package/lib/playback/hls-playback/HlsPlayback.js +0 -21
  39. package/lib/plugins/click-to-pause/ClickToPause.js +1 -1
  40. package/lib/plugins/clips/Clips.d.ts +21 -16
  41. package/lib/plugins/clips/Clips.d.ts.map +1 -1
  42. package/lib/plugins/clips/Clips.js +96 -98
  43. package/lib/plugins/clips/types.d.ts +19 -0
  44. package/lib/plugins/clips/types.d.ts.map +1 -0
  45. package/lib/plugins/clips/types.js +1 -0
  46. package/lib/plugins/clips/utils.d.ts +4 -0
  47. package/lib/plugins/clips/utils.d.ts.map +1 -0
  48. package/lib/plugins/clips/utils.js +36 -0
  49. package/lib/plugins/context-menu/ContextMenu.d.ts +33 -12
  50. package/lib/plugins/context-menu/ContextMenu.d.ts.map +1 -1
  51. package/lib/plugins/context-menu/ContextMenu.js +40 -37
  52. package/lib/plugins/media-control/MediaControl.d.ts +4 -7
  53. package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
  54. package/lib/plugins/media-control/MediaControl.js +19 -31
  55. package/lib/plugins/utils.d.ts +9 -1
  56. package/lib/plugins/utils.d.ts.map +1 -1
  57. package/lib/plugins/utils.js +9 -10
  58. package/lib/plugins/vast-ads/loaderxml.js +2 -2
  59. package/lib/testUtils.d.ts.map +1 -1
  60. package/lib/testUtils.js +2 -5
  61. package/package.json +1 -1
  62. package/src/Player.ts +4 -3
  63. package/src/playback/hls-playback/HlsPlayback.ts +0 -22
  64. package/src/plugins/click-to-pause/ClickToPause.ts +1 -1
  65. package/src/plugins/clips/Clips.ts +116 -135
  66. package/src/plugins/clips/__tests__/Clips.test.ts +72 -0
  67. package/src/plugins/clips/__tests__/__snapshots__/Clips.test.ts.snap +14 -0
  68. package/src/plugins/clips/types.ts +22 -0
  69. package/src/plugins/clips/utils.ts +54 -0
  70. package/src/plugins/context-menu/ContextMenu.ts +72 -56
  71. package/src/plugins/level-selector/__tests__/__snapshots__/QualityLevels.test.ts.snap +18 -18
  72. package/src/plugins/media-control/MediaControl.ts +31 -58
  73. package/src/plugins/media-control/__tests__/MediaControl.test.ts +66 -30
  74. package/src/plugins/media-control/__tests__/__snapshots__/MediaControl.test.ts.snap +7 -35
  75. package/src/plugins/utils.ts +9 -7
  76. package/src/plugins/vast-ads/loaderxml.ts +2 -2
  77. package/src/testUtils.ts +2 -5
  78. package/temp/player.api.json +332 -262
  79. package/tsconfig.tsbuildinfo +1 -1
  80. package/docs/api/player.mediacontrol.handlecustomarea.md +0 -52
@@ -1,55 +1,54 @@
1
- import { Container, Events, UICorePlugin, $ } from '@clappr/core'
1
+ import { Container, Events, UICorePlugin, $, template } from '@clappr/core'
2
+ import { trace } from '@gcorevideo/utils'
3
+ import assert from 'assert'
2
4
 
3
5
  import { TimeProgress } from '../../playback.types.js'
4
6
  import type { ZeptoResult } from '../../types.js'
5
- import { strtimeToMiliseconds } from '../utils.js'
6
7
  import '../../../assets/clips/clips.scss'
7
- import assert from 'assert'
8
-
9
- type ClipDesc = {
10
- start: number
11
- text: string
12
- end: number
13
- index: number
14
- }
8
+ import { ClipDesc } from './types.js'
9
+ import { buildSvg, parseClips } from './utils.js'
10
+ import clipsHTML from '../../../assets/clips/clips.ejs'
15
11
 
16
- type ClipItem = {
17
- start: number
18
- text: string
19
- }
12
+ const T = 'plugins.clips'
20
13
 
21
14
  /**
22
- * Configuration options for the {@link ClipsPlugin | clips} plugin.
15
+ * Configuration options for the {@link ClipsPlugin} plugin.
23
16
  * @beta
24
17
  */
25
18
  export interface ClipsPluginSettings {
26
19
  /**
27
- * The text to display over the seekbar.
20
+ * The compiled text of the clips description, one clip per line in format :
21
+ * `HH:MM:SS text` or `MM:SS text` or `SS text`
28
22
  */
29
23
  text: string
30
24
  }
31
25
 
26
+ const VERSION = '2.22.16'
27
+ const CLAPPR_VERSION = '0.11.4'
28
+
32
29
  /**
33
- * `PLUGIN` that shows text over the seekbar to indicate the current clip.
30
+ * `PLUGIN` that allows marking up the timeline of the video
34
31
  * @beta
35
32
  * @remarks
33
+ * The plugin decorates the seekbar with notches to indicate the clips of the video and displays current clip text in the left panel
34
+ *
36
35
  * Depends on:
37
36
  *
38
37
  * - {@link MediaControl}
39
38
  *
40
39
  * Configuration options - {@link ClipsPluginSettings}
41
40
  */
42
- export class ClipsPlugin extends UICorePlugin {
43
- private clips: Map<number, ClipDesc> = new Map()
44
-
45
- private duration: number = 0
41
+ export class Clips extends UICorePlugin {
42
+ private barStyle: HTMLStyleElement | null = null
46
43
 
47
- private durationGetting = false
44
+ private clips: ClipDesc[] = []
48
45
 
49
- private _oldContainer: Container | undefined
46
+ private oldContainer: Container | undefined
50
47
 
51
48
  private svgMask: ZeptoResult | null = null
52
49
 
50
+ private static readonly template = template(clipsHTML)
51
+
53
52
  /**
54
53
  * @internal
55
54
  */
@@ -62,174 +61,156 @@ export class ClipsPlugin extends UICorePlugin {
62
61
  */
63
62
  override get attributes() {
64
63
  return {
65
- class: this.name,
64
+ class: 'media-control-clips',
66
65
  }
67
66
  }
68
67
 
68
+ get version() {
69
+ return VERSION
70
+ }
71
+
72
+ get supportedVersion() {
73
+ return { min: CLAPPR_VERSION }
74
+ }
75
+
69
76
  /**
70
77
  * @internal
71
78
  */
72
79
  override bindEvents() {
73
- const mediaControl = this.core.getPlugin('media_control')
74
- assert(mediaControl, 'media_control plugin is required')
75
- this.listenToOnce(this.core, Events.CORE_READY, this._onCoreReady)
76
- // TODO listen to CORE_ACTIVE_CONTAINER_CHANGED
80
+ this.listenToOnce(this.core, Events.CORE_READY, this.onCoreReady)
81
+ this.listenTo(this.core, Events.CORE_RESIZE, this.playerResize)
77
82
  this.listenTo(
78
- mediaControl,
79
- Events.MEDIACONTROL_CONTAINERCHANGED,
80
- this._onMediaControlContainerChanged,
83
+ this.core,
84
+ Events.CORE_ACTIVE_CONTAINER_CHANGED,
85
+ this.onContainerChanged,
81
86
  )
82
- this.listenTo(this.core, Events.CORE_RESIZE, this.playerResize)
83
87
  }
84
88
 
85
- private _onCoreReady() {
89
+ override render() {
90
+ trace(`${T} render`)
86
91
  if (!this.options.clips) {
87
- this.destroy()
92
+ return this
93
+ }
94
+ this.$el.html(Clips.template())
95
+ this.$el.hide()
96
+ return this
97
+ }
88
98
 
89
- return
99
+ override destroy() {
100
+ if (this.barStyle) {
101
+ this.barStyle.remove()
102
+ this.barStyle = null
90
103
  }
104
+ return super.destroy()
105
+ }
91
106
 
92
- this.parseClips()
107
+ override disable() {
108
+ if (this.barStyle) {
109
+ this.barStyle.remove()
110
+ this.barStyle = null
111
+ }
112
+ return super.disable()
93
113
  }
94
114
 
95
- private _onMediaControlContainerChanged() {
96
- this._bindContainerEvents()
115
+ override enable() {
116
+ this.render()
117
+ return super.enable()
97
118
  }
98
119
 
99
- private playerResize() {
100
- this.durationGetting = false
101
- if (this.durationGetting) {
102
- this.makeSvg(this.duration)
103
- }
120
+ private onCoreReady() {
121
+ trace(`${T} onCoreReady`)
122
+ const mediaControl = this.core.getPlugin('media_control')
123
+ assert(mediaControl, 'media_control plugin is required')
124
+
125
+ this.parseClips(this.options.clips.text)
126
+ this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, this.onMcRender)
104
127
  }
105
128
 
106
- private _bindContainerEvents() {
107
- if (this._oldContainer) {
129
+ private onMcRender() {
130
+ trace(`${T} onMcRender`)
131
+ const mediaControl = this.core.getPlugin('media_control')
132
+ mediaControl.mount('clips', this.$el)
133
+ }
134
+
135
+ private onContainerChanged() {
136
+ trace(`${T} onContainerChanged`)
137
+ // TODO figure out the conditions of changing the container (without destroying the previous one)
138
+ if (this.oldContainer) {
108
139
  this.stopListening(
109
- this._oldContainer,
140
+ this.oldContainer,
110
141
  Events.CONTAINER_TIMEUPDATE,
111
142
  this.onTimeUpdate,
112
143
  )
113
144
  }
114
-
115
- const mediaControl = this.core.getPlugin('media_control')
116
- this._oldContainer = mediaControl.container
117
- this.durationGetting = false
145
+ this.oldContainer = this.core.activeContainer
146
+ if (this.svgMask) {
147
+ this.svgMask.remove()
148
+ this.svgMask = null
149
+ }
118
150
  this.listenTo(
119
- mediaControl.container,
151
+ this.core.activeContainer,
120
152
  Events.CONTAINER_TIMEUPDATE,
121
153
  this.onTimeUpdate,
122
154
  )
123
155
  }
124
156
 
157
+ private playerResize() {
158
+ const duration = this.core.activeContainer.getDuration()
159
+ if (duration) {
160
+ this.makeSvg(duration)
161
+ }
162
+ }
163
+
125
164
  private onTimeUpdate(event: TimeProgress) {
126
- if (!this.durationGetting) {
127
- this.duration = event.total
165
+ if (!this.svgMask) {
128
166
  this.makeSvg(event.total)
129
- this.durationGetting = true
130
167
  }
131
-
132
- for (const value of this.clips.values()) {
133
- if (event.current >= value.start && event.current < value.end) {
168
+ for (const value of this.clips) {
169
+ if (
170
+ (event.current >= value.start && !value.end) ||
171
+ event.current < value.end
172
+ ) {
134
173
  this.setClipText(value.text)
135
174
  break
136
175
  }
137
176
  }
138
177
  }
139
178
 
140
- private parseClips() {
141
- const textArr = this.options.clips.text.split('\n')
142
-
143
- const clipsArr = textArr
144
- .map((val: string) => {
145
- const matchRes = val.match(/(\d+:\d+|:\d+) (.+)/i)
146
-
147
- return matchRes
148
- ? {
149
- start: strtimeToMiliseconds(matchRes[1]),
150
- text: matchRes[2],
151
- }
152
- : null
153
- })
154
- .filter((clip: ClipItem | null) => clip !== null)
155
-
156
- clipsArr.sort((a: ClipDesc, b: ClipDesc) => a.start - b.start)
157
-
158
- clipsArr.forEach((clip: ClipDesc, index: number) => {
159
- this.clips.set(clip.start, {
160
- index,
161
- start: clip.start,
162
- text: clip.text,
163
- end: clipsArr[index + 1] ? clipsArr[index + 1].start : null,
164
- })
165
- })
166
- }
167
-
168
- /**
169
- * Returns the text of the current clip.
170
- * @param time - The current time of the player.
171
- * @returns The text of the current clip.
172
- */
173
- getText(time: number) {
174
- for (const [key, value] of this.clips.entries()) {
175
- if (time >= value.start && time < value.end) {
176
- return value.text
177
- }
178
- }
179
- return ''
179
+ private parseClips(text: string) {
180
+ this.clips = parseClips(text)
180
181
  }
181
182
 
182
183
  private makeSvg(duration: number) {
183
- let svg =
184
- '<svg width="0" height="0">\n' + '<defs>\n' + '<clipPath id="myClip">\n'
185
- const widthOfSeek = this.core.activeContainer.$el.width()
186
- let finishValue = 0
187
-
188
- this.clips.forEach((val) => {
189
- let end = val.end
190
-
191
- if (!end) {
192
- end = val.end = duration
193
- }
194
-
195
- const widthChunk = ((end - val.start) * widthOfSeek) / duration
196
-
197
- svg += `<rect x="${finishValue}" y="0" width="${
198
- widthChunk - 2
199
- }" height="30"/>\n`
200
- finishValue += widthChunk
201
- })
202
-
203
- svg += `<rect x="${finishValue}" y="0" width="${
204
- widthOfSeek - finishValue
205
- }" height="30"/>\n`
206
- svg += '</clipPath>' + '</defs>' + '</svg>'
184
+ const svg = buildSvg(
185
+ this.clips,
186
+ duration,
187
+ this.core.activeContainer.$el.width(),
188
+ )
207
189
  this.setSVGMask(svg)
208
190
  }
209
191
 
210
192
  private setSVGMask(svg: string) {
211
- // this.core.mediaControl.setSVGMask(svg);
212
193
  if (this.svgMask) {
213
194
  this.svgMask.remove()
214
195
  }
215
196
 
216
- const mediaControl = this.core.getPlugin('media_control')
217
- const $seekBarContainer =
218
- mediaControl.getElement('seekBarContainer')
219
- if ($seekBarContainer?.get(0)) {
220
- $seekBarContainer.addClass('clips')
221
- }
222
-
223
197
  this.svgMask = $(svg)
224
- $seekBarContainer?.append(this.svgMask)
198
+ this.$el.append(this.svgMask)
199
+ if (!this.barStyle) {
200
+ this.barStyle = document.createElement('style')
201
+ this.barStyle.textContent = `
202
+ .bar-container[data-seekbar] {
203
+ clip-path: url("#myClip");
204
+ }`
205
+ this.$el.append(this.barStyle)
206
+ }
225
207
  }
226
208
 
227
209
  private setClipText(text: string) {
228
- const mediaControl = this.core.getPlugin('media_control')
229
- const $clipText = mediaControl.getElement('clipText')
230
- if ($clipText && text) {
231
- $clipText.show()
232
- $clipText.text(`${text}`)
210
+ if (text) {
211
+ this.$el.show().find('#clips-text').text(text)
212
+ } else {
213
+ this.$el.hide()
233
214
  }
234
215
  }
235
216
  }
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { Clips } from '../Clips'
3
+ import { createMockCore, createMockMediaControl } from '../../../testUtils'
4
+ import { Events } from '@clappr/core'
5
+
6
+ // import { LogTracer, Logger, setTracer } from '@gcorevideo/utils'
7
+
8
+ // Logger.enable('*')
9
+ // setTracer(new LogTracer('Clips.text'))
10
+
11
+ describe('ClipsPlugin', () => {
12
+ let core: any
13
+ let mediaControl: any
14
+ let clips: Clips
15
+ beforeEach(() => {
16
+ core = createMockCore({
17
+ clips: {
18
+ text: `
19
+ 00:00:00 Introduction
20
+ 00:05:00 Main part
21
+ 00:15:00 Conclusion
22
+ `,
23
+ },
24
+ })
25
+ mediaControl = createMockMediaControl(core)
26
+ core.getPlugin.mockImplementation((name: string) => {
27
+ if (name === 'media_control') return mediaControl
28
+ return null
29
+ })
30
+ clips = new Clips(core)
31
+ core.emit(Events.CORE_READY)
32
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
33
+ vi.spyOn(core.activeContainer.$el, 'width').mockReturnValue(600)
34
+ core.activeContainer.emit(Events.CONTAINER_TIMEUPDATE, {
35
+ current: 0,
36
+ total: 1200,
37
+ })
38
+ })
39
+ it('should render indicator', () => {
40
+ expect(clips.el.innerHTML).toMatchSnapshot()
41
+ })
42
+ it('should render notches on the seek bar', () => {
43
+ const svg = clips.$el.find('svg')
44
+ expect(svg).toBeDefined()
45
+ expect(svg?.find('rect').length).toBe(3)
46
+ })
47
+ describe('as time progresses', () => {
48
+ describe.each([
49
+ [60, 'Introduction'],
50
+ [310, 'Main part'],
51
+ [1001, 'Conclusion'],
52
+ ])('@%s', (time, expected) => {
53
+ beforeEach(() => {
54
+ core.activeContainer.emit(Events.CONTAINER_TIMEUPDATE, {
55
+ current: time,
56
+ total: 1200,
57
+ })
58
+ })
59
+ it(`text should be "${expected}"`, () => {
60
+ expect(clips.$el.find('#clips-text').text()).toBe(expected)
61
+ })
62
+ })
63
+ })
64
+ describe('when media control is rendered', () => {
65
+ beforeEach(() => {
66
+ mediaControl.trigger(Events.MEDIACONTROL_RENDERED)
67
+ })
68
+ it('should mount the indicator', () => {
69
+ expect(mediaControl.mount).toHaveBeenCalledWith('clips', clips.$el)
70
+ })
71
+ })
72
+ })
@@ -0,0 +1,14 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`ClipsPlugin > should render indicator 1`] = `
4
+ "<div class="media-clip-text" id="clips-text">Introduction</div><svg width="0" height="0">
5
+ <defs>
6
+ <clipPath id="myClip">
7
+ <rect x="0" y="0" width="148" height="30"></rect>
8
+ <rect x="150" y="0" width="298" height="30"></rect>
9
+ <rect x="450" y="0" width="148" height="30"></rect>
10
+ </clipPath></defs></svg><style>
11
+ .bar-container[data-seekbar] {
12
+ clip-path: url("#myClip");
13
+ }</style>"
14
+ `;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Clip description.
3
+ * @beta
4
+ */
5
+ export type ClipDesc = {
6
+ /**
7
+ * Start time of the clip in the video timeline, s.
8
+ */
9
+ start: number
10
+ /**
11
+ * Text to display over the seekbar.
12
+ */
13
+ text: string
14
+ /**
15
+ * End time of the clip (start time of the next clip).
16
+ */
17
+ end: number
18
+ /**
19
+ * Index of the clip.
20
+ */
21
+ // index: number
22
+ }
@@ -0,0 +1,54 @@
1
+ import { ClipDesc } from './types.js'
2
+ import { parseClipTime } from '../utils.js'
3
+
4
+ type ClipItem = {
5
+ start: number
6
+ text: string
7
+ }
8
+
9
+ export function parseClips(text: string): ClipDesc[] {
10
+ const clipsArr = text
11
+ .split('\n')
12
+ .map((val: string) => {
13
+ const matchRes = val.match(/(((\d+:)?\d+:)?\d+) (.+)/i)
14
+ return matchRes
15
+ ? {
16
+ start: parseClipTime(matchRes[1]),
17
+ text: matchRes[4],
18
+ }
19
+ : null
20
+ })
21
+ .filter((clip: ClipItem | null) => clip !== null)
22
+ .sort((a: ClipItem, b: ClipItem) => a.start - b.start)
23
+ return clipsArr.map((clip: ClipItem, index: number) => ({
24
+ start: clip.start,
25
+ text: clip.text,
26
+ end: index < clipsArr.length - 1 ? clipsArr[index + 1].start : 0,
27
+ }))
28
+ }
29
+
30
+ export function buildSvg(clips: ClipDesc[], duration: number, barWidth: number): string {
31
+ let svg =
32
+ '<svg width="0" height="0">\n' + '<defs>\n' + '<clipPath id="myClip">\n'
33
+ let rightEdge = 0
34
+
35
+ clips.forEach((val) => {
36
+ const end = val.end || duration
37
+
38
+ const chunkWidth = Math.round(((end - val.start) * barWidth) / duration)
39
+
40
+ svg += `<rect x="${rightEdge}" y="0" width="${
41
+ chunkWidth - 2
42
+ }" height="30"/>\n`
43
+ rightEdge += chunkWidth
44
+ })
45
+
46
+ if (rightEdge < barWidth) {
47
+ svg += `<rect x="${rightEdge}" y="0" width="${
48
+ barWidth - rightEdge
49
+ }" height="30"/>\n`
50
+ }
51
+ svg += '</clipPath>' + '</defs>' + '</svg>'
52
+
53
+ return svg
54
+ }