@gcorevideo/player 2.22.2 → 2.22.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.
Files changed (35) hide show
  1. package/assets/audio-selector/style.scss +4 -2
  2. package/assets/audio-selector/track-selector.ejs +2 -2
  3. package/assets/media-control/container.scss +1 -1
  4. package/assets/spinner-three-bounce/spinner.scss +1 -1
  5. package/dist/core.js +1 -1
  6. package/dist/index.css +1374 -1372
  7. package/dist/index.js +76 -95
  8. package/dist/player.d.ts +6 -3
  9. package/dist/plugins/index.css +571 -569
  10. package/dist/plugins/index.js +47 -59
  11. package/docs/api/player.md +1 -1
  12. package/docs/api/player.sourcecontroller.md +1 -1
  13. package/docs/api/player.spinnerthreebounce.md +1 -1
  14. package/lib/index.plugins.d.ts +2 -0
  15. package/lib/index.plugins.d.ts.map +1 -1
  16. package/lib/index.plugins.js +2 -0
  17. package/lib/plugins/audio-selector/AudioSelector.d.ts +3 -9
  18. package/lib/plugins/audio-selector/AudioSelector.d.ts.map +1 -1
  19. package/lib/plugins/audio-selector/AudioSelector.js +34 -57
  20. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.d.ts +1 -0
  21. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.d.ts.map +1 -1
  22. package/lib/plugins/spinner-three-bounce/SpinnerThreeBounce.js +1 -0
  23. package/lib/testUtils.d.ts +2 -0
  24. package/lib/testUtils.d.ts.map +1 -1
  25. package/lib/testUtils.js +2 -0
  26. package/package.json +1 -1
  27. package/src/index.plugins.ts +2 -0
  28. package/src/plugins/audio-selector/AudioSelector.ts +36 -72
  29. package/src/plugins/audio-selector/__tests__/AudioSelector.test.ts +176 -0
  30. package/src/plugins/audio-selector/__tests__/__snapshots__/AudioSelector.test.ts.snap +67 -0
  31. package/src/plugins/spinner-three-bounce/SpinnerThreeBounce.ts +1 -0
  32. package/src/testUtils.ts +2 -0
  33. package/temp/player.api.json +2 -2
  34. package/tsconfig.tsbuildinfo +1 -1
  35. package/assets/bottom-gear/bottomgear copy.ejs +0 -10
@@ -1,6 +1,5 @@
1
1
  import { Events, UICorePlugin, template } from '@clappr/core'
2
2
  import { AudioTrack } from '@clappr/core/types/base/playback/playback.js'
3
- import { trace } from '@gcorevideo/utils'
4
3
  import assert from 'assert'
5
4
 
6
5
  import { CLAPPR_VERSION } from '../../build.js'
@@ -11,9 +10,9 @@ import audioArrow from '../../../assets/icons/old/quality-arrow.svg'
11
10
  import { ZeptoResult } from '../../types.js'
12
11
  import { MediaControl } from '../media-control/MediaControl.js'
13
12
 
14
- const VERSION: string = '0.0.1'
13
+ const VERSION: string = '2.22.4'
15
14
 
16
- const T = 'plugins.audio_selector'
15
+ // const T = 'plugins.audiotracks'
17
16
 
18
17
  /**
19
18
  * `PLUGIN` that makes possible to switch audio tracks via the media control UI.
@@ -25,7 +24,7 @@ const T = 'plugins.audio_selector'
25
24
  *
26
25
  * - {@link MediaControl}
27
26
  */
28
- export class AudioSelector extends UICorePlugin {
27
+ export class AudioTracks extends UICorePlugin {
29
28
  private currentTrack: AudioTrack | null = null
30
29
 
31
30
  private tracks: AudioTrack[] = []
@@ -34,7 +33,7 @@ export class AudioSelector extends UICorePlugin {
34
33
  * @internal
35
34
  */
36
35
  get name() {
37
- return 'audio_selector'
36
+ return 'audio_selector' // TODO rename to audiotracks
38
37
  }
39
38
 
40
39
  /**
@@ -59,7 +58,6 @@ export class AudioSelector extends UICorePlugin {
59
58
  override get attributes() {
60
59
  return {
61
60
  class: 'media-control-audiotracks',
62
-
63
61
  }
64
62
  }
65
63
 
@@ -69,7 +67,7 @@ export class AudioSelector extends UICorePlugin {
69
67
  override get events() {
70
68
  return {
71
69
  'click [data-audiotracks-select]': 'onTrackSelect',
72
- 'click [data-audiotracks-button]': 'onShowLevelSelectMenu',
70
+ 'click #audiotracks-button': 'toggleContextMenu',
73
71
  }
74
72
  }
75
73
 
@@ -77,7 +75,7 @@ export class AudioSelector extends UICorePlugin {
77
75
  * @internal
78
76
  */
79
77
  override bindEvents() {
80
- this.listenTo(this.core, Events.CORE_READY, this.onCoreReady)
78
+ this.listenToOnce(this.core, Events.CORE_READY, this.onCoreReady)
81
79
  this.listenTo(
82
80
  this.core,
83
81
  Events.CORE_ACTIVE_CONTAINER_CHANGED,
@@ -86,41 +84,30 @@ export class AudioSelector extends UICorePlugin {
86
84
  }
87
85
 
88
86
  private onCoreReady() {
89
- trace(`${T} onCoreReady`)
90
87
  const mediaControl = this.core.getPlugin('media_control')
91
88
  assert(mediaControl, 'media_control plugin is required')
92
- this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, this.render)
93
- this.listenTo(
94
- mediaControl,
95
- Events.MEDIACONTROL_HIDE,
96
- this.hideSelectTrackMenu,
97
- )
89
+ this.listenTo(mediaControl, Events.MEDIACONTROL_RENDERED, () => {
90
+ mediaControl.putElement('audiotracks', this.$el)
91
+ })
92
+ this.listenTo(mediaControl, Events.MEDIACONTROL_HIDE, this.hideMenu)
98
93
  }
99
94
 
100
- private bindPlaybackEvents() {
101
- trace(`${T} bindPlaybackEvents`)
95
+ private onActiveContainerChanged() {
102
96
  this.currentTrack = null
103
- this.listenTo(this.core.activePlayback, Events.PLAYBACK_STOP, this.onStop)
104
- this.setupAudioTrackListeners()
105
- }
106
-
107
- private setupAudioTrackListeners() {
108
97
  this.listenTo(
109
- this.core.activePlayback,
110
- Events.PLAYBACK_AUDIO_AVAILABLE,
98
+ this.core.activeContainer,
99
+ Events.CONTAINER_AUDIO_AVAILABLE,
111
100
  (tracks: AudioTrack[]) => {
112
- trace(`${T} on PLAYBACK_AUDIO_AVAILABLE`, { audioTracks: tracks })
113
101
  this.currentTrack =
114
- tracks.find((track) => track.kind === 'main') ?? null
115
- this.fillTracks(tracks)
102
+ tracks.find((track) => track.kind === 'main') ?? null // TODO test
103
+ this.tracks = tracks
104
+ this.render()
116
105
  },
117
106
  )
118
-
119
107
  this.listenTo(
120
- this.core.activePlayback,
121
- Events.PLAYBACK_AUDIO_CHANGED,
108
+ this.core.activeContainer,
109
+ Events.CONTAINER_AUDIO_CHANGED,
122
110
  (track: AudioTrack) => {
123
- trace(`${T} PLAYBACK_AUDIO_CHANGED`, { audioTrack: track })
124
111
  this.currentTrack = track
125
112
  this.highlightCurrentTrack()
126
113
  this.buttonElement().removeClass('changing')
@@ -129,24 +116,10 @@ export class AudioSelector extends UICorePlugin {
129
116
  )
130
117
  }
131
118
 
132
- private onStop() {
133
- trace(`${T} onStop`)
134
- }
135
-
136
- private onActiveContainerChanged() {
137
- trace(`${T} onActiveContainerChanged`)
138
- this.bindPlaybackEvents()
139
- }
140
-
141
119
  private shouldRender() {
142
- if (!this.core.activePlayback) {
143
- return false
144
- }
145
-
146
- this.tracks = this.core.activePlayback.audioTracks
147
-
120
+ // Render is called from the parent class constructor so tracks aren't available
148
121
  // Only care if we have at least 2 to choose from
149
- return this.tracks && this.tracks.length > 1
122
+ return this.tracks?.length > 1
150
123
  }
151
124
 
152
125
  /**
@@ -159,52 +132,40 @@ export class AudioSelector extends UICorePlugin {
159
132
 
160
133
  const mediaControl = this.core.getPlugin('media_control') as MediaControl
161
134
  this.$el.html(
162
- AudioSelector.template({ tracks: this.tracks, title: this.getTitle() }),
135
+ AudioTracks.template({
136
+ tracks: this.tracks,
137
+ title: this.getTitle(),
138
+ icon: audioArrow,
139
+ }),
163
140
  )
164
- this.$('.audio-arrow').append(audioArrow)
165
- mediaControl.putElement('audiotracks', this.el)
166
-
167
141
  this.updateText()
168
142
  this.highlightCurrentTrack()
169
143
 
170
144
  return this
171
145
  }
172
146
 
173
- private fillTracks(tracks: AudioTrack[]) {
174
- this.tracks = tracks
175
- this.render()
176
- }
177
-
178
- private findTrackBy(id: string) {
179
- return this.tracks.find((track) => track.id === id)
180
- }
181
-
182
147
  private onTrackSelect(event: MouseEvent) {
183
148
  const id = (event.target as HTMLElement)?.dataset?.audiotracksSelect
184
149
  if (id) {
185
150
  this.selectAudioTrack(id)
186
151
  }
187
- this.toggleContextMenu()
152
+ this.hideMenu()
188
153
  event.stopPropagation()
189
154
  return false
190
155
  }
191
156
 
192
157
  private selectAudioTrack(id: string) {
193
158
  this.startTrackSwitch()
194
- this.core.activePlayback.switchAudioTrack(id)
159
+ this.core.activeContainer.switchAudioTrack(id)
195
160
  this.updateText()
196
161
  }
197
162
 
198
- private onShowLevelSelectMenu() {
199
- this.toggleContextMenu()
200
- }
201
-
202
- private hideSelectTrackMenu() {
203
- ;(this.$('ul') as ZeptoResult).hide()
163
+ private hideMenu() {
164
+ this.$el.find('#audiotracks-select').addClass('hidden')
204
165
  }
205
166
 
206
167
  private toggleContextMenu() {
207
- ;(this.$('ul') as ZeptoResult).toggle()
168
+ this.$el.find('#audiotracks-select').toggleClass('hidden')
208
169
  }
209
170
 
210
171
  private buttonElement(): ZeptoResult {
@@ -218,14 +179,17 @@ export class AudioSelector extends UICorePlugin {
218
179
  private trackElement(id?: string): ZeptoResult {
219
180
  return (
220
181
  this.$(
221
- 'ul a' +
222
- (id !== undefined ? '[data-audiotracks-select="' + id + '"]' : ''),
182
+ '#audiotracks-select a' +
183
+ (id !== undefined ? `[data-audiotracks-select="${id}"]` : ''),
223
184
  ) as ZeptoResult
224
185
  ).parent()
225
186
  }
226
187
 
227
188
  private getTitle(): string {
228
- return this.currentTrack?.label || ''
189
+ if (!this.currentTrack) {
190
+ return ''
191
+ }
192
+ return this.currentTrack.label || this.currentTrack.language
229
193
  }
230
194
 
231
195
  private startTrackSwitch() {
@@ -0,0 +1,176 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { Events } from '@clappr/core'
3
+
4
+ import { AudioTracks } from '../AudioSelector'
5
+
6
+ import { createMockCore, createMockMediaControl } from '../../../testUtils'
7
+ // import { LogTracer, Logger, setTracer } from '@gcorevideo/utils'
8
+
9
+ // Logger.enable('*')
10
+ // setTracer(new LogTracer('AudioSelector.test'))
11
+
12
+ const TRACKS = [
13
+ { id: '1', label: 'English', language: 'en', track: {} },
14
+ { id: '2', label: 'Spanish', language: 'es', track: {} },
15
+ ]
16
+
17
+ describe('AudioSelector', () => {
18
+ let core: any
19
+ let mediaControl: any
20
+ let audioSelector: AudioTracks
21
+ beforeEach(() => {
22
+ core = createMockCore()
23
+ mediaControl = createMockMediaControl(core)
24
+ core.getPlugin = vi.fn().mockImplementation((name: string) => {
25
+ if (name === 'media_control') return mediaControl
26
+ return null
27
+ })
28
+ audioSelector = new AudioTracks(core)
29
+ core.emit(Events.CORE_READY)
30
+ core.emit(Events.CORE_ACTIVE_CONTAINER_CHANGED, core.activeContainer)
31
+ })
32
+ describe('before media control is rendererd', () => {
33
+ beforeEach(() => {
34
+ emitTracksAvailable(core, TRACKS)
35
+ })
36
+ it('should not attach to the media control', () => {
37
+ expect(mediaControl.putElement).not.toHaveBeenCalledWith(
38
+ 'audiotracks',
39
+ expect.anything(),
40
+ )
41
+ })
42
+ })
43
+ describe('when media control is rendered', () => {
44
+ beforeEach(() => {
45
+ mediaControl.trigger(Events.MEDIACONTROL_RENDERED)
46
+ })
47
+ it('should attach to the media control', () => {
48
+ expect(mediaControl.putElement).toHaveBeenCalledWith(
49
+ 'audiotracks',
50
+ audioSelector.$el,
51
+ )
52
+ })
53
+ })
54
+ describe('when audio tracks are available', () => {
55
+ beforeEach(() => {
56
+ emitTracksAvailable(core, TRACKS)
57
+ })
58
+ it('should render the button', () => {
59
+ expect(audioSelector.$el.find('#audiotracks-button').length).toBe(1)
60
+ })
61
+ it('should render the menu hidden', () => {
62
+ expect(audioSelector.el.innerHTML).toMatchSnapshot()
63
+ expect(
64
+ audioSelector.$el.find('#audiotracks-select').hasClass('hidden'),
65
+ ).toBe(true)
66
+ const trackItems = audioSelector.$el.find('#audiotracks-select li')
67
+ expect(trackItems.length).toBe(2)
68
+ expect(trackItems.eq(0).text().trim()).toBe('English')
69
+ expect(trackItems.eq(1).text().trim()).toBe('Spanish')
70
+ })
71
+ describe('when the button is clicked', () => {
72
+ beforeEach(() => {
73
+ audioSelector.$el.find('#audiotracks-button').click()
74
+ })
75
+ it('should show the menu', () => {
76
+ expect(audioSelector.$el.html()).toMatchSnapshot()
77
+ expect(
78
+ audioSelector.$el.find('#audiotracks-select').hasClass('hidden'),
79
+ ).toBe(false)
80
+ })
81
+ describe('when an audio track is selected', () => {
82
+ beforeEach(() => {
83
+ audioSelector.$el
84
+ .find('#audiotracks-select [data-audiotracks-select="2"]')
85
+ .click()
86
+ })
87
+ it('should switch to the selected audio track', () => {
88
+ expect(core.activeContainer.switchAudioTrack).toHaveBeenCalledWith(
89
+ '2',
90
+ )
91
+ })
92
+ it('should hide the menu', () => {
93
+ expect(audioSelector.$el.html()).toMatchSnapshot()
94
+ expect(
95
+ audioSelector.$el.find('#audiotracks-select').hasClass('hidden'),
96
+ ).toBe(true)
97
+ })
98
+ it('should add changing class to the button', () => {
99
+ expect(
100
+ audioSelector.$el.find('#audiotracks-button').hasClass('changing'),
101
+ ).toBe(true)
102
+ })
103
+ describe('when current audio track changes', () => {
104
+ beforeEach(() => {
105
+ core.activePlayback.currentAudioTrack =
106
+ core.activePlayback.audioTracks[1]
107
+ core.activePlayback.emit(
108
+ Events.PLAYBACK_AUDIO_CHANGED,
109
+ core.activePlayback.currentAudioTrack,
110
+ )
111
+ core.activeContainer.emit(
112
+ Events.CONTAINER_AUDIO_CHANGED,
113
+ core.activePlayback.currentAudioTrack,
114
+ )
115
+ })
116
+ it('should update button class', () => {
117
+ expect(
118
+ audioSelector.$el
119
+ .find('#audiotracks-button')
120
+ .hasClass('changing'),
121
+ ).toBe(false)
122
+ })
123
+ it('should update button label', () => {
124
+ expect(
125
+ audioSelector.$el
126
+ .find('#audiotracks-button')
127
+ .text()
128
+ .replace(/\/assets.*\.svg/g, '')
129
+ .trim(),
130
+ ).toBe('Spanish')
131
+ })
132
+ it('should highlight the selected menu item', () => {
133
+ const selectedItem = audioSelector.$el.find(
134
+ '#audiotracks-select .current',
135
+ )
136
+ expect(selectedItem.text().trim()).toBe('Spanish')
137
+ expect(
138
+ selectedItem
139
+ .find('a[data-audiotracks-select]')
140
+ .hasClass('gcore-skin-active'),
141
+ ).toBe(true)
142
+ })
143
+ it('should unhighlight any previously highlighted menu item', () => {
144
+ expect(
145
+ audioSelector.$el.find('#audiotracks-select li.current').length,
146
+ ).toBe(1)
147
+ expect(
148
+ audioSelector.$el.find(
149
+ '#audiotracks-select a.gcore-skin-active[data-audiotracks-select]',
150
+ ).length,
151
+ ).toBe(1)
152
+ })
153
+ })
154
+ })
155
+ })
156
+ })
157
+ describe('when audio tracks are not available', () => {
158
+ it('should not render the button', () => {
159
+ expect(audioSelector.$el.find('#audiotracks-button').length).toBe(0)
160
+ expect(audioSelector.$el.find('#audiotracks-select').length).toBe(0)
161
+ })
162
+ })
163
+ })
164
+
165
+ function emitTracksAvailable(core: any, tracks: any[]) {
166
+ core.activePlayback.audioTracks = tracks
167
+ core.activePlayback.currentAudioTrack = tracks[0]
168
+ core.activePlayback.emit(
169
+ Events.PLAYBACK_AUDIO_AVAILABLE,
170
+ core.activePlayback.audioTracks,
171
+ )
172
+ core.activeContainer.emit(
173
+ Events.CONTAINER_AUDIO_AVAILABLE,
174
+ core.activePlayback.audioTracks,
175
+ )
176
+ }
@@ -0,0 +1,67 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`AudioSelector > when audio tracks are available > should render the menu hidden 1`] = `
4
+ "<button data-audiotracks-button="" class="gcore-skin-button-color" id="audiotracks-button">
5
+ <span class="audio-text"></span> <span class="audio-arrow">/assets/icons/old/quality-arrow.svg</span>
6
+ </button>
7
+ <ul class="gcore-skin-bg-color menu hidden" id="audiotracks-select">
8
+
9
+ <li class="">
10
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="1">
11
+ English
12
+ </a>
13
+ </li>
14
+
15
+ <li class="">
16
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="2">
17
+ Spanish
18
+ </a>
19
+ </li>
20
+
21
+ </ul>
22
+ "
23
+ `;
24
+
25
+ exports[`AudioSelector > when audio tracks are available > when the button is clicked > should show the menu 1`] = `
26
+ "<button data-audiotracks-button="" class="gcore-skin-button-color" id="audiotracks-button">
27
+ <span class="audio-text"></span> <span class="audio-arrow">/assets/icons/old/quality-arrow.svg</span>
28
+ </button>
29
+ <ul class="gcore-skin-bg-color menu" id="audiotracks-select">
30
+
31
+ <li class="">
32
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="1">
33
+ English
34
+ </a>
35
+ </li>
36
+
37
+ <li class="">
38
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="2">
39
+ Spanish
40
+ </a>
41
+ </li>
42
+
43
+ </ul>
44
+ "
45
+ `;
46
+
47
+ exports[`AudioSelector > when audio tracks are available > when the button is clicked > when an audio track is selected > should hide the menu 1`] = `
48
+ "<button data-audiotracks-button="" class="gcore-skin-button-color changing" id="audiotracks-button">
49
+ <span class="audio-text"></span> <span class="audio-arrow">/assets/icons/old/quality-arrow.svg</span>
50
+ </button>
51
+ <ul class="gcore-skin-bg-color menu hidden" id="audiotracks-select">
52
+
53
+ <li class="">
54
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="1">
55
+ English
56
+ </a>
57
+ </li>
58
+
59
+ <li class="">
60
+ <a href="#" class="gcore-skin-text-color" data-audiotracks-select="2">
61
+ Spanish
62
+ </a>
63
+ </li>
64
+
65
+ </ul>
66
+ "
67
+ `;
@@ -36,6 +36,7 @@ export enum SpinnerEvents {
36
36
  * `PLUGIN` that shows a pending operation indicator when playback is buffering or in a similar state.
37
37
  * @public
38
38
  * @remarks
39
+ * It is aliased as `Spinner` for convenience.
39
40
  * Events emitted - {@link SpinnerEvents}
40
41
  * Other plugins can use {@link SpinnerThreeBounce.show | show} and {@link SpinnerThreeBounce.hide | hide} methods to
41
42
  * implement custom pending/progress indication scenarios.
package/src/testUtils.ts CHANGED
@@ -152,6 +152,7 @@ export function createMockPlayback(name = 'mock') {
152
152
  canAutoPlay: vi.fn().mockImplementation(() => true),
153
153
  onResize: vi.fn().mockImplementation(() => true),
154
154
  setPlaybackRate: vi.fn(),
155
+ switchAudioTrack: vi.fn(),
155
156
  trigger: emitter.emit,
156
157
  })
157
158
  }
@@ -171,6 +172,7 @@ export function createMockContainer(playback: any = createMockPlayback()) {
171
172
  isPlaying: vi.fn().mockReturnValue(false),
172
173
  play: vi.fn(),
173
174
  seek: vi.fn(),
175
+ switchAudioTrack: vi.fn(),
174
176
  trigger: emitter.emit,
175
177
  })
176
178
  }
@@ -7791,7 +7791,7 @@
7791
7791
  {
7792
7792
  "kind": "Class",
7793
7793
  "canonicalReference": "@gcorevideo/player!SourceController:class",
7794
- "docComment": "/**\n * `PLUGIN` that is responsible for managing the automatic failover between media sources.\n *\n * @remarks\n *\n * Have a look at the {@link https://miro.com/app/board/uXjVLiN15tY=/?share_link_id=390327585787 | source failover diagram} for the details on how sources ordering and selection works. Below is a simplified diagram:\n * ```markdown\n * sources_list:\n * - a.mpd | +--------------------+\n * - b.m3u8 |--->| init |\n * - ... | |--------------------|\n * | current_source = 0 |\n * +--------------------+\n * |\n * | source = a.mpd\n * | playback = dash.js\n * v\n * +------------------+\n * +-->| load source |\n * | +---------|--------+\n * | v\n * | +------------------+\n * | | play |\n * | +---------|--------+\n * | |\n * | v\n * | +-----------------------+\n * | | on playback_error |\n * | |-----------------------|\n * | | current_source = |\n * | | (current_source + 1) |\n * | | % len sources_list |\n * | | |\n * | | delay 1..3s |\n * | +---------------|-------+\n * | |\n * | source=b.m3u8 |\n * | playback=hls.js |\n * +-------------------+\n *\n * ```\n *\n * This plugin does not expose any public methods apart from required by the Clappr plugin interface. It is supposed to work autonomously.\n *\n * @example\n * ```ts\n * import { SourceController } from '@gcorevideo/player'\n *\n * Player.registerPlugin(SourceController)\n * ```\n *\n * @public\n */\n",
7794
+ "docComment": "/**\n * `PLUGIN` that is managing the automatic failover between media sources.\n *\n * @remarks\n *\n * Have a look at the {@link https://miro.com/app/board/uXjVLiN15tY=/?share_link_id=390327585787 | source failover diagram} for the details on how sources ordering and selection works. Below is a simplified diagram:\n * ```markdown\n * sources_list:\n * - a.mpd | +--------------------+\n * - b.m3u8 |--->| init |\n * - ... | |--------------------|\n * | current_source = 0 |\n * +--------------------+\n * |\n * | source = a.mpd\n * | playback = dash.js\n * v\n * +------------------+\n * +-->| load source |\n * | +---------|--------+\n * | v\n * | +------------------+\n * | | play |\n * | +---------|--------+\n * | |\n * | v\n * | +-----------------------+\n * | | on playback_error |\n * | |-----------------------|\n * | | current_source = |\n * | | (current_source + 1) |\n * | | % len sources_list |\n * | | |\n * | | delay 1..3s |\n * | +---------------|-------+\n * | |\n * | source=b.m3u8 |\n * | playback=hls.js |\n * +-------------------+\n *\n * ```\n *\n * This plugin does not expose any public methods apart from required by the Clappr plugin interface. It is supposed to work autonomously.\n *\n * @example\n * ```ts\n * import { SourceController } from '@gcorevideo/player'\n *\n * Player.registerPlugin(SourceController)\n * ```\n *\n * @public\n */\n",
7795
7795
  "excerptTokens": [
7796
7796
  {
7797
7797
  "kind": "Content",
@@ -7894,7 +7894,7 @@
7894
7894
  {
7895
7895
  "kind": "Class",
7896
7896
  "canonicalReference": "@gcorevideo/player!SpinnerThreeBounce:class",
7897
- "docComment": "/**\n * `PLUGIN` that shows a pending operation indicator when playback is buffering or in a similar state.\n *\n * @remarks\n *\n * Events emitted- {@link SpinnerEvents} Other plugins can use {@link SpinnerThreeBounce.show | show} and {@link SpinnerThreeBounce.hide | hide} methods to implement custom pending/progress indication scenarios.\n *\n * @public\n */\n",
7897
+ "docComment": "/**\n * `PLUGIN` that shows a pending operation indicator when playback is buffering or in a similar state.\n *\n * @remarks\n *\n * It is aliased as `Spinner` for convenience. Events emitted - {@link SpinnerEvents} Other plugins can use {@link SpinnerThreeBounce.show | show} and {@link SpinnerThreeBounce.hide | hide} methods to implement custom pending/progress indication scenarios.\n *\n * @public\n */\n",
7898
7898
  "excerptTokens": [
7899
7899
  {
7900
7900
  "kind": "Content",