@gcorevideo/player 2.20.6 → 2.20.7

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 (34) hide show
  1. package/dist/core.js +1 -1
  2. package/dist/index.css +662 -662
  3. package/dist/index.js +5143 -5123
  4. package/dist/plugins/index.css +524 -524
  5. package/dist/plugins/index.js +5111 -5096
  6. package/lib/plugins/bottom-gear/BottomGear.d.ts.map +1 -1
  7. package/lib/plugins/bottom-gear/BottomGear.js +5 -8
  8. package/lib/plugins/level-selector/LevelSelector.d.ts +15 -5
  9. package/lib/plugins/level-selector/LevelSelector.d.ts.map +1 -1
  10. package/lib/plugins/level-selector/LevelSelector.js +22 -22
  11. package/lib/plugins/media-control/MediaControl.d.ts +10 -0
  12. package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
  13. package/lib/plugins/media-control/MediaControl.js +11 -0
  14. package/lib/plugins/source-controller/SourceController.d.ts +1 -0
  15. package/lib/plugins/source-controller/SourceController.d.ts.map +1 -1
  16. package/lib/plugins/source-controller/SourceController.js +8 -4
  17. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.d.ts +7 -3
  18. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.d.ts.map +1 -1
  19. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.js +35 -27
  20. package/lib/testUtils.d.ts +5 -8
  21. package/lib/testUtils.d.ts.map +1 -1
  22. package/lib/testUtils.js +15 -9
  23. package/package.json +1 -1
  24. package/src/plugins/bottom-gear/BottomGear.ts +5 -7
  25. package/src/plugins/bottom-gear/__tests__/BottomGear.test.ts +36 -0
  26. package/src/plugins/bottom-gear/__tests__/__snapshots__/BottomGear.test.ts.snap +41 -0
  27. package/src/plugins/level-selector/LevelSelector.ts +50 -29
  28. package/src/plugins/level-selector/__tests__/LevelSelector.test.ts +15 -16
  29. package/src/plugins/media-control/MediaControl.ts +11 -0
  30. package/src/plugins/source-controller/SourceController.ts +9 -4
  31. package/src/plugins/source-controller/__tests__/SourceController.test.ts +35 -1
  32. package/src/plugins/spinner-three-bounce/SpinnerThreeBounce.ts +80 -57
  33. package/src/testUtils.ts +16 -9
  34. package/tsconfig.tsbuildinfo +1 -1
@@ -10,6 +10,7 @@ import '../../../assets/bottom-gear/gear-sub-menu.scss';
10
10
  import gearIcon from '../../../assets/icons/new/gear.svg';
11
11
  import gearHdIcon from '../../../assets/icons/new/gear-hd.svg';
12
12
  import { ZeptoResult } from '../../utils/types.js';
13
+ import { MediaControlEvents } from '../media-control/MediaControl';
13
14
 
14
15
  const VERSION = '2.19.12';
15
16
 
@@ -127,13 +128,9 @@ export class BottomGear extends UICorePlugin {
127
128
  }
128
129
 
129
130
  private highDefinitionUpdate(isHd: boolean) {
130
- trace(`${this.name} highDefinitionUpdate`, { isHd });
131
+ trace(`${T} highDefinitionUpdate`, { isHd });
131
132
  this.isHd = isHd;
132
- if (isHd) {
133
- this.$el.find('.gear-icon').html(gearHdIcon);
134
- } else {
135
- this.$el.find('.gear-icon').html(gearIcon);
136
- }
133
+ this.$el.find('.gear-icon').html(isHd ? gearHdIcon : gearIcon);
137
134
  }
138
135
 
139
136
  /**
@@ -154,7 +151,8 @@ export class BottomGear extends UICorePlugin {
154
151
 
155
152
  mediaControl.getElement('gear')?.html(this.el);
156
153
  this.core.trigger('gear:rendered'); // @deprecated
157
- mediaControl.trigger(GearEvents.MEDIACONTROL_GEAR_RENDERED);
154
+ mediaControl.trigger(GearEvents.MEDIACONTROL_GEAR_RENDERED); // TODO drop
155
+ mediaControl.trigger(MediaControlEvents.MEDIACONTROL_GEAR_RENDERED);
158
156
  return this;
159
157
  }
160
158
 
@@ -0,0 +1,36 @@
1
+ import { MockedFunction, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import { BottomGear } from '../BottomGear'
4
+ import { createMockCore, createMockMediaControl } from '../../../testUtils'
5
+ import { MediaControlEvents } from '../../media-control/MediaControl'
6
+
7
+ describe('BottomGear', () => {
8
+ let mediaControl: any
9
+ let core: any
10
+ let bottomGear: BottomGear
11
+ let onGearRendered: MockedFunction<() => void>
12
+ beforeEach(() => {
13
+ core = createMockCore()
14
+ mediaControl = createMockMediaControl(core)
15
+ core.getPlugin = vi
16
+ .fn()
17
+ .mockImplementation((name) =>
18
+ name === 'media_control' ? mediaControl : null,
19
+ )
20
+ bottomGear = new BottomGear(core)
21
+ onGearRendered = vi.fn()
22
+ mediaControl.on(MediaControlEvents.MEDIACONTROL_GEAR_RENDERED, onGearRendered, null)
23
+ bottomGear.render()
24
+ })
25
+ it('should render', () => {
26
+ expect(bottomGear.el.innerHTML).toMatchSnapshot()
27
+ })
28
+ it('should attach to media control', () => {
29
+ const gearElement = mediaControl.getElement('gear')
30
+ expect(gearElement[0].innerHTML).not.toEqual('')
31
+ expect(gearElement[0].innerHTML).toMatchSnapshot()
32
+ })
33
+ it('should emit event', () => {
34
+ expect(onGearRendered).toHaveBeenCalled()
35
+ })
36
+ })
@@ -0,0 +1,41 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`BottomGear > should attach to media control 1`] = `
4
+ "<div class="bottom_gear" data-track-selector=""><div class="media-control-gear" data-="">
5
+ <button type="button" class="button-gear gplayer-lite-btn gcore-skin-button-color" data-gear-button="-1">
6
+ <span class="gear-icon">/assets/icons/new/gear.svg</span>
7
+ </button>
8
+ <div class="gear-wrapper gcore-skin-bg-color">
9
+ <ul class="gear-options-list">
10
+
11
+ <li data-quality=""></li>
12
+
13
+ <li data-rate=""></li>
14
+
15
+ <li data-nerd=""></li>
16
+
17
+ </ul>
18
+ </div>
19
+ </div>
20
+ </div>"
21
+ `;
22
+
23
+ exports[`BottomGear > should render 1`] = `
24
+ "<div class="media-control-gear" data-="">
25
+ <button type="button" class="button-gear gplayer-lite-btn gcore-skin-button-color" data-gear-button="-1">
26
+ <span class="gear-icon">/assets/icons/new/gear.svg</span>
27
+ </button>
28
+ <div class="gear-wrapper gcore-skin-bg-color">
29
+ <ul class="gear-options-list">
30
+
31
+ <li data-quality=""></li>
32
+
33
+ <li data-rate=""></li>
34
+
35
+ <li data-nerd=""></li>
36
+
37
+ </ul>
38
+ </div>
39
+ </div>
40
+ "
41
+ `;
@@ -1,5 +1,6 @@
1
1
  import { Events, template, UICorePlugin } from '@clappr/core'
2
2
  import { reportError, trace } from '@gcorevideo/utils'
3
+ import assert from 'assert'
3
4
 
4
5
  import { type QualityLevel } from '../../playback.types.js'
5
6
  import { CLAPPR_VERSION } from '../../build.js'
@@ -14,12 +15,26 @@ import arrowRightIcon from '../../../assets/icons/new/arrow-right.svg'
14
15
  import arrowLeftIcon from '../../../assets/icons/new/arrow-left.svg'
15
16
  import checkIcon from '../../../assets/icons/new/check.svg'
16
17
  import '../../../assets/level-selector/style.scss'
17
- import assert from 'assert'
18
-
18
+ import { MediaControl } from '../media-control/MediaControl.js'
19
19
 
20
20
  const T = 'plugins.level_selector'
21
21
  const VERSION = '2.19.4'
22
22
 
23
+ export interface LevelSelectorPluginSettings {
24
+ /**
25
+ * The maximum resolution to allow in the level selector.
26
+ */
27
+ restrictResolution?: number
28
+ /**
29
+ * The labels to show in the level selector.
30
+ * @example
31
+ * ```ts
32
+ * { 360: 'SD', 720: 'HD' }
33
+ * ```
34
+ */
35
+ labels?: Record<number, string>
36
+ }
37
+
23
38
  /**
24
39
  * A {@link MediaControl | media control} plugin that provides a UI to control the quality level of the playback.
25
40
  * @beta
@@ -35,11 +50,7 @@ const VERSION = '2.19.4'
35
50
  *
36
51
  * When clicked, it shows a list of quality levels to choose from.
37
52
  *
38
- * Configuration options:
39
- *
40
- * - `labels`: The labels to show in the level selector. [video resolution]: string
41
- *
42
- * - `restrictResolution`: The maximum resolution to allow in the level selector.
53
+ * Configuration options - {@link LevelSelectorPluginSettings}
43
54
  *
44
55
  * @example
45
56
  * ```ts
@@ -62,7 +73,8 @@ export class LevelSelector extends UICorePlugin {
62
73
 
63
74
  private isOpen = false
64
75
 
65
- private static readonly buttonTemplate: TemplateFunction = template(buttonHtml)
76
+ private static readonly buttonTemplate: TemplateFunction =
77
+ template(buttonHtml)
66
78
 
67
79
  private static readonly listTemplate: TemplateFunction = template(listHtml)
68
80
 
@@ -113,8 +125,13 @@ export class LevelSelector extends UICorePlugin {
113
125
  * @internal
114
126
  */
115
127
  override bindEvents() {
116
- this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, () => this.bindPlaybackEvents())
117
- this.listenTo(this.core, 'gear:rendered', this.render)
128
+ const mediaControl = this.core.getPlugin('media_control') as MediaControl
129
+ assert(mediaControl, 'media_control plugin is required')
130
+ this.listenTo(
131
+ this.core,
132
+ Events.CORE_ACTIVE_CONTAINER_CHANGED,
133
+ this.bindPlaybackEvents,
134
+ )
118
135
  }
119
136
 
120
137
  private bindPlaybackEvents() {
@@ -123,8 +140,10 @@ export class LevelSelector extends UICorePlugin {
123
140
 
124
141
  const activePlayback = this.core.activePlayback
125
142
 
126
- this.listenTo(activePlayback, Events.PLAYBACK_LEVELS_AVAILABLE, (levels: QualityLevel[]) =>
127
- this.fillLevels(levels),
143
+ this.listenTo(
144
+ activePlayback,
145
+ Events.PLAYBACK_LEVELS_AVAILABLE,
146
+ this.fillLevels,
128
147
  )
129
148
  this.listenTo(
130
149
  activePlayback,
@@ -150,32 +169,27 @@ export class LevelSelector extends UICorePlugin {
150
169
  this.deferRender()
151
170
  },
152
171
  )
153
- if (activePlayback?.levels?.length > 0) {
172
+ if (activePlayback.levels?.length > 0) {
154
173
  this.fillLevels(activePlayback.levels)
155
174
  }
156
175
  }
157
176
 
158
177
  private onStop() {
159
178
  trace(`${T} onStop`)
160
- const currentPlayback = this.core.activePlayback
161
-
162
- this.listenToOnce(currentPlayback, Events.PLAYBACK_PLAY, () => {
163
- trace(`${T} on PLAYBACK_PLAY after stop`, { selectedLevelId: this.selectedLevelId })
164
- if (currentPlayback.getPlaybackType() === 'live') {
179
+ this.listenToOnce(this.core.activePlayback, Events.PLAYBACK_PLAY, () => {
180
+ trace(`${T} on PLAYBACK_PLAY after stop`, {
181
+ selectedLevelId: this.selectedLevelId,
182
+ })
183
+ if (this.core.activePlayback.getPlaybackType() === 'live') {
165
184
  if (this.selectedLevelId !== -1) {
166
- currentPlayback.currentLevel = this.selectedLevelId
185
+ this.core.activePlayback.currentLevel = this.selectedLevelId
167
186
  }
168
187
  }
169
188
  })
170
189
  }
171
190
 
172
191
  private shouldRender() {
173
- if (!this.core.activeContainer) {
174
- return false
175
- }
176
-
177
192
  const activePlayback = this.core.activePlayback
178
-
179
193
  if (!activePlayback) {
180
194
  return false
181
195
  }
@@ -192,8 +206,6 @@ export class LevelSelector extends UICorePlugin {
192
206
  * @internal
193
207
  */
194
208
  override render() {
195
- assert(this.core.getPlugin('bottom_gear'), 'bottom_gear plugin is required')
196
-
197
209
  if (!this.shouldRender()) {
198
210
  return this
199
211
  }
@@ -213,7 +225,10 @@ export class LevelSelector extends UICorePlugin {
213
225
  })
214
226
  this.$el.html(html)
215
227
  const gear = this.core.getPlugin('bottom_gear') as BottomGear
216
- gear.getElement('quality')?.html(this.el)
228
+ if (!gear) {
229
+ trace(`${T} renderButton: bottom_gear plugin not found`)
230
+ }
231
+ gear?.getElement('quality')?.html(this.el)
217
232
  }
218
233
  }
219
234
 
@@ -228,6 +243,7 @@ export class LevelSelector extends UICorePlugin {
228
243
  })
229
244
  this.$el.html(html)
230
245
  const gear = this.core.getPlugin('bottom_gear') as BottomGear
246
+ trace(`${T} renderDropdown: bottom_gear plugin not found`)
231
247
  gear?.setContent(this.el)
232
248
  }
233
249
 
@@ -236,7 +252,8 @@ export class LevelSelector extends UICorePlugin {
236
252
  return maxRes
237
253
  ? this.levels.findIndex(
238
254
  (level) =>
239
- (level.height > level.width ? level.width : level.height) === maxRes,
255
+ (level.height > level.width ? level.width : level.height) ===
256
+ maxRes,
240
257
  )
241
258
  : -1
242
259
  }
@@ -248,7 +265,11 @@ export class LevelSelector extends UICorePlugin {
248
265
  if (maxResolution) {
249
266
  this.removeAuto = true
250
267
  const initialLevel = levels
251
- .filter((level) => (level.width > level.height ? level.height : level.width) <= maxResolution)
268
+ .filter(
269
+ (level) =>
270
+ (level.width > level.height ? level.height : level.width) <=
271
+ maxResolution,
272
+ )
252
273
  .pop()
253
274
  this.setLevel(initialLevel?.level ?? 0)
254
275
  }
@@ -1,9 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { UICorePlugin } from '@clappr/core'
2
+ import { $, UICorePlugin } from '@clappr/core'
3
3
  import FakeTimers from '@sinonjs/fake-timers'
4
4
  import { Logger, LogTracer, setTracer } from '@gcorevideo/utils'
5
5
  import { LevelSelector } from '../LevelSelector.js'
6
- import { createMockCore, createMockPlayback } from '../../../testUtils.js'
6
+ import {
7
+ createMockCore,
8
+ createMockMediaControl,
9
+ createMockPlayback,
10
+ } from '../../../testUtils.js'
11
+ import { MediaControlEvents } from '../../media-control/MediaControl.js'
7
12
 
8
13
  setTracer(new LogTracer('LevelSelector.test'))
9
14
  Logger.enable('*')
@@ -33,6 +38,8 @@ describe('LevelSelector', () => {
33
38
  let core: any
34
39
  let levelSelector: LevelSelector
35
40
  let activePlayback: any
41
+ let mediaControl: UICorePlugin
42
+ let bottomGear: UICorePlugin | null
36
43
  beforeEach(() => {
37
44
  clock = FakeTimers.install()
38
45
  })
@@ -41,10 +48,6 @@ describe('LevelSelector', () => {
41
48
  })
42
49
  describe('basically', () => {
43
50
  beforeEach(() => {
44
- // const activeContainer = createMockContainer()
45
- let mediaControl: UICorePlugin | null = null
46
- let bottomGear: UICorePlugin | null = null
47
- // TODO create mock core
48
51
  core = createMockCore({
49
52
  levelSelector: {
50
53
  // restrictResolution: 360,
@@ -61,7 +64,7 @@ describe('LevelSelector', () => {
61
64
  }
62
65
  return null
63
66
  })
64
- mediaControl = createMediaControl(core)
67
+ mediaControl = createMockMediaControl(core)
65
68
  bottomGear = createBottomGear(core)
66
69
  levelSelector = new LevelSelector(core)
67
70
  })
@@ -126,7 +129,7 @@ describe('LevelSelector', () => {
126
129
  }
127
130
  return null
128
131
  })
129
- mediaControl = createMediaControl(core)
132
+ mediaControl = createMockMediaControl(core)
130
133
  bottomGear = createBottomGear(core)
131
134
  levelSelector = new LevelSelector(core)
132
135
  })
@@ -219,17 +222,13 @@ expect.extend({
219
222
  },
220
223
  })
221
224
 
222
- function createMediaControl(core: any) {
223
- const mediaControl = new UICorePlugin(core)
224
- // @ts-ignore
225
- mediaControl.getElement = vi.fn().mockReturnValue(null)
226
- return mediaControl
227
- }
228
-
229
225
  function createBottomGear(core: any) {
230
226
  const bottomGear = new UICorePlugin(core)
227
+ const elemets = {
228
+ quality: $(document.createElement('div')),
229
+ }
231
230
  // @ts-ignore
232
- bottomGear.getElement = vi.fn().mockReturnValue(null)
231
+ bottomGear.getElement = vi.fn().mockImplementation((name) => elemets[name])
233
232
  // @ts-ignore
234
233
  bottomGear.setContent = vi.fn()
235
234
  return bottomGear
@@ -49,6 +49,17 @@ export type MediaControlElement =
49
49
  | 'seekBarContainer'
50
50
  | 'subtitlesSelector'
51
51
 
52
+ /**
53
+ * Custom events emitted by the plugins to communicate with one another
54
+ * @beta
55
+ */
56
+ export enum MediaControlEvents {
57
+ /**
58
+ * Emitted when the gear menu is rendered
59
+ */
60
+ MEDIACONTROL_GEAR_RENDERED = 'mediacontrol:gear:rendered',
61
+ }
62
+
52
63
  const T = 'plugins.media_control'
53
64
 
54
65
  const LEFT_ORDER = [
@@ -90,6 +90,8 @@ export class SourceController extends CorePlugin {
90
90
 
91
91
  private active = false
92
92
 
93
+ private switching = false
94
+
93
95
  private sync: SyncFn = noSync
94
96
 
95
97
  /**
@@ -166,15 +168,18 @@ export class SourceController extends CorePlugin {
166
168
  description: error?.description,
167
169
  level: error?.level,
168
170
  },
171
+ switching: this.switching,
169
172
  retrying: this.active,
170
173
  currentSource: this.sourcesList[this.currentSourceIndex],
171
174
  })
175
+ if (this.switching) {
176
+ return
177
+ }
172
178
  switch (error.code) {
173
179
  case PlaybackErrorCode.MediaSourceUnavailable:
174
180
  this.core.activeContainer?.getPlugin('poster_custom')?.disable()
175
- setTimeout(() => this.retryPlayback(), 0)
181
+ this.retryPlayback()
176
182
  break
177
- // TODO handle other errors
178
183
  default:
179
184
  break
180
185
  }
@@ -187,7 +192,6 @@ export class SourceController extends CorePlugin {
187
192
  })
188
193
  if (this.active) {
189
194
  this.reset()
190
- // TODO make poster reset its state on enable
191
195
  this.core.activeContainer?.getPlugin('poster_custom')?.enable()
192
196
  this.core.activeContainer?.getPlugin('spinner')?.hide()
193
197
  }
@@ -205,6 +209,7 @@ export class SourceController extends CorePlugin {
205
209
  currentSource: this.sourcesList[this.currentSourceIndex],
206
210
  })
207
211
  this.active = true
212
+ this.switching = true
208
213
  this.core.activeContainer?.getPlugin('spinner')?.show(0)
209
214
  this.getNextMediaSource().then((nextSource: PlayerMediaSourceDesc) => {
210
215
  trace(`${T} retryPlayback syncing...`, {
@@ -213,12 +218,12 @@ export class SourceController extends CorePlugin {
213
218
  const rnd = RETRY_DELAY_BLUR * Math.random()
214
219
  this.sync(() => {
215
220
  trace(`${T} retryPlayback loading...`)
221
+ this.switching = false
216
222
  this.core.load(nextSource.source, nextSource.mimeType)
217
223
  trace(`${T} retryPlayback loaded`, {
218
224
  nextSource,
219
225
  })
220
226
  setTimeout(() => {
221
- // this.core.activePlayback.consent()
222
227
  this.core.activePlayback.play()
223
228
  trace(`${T} retryPlayback playing`)
224
229
  }, rnd)
@@ -74,7 +74,6 @@ describe('SourceController', () => {
74
74
  describe('on fatal playback failure', () => {
75
75
  let core: any
76
76
  let nextPlayback: any
77
-
78
77
  describe('basically', () => {
79
78
  beforeEach(() => {
80
79
  core = createMockCore({
@@ -215,5 +214,40 @@ describe('SourceController', () => {
215
214
  })
216
215
  })
217
216
  })
217
+ describe('given that playback triggers many errors in a row', () => {
218
+ beforeEach(async () => {
219
+ core = createMockCore({
220
+ sources: MOCK_SOURCES,
221
+ })
222
+ const _ = new SourceController(core)
223
+ core.emit('core:ready')
224
+ core.emit('core:active:container:changed')
225
+ core.activePlayback.emit('playback:error', {
226
+ code: PlaybackErrorCode.MediaSourceUnavailable,
227
+ })
228
+ await clock.tickAsync(1)
229
+ core.activePlayback.emit('playback:error', {
230
+ code: PlaybackErrorCode.MediaSourceUnavailable,
231
+ })
232
+ await clock.tickAsync(1)
233
+ core.activePlayback.emit('playback:error', {
234
+ code: PlaybackErrorCode.MediaSourceUnavailable,
235
+ })
236
+ await clock.tickAsync(1)
237
+ nextPlayback = createMockPlayback()
238
+ vi.spyOn(nextPlayback, 'consent')
239
+ vi.spyOn(nextPlayback, 'play')
240
+ core.activePlayback = nextPlayback
241
+ })
242
+ it('should run handler only once', async () => {
243
+ expect(core.load).not.toHaveBeenCalled()
244
+ await clock.tickAsync(1000)
245
+ expect(core.load).toHaveBeenCalledTimes(1)
246
+ expect(core.load).toHaveBeenCalledWith(
247
+ MOCK_SOURCES[1].source,
248
+ MOCK_SOURCES[1].mimeType,
249
+ )
250
+ })
251
+ })
218
252
  })
219
253
  })