@gcorevideo/player 2.22.15 → 2.22.17

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 (124) hide show
  1. package/assets/clips/clips.ejs +1 -0
  2. package/assets/clips/clips.scss +23 -3
  3. package/assets/level-selector/list.ejs +9 -3
  4. package/assets/media-control/media-control.ejs +1 -9
  5. package/assets/media-control/media-control.scss +0 -25
  6. package/assets/media-control/width370.scss +4 -4
  7. package/dist/core.js +6 -8
  8. package/dist/index.css +855 -855
  9. package/dist/index.js +609 -664
  10. package/dist/player.d.ts +426 -302
  11. package/dist/plugins/index.css +551 -551
  12. package/dist/plugins/index.js +648 -703
  13. package/docs/api/{player.audioselector.md → player.audiotracks.md} +3 -3
  14. package/docs/api/player.clapprstats.exportmetrics.md +1 -1
  15. package/docs/api/player.clapprstats.md +5 -15
  16. package/docs/api/player.clapprstatssettings.md +13 -0
  17. package/docs/api/{player.contextmenupluginsettings.preventshowcontextmenu.md → player.clips.destroy.md} +7 -3
  18. package/docs/api/{player.contextmenupluginsettings.label.md → player.clips.disable.md} +7 -3
  19. package/docs/api/player.clips.enable.md +18 -0
  20. package/docs/api/player.clips.md +170 -0
  21. package/docs/api/player.clips.render.md +18 -0
  22. package/docs/api/player.clips.supportedversion.md +16 -0
  23. package/docs/api/player.clips.version.md +14 -0
  24. package/docs/api/player.clipspluginsettings.md +2 -2
  25. package/docs/api/player.clipspluginsettings.text.md +1 -1
  26. package/docs/api/player.contextmenu.md +2 -0
  27. package/docs/api/player.contextmenupluginsettings.md +2 -40
  28. package/docs/api/{player.contextmenupluginsettings.url.md → player.contextmenupluginsettings.options.md} +3 -3
  29. package/docs/api/player.md +101 -37
  30. package/docs/api/player.mediacontrol.md +9 -15
  31. package/docs/api/{player.mediacontrol.getelement.md → player.mediacontrol.mount.md} +20 -7
  32. package/docs/api/player.mediacontrolelement.md +4 -2
  33. package/docs/api/player.mediacontrollayerelement.md +16 -0
  34. package/docs/api/player.mediacontrolleftelement.md +16 -0
  35. package/docs/api/player.mediacontrolrightelement.md +16 -0
  36. package/docs/api/player.mediacontrolsettings.md +23 -0
  37. package/docs/api/player.menuoption.md +21 -0
  38. package/docs/api/{player.clapprnerdstats._constructor_.md → player.nerdstats._constructor_.md} +3 -3
  39. package/docs/api/{player.clapprnerdstats.md → player.nerdstats.md} +5 -5
  40. package/docs/api/player.playbackrate.md +1 -1
  41. package/docs/api/player.playerconfig.md +1 -1
  42. package/docs/api/player.playerconfig.playbacktype.md +1 -1
  43. package/docs/api/player.qualitylevel.height.md +1 -1
  44. package/docs/api/player.qualitylevel.level.md +1 -1
  45. package/docs/api/player.qualitylevel.md +4 -4
  46. package/docs/api/player.qualitylevel.width.md +1 -1
  47. package/docs/api/{player.levelselector.events.md → player.qualitylevels.events.md} +2 -2
  48. package/docs/api/{player.levelselector.md → player.qualitylevels.md} +6 -6
  49. package/docs/api/{player.levelselectorpluginsettings.labels.md → player.qualitylevelspluginsettings.labels.md} +2 -2
  50. package/docs/api/{player.levelselectorpluginsettings.md → player.qualitylevelspluginsettings.md} +6 -6
  51. package/docs/api/{player.levelselectorpluginsettings.restrictresolution.md → player.qualitylevelspluginsettings.restrictresolution.md} +2 -2
  52. package/docs/api/player.timeposition.current.md +1 -1
  53. package/docs/api/player.timeposition.md +2 -2
  54. package/docs/api/player.timeposition.total.md +1 -1
  55. package/docs/api/player.timeprogress.md +6 -4
  56. package/docs/api/player.timevalue.md +1 -1
  57. package/lib/index.plugins.d.ts +2 -1
  58. package/lib/index.plugins.d.ts.map +1 -1
  59. package/lib/index.plugins.js +2 -1
  60. package/lib/playback/dash-playback/DashPlayback.d.ts.map +1 -1
  61. package/lib/playback/dash-playback/DashPlayback.js +5 -7
  62. package/lib/playback.types.d.ts +22 -9
  63. package/lib/playback.types.d.ts.map +1 -1
  64. package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.d.ts +4 -0
  65. package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.d.ts.map +1 -1
  66. package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.js +20 -23
  67. package/lib/plugins/clappr-nerd-stats/NerdStats.d.ts +83 -0
  68. package/lib/plugins/clappr-nerd-stats/NerdStats.d.ts.map +1 -0
  69. package/lib/plugins/clappr-nerd-stats/NerdStats.js +339 -0
  70. package/lib/plugins/clappr-stats/ClapprStats.d.ts +27 -32
  71. package/lib/plugins/clappr-stats/ClapprStats.d.ts.map +1 -1
  72. package/lib/plugins/clappr-stats/ClapprStats.js +94 -202
  73. package/lib/plugins/clappr-stats/types.d.ts +65 -24
  74. package/lib/plugins/clappr-stats/types.d.ts.map +1 -1
  75. package/lib/plugins/clappr-stats/types.js +37 -2
  76. package/lib/plugins/clappr-stats/utils.d.ts.map +1 -1
  77. package/lib/plugins/clappr-stats/utils.js +1 -2
  78. package/lib/plugins/clips/Clips.d.ts +21 -16
  79. package/lib/plugins/clips/Clips.d.ts.map +1 -1
  80. package/lib/plugins/clips/Clips.js +96 -98
  81. package/lib/plugins/clips/types.d.ts +19 -0
  82. package/lib/plugins/clips/types.d.ts.map +1 -0
  83. package/lib/plugins/clips/types.js +1 -0
  84. package/lib/plugins/clips/utils.d.ts +4 -0
  85. package/lib/plugins/clips/utils.d.ts.map +1 -0
  86. package/lib/plugins/clips/utils.js +36 -0
  87. package/lib/plugins/media-control/MediaControl.d.ts +4 -7
  88. package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
  89. package/lib/plugins/media-control/MediaControl.js +19 -31
  90. package/lib/plugins/utils.d.ts +9 -1
  91. package/lib/plugins/utils.d.ts.map +1 -1
  92. package/lib/plugins/utils.js +9 -10
  93. package/lib/plugins/vast-ads/loaderxml.js +2 -2
  94. package/lib/testUtils.d.ts +2 -1
  95. package/lib/testUtils.d.ts.map +1 -1
  96. package/lib/testUtils.js +5 -7
  97. package/package.json +1 -1
  98. package/src/index.plugins.ts +2 -1
  99. package/src/playback/dash-playback/DashPlayback.ts +5 -8
  100. package/src/playback.types.ts +23 -8
  101. package/src/plugins/clappr-nerd-stats/{ClapprNerdStats.ts → NerdStats.ts} +25 -30
  102. package/src/plugins/clappr-stats/ClapprStats.ts +242 -306
  103. package/src/plugins/clappr-stats/__tests__/ClapprStats.test.ts +133 -0
  104. package/src/plugins/clappr-stats/types.ts +72 -25
  105. package/src/plugins/clappr-stats/utils.ts +1 -2
  106. package/src/plugins/clips/Clips.ts +116 -135
  107. package/src/plugins/clips/__tests__/Clips.test.ts +72 -0
  108. package/src/plugins/clips/__tests__/__snapshots__/Clips.test.ts.snap +14 -0
  109. package/src/plugins/clips/types.ts +22 -0
  110. package/src/plugins/clips/utils.ts +54 -0
  111. package/src/plugins/error-screen/__tests__/ErrorScreen.test.ts +3 -4
  112. package/src/plugins/level-selector/__tests__/__snapshots__/QualityLevels.test.ts.snap +18 -18
  113. package/src/plugins/media-control/MediaControl.ts +31 -58
  114. package/src/plugins/media-control/__tests__/__snapshots__/MediaControl.test.ts.snap +7 -35
  115. package/src/plugins/subtitles/__tests__/ClosedCaptions.test.ts +1 -0
  116. package/src/plugins/utils.ts +9 -7
  117. package/src/plugins/vast-ads/loaderxml.ts +2 -2
  118. package/src/testUtils.ts +5 -7
  119. package/temp/player.api.json +693 -471
  120. package/tsconfig.tsbuildinfo +1 -1
  121. package/docs/api/player.clapprstats.setupdatemetrics.md +0 -56
  122. package/docs/api/player.clipsplugin.gettext.md +0 -58
  123. package/docs/api/player.clipsplugin.md +0 -59
  124. package/docs/api/player.mediacontrol.handlecustomarea.md +0 -52
@@ -0,0 +1,133 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { Events as CoreEvents } from '@clappr/core'
3
+ import FakeTimers from '@sinonjs/fake-timers'
4
+
5
+ import { ClapprStats } from '../ClapprStats'
6
+ import { createMockCore } from '../../../testUtils'
7
+ import { Chronograph, ClapprStatsEvents, Counter } from '../types'
8
+
9
+ describe('ClapprStats', () => {
10
+ let core: any
11
+ let stats: ClapprStats
12
+ let onReport: any
13
+ let clock: FakeTimers.InstalledClock
14
+ beforeEach(() => {
15
+ core = createMockCore()
16
+ stats = new ClapprStats(core.activeContainer)
17
+ clock = FakeTimers.install()
18
+ })
19
+ afterEach(() => {
20
+ clock.uninstall()
21
+ })
22
+ describe('time measurements', () => {
23
+ describe('startup', () => {
24
+ beforeEach(() => {
25
+ vi.spyOn(performance, 'now').mockReturnValue(100)
26
+ core.activeContainer.playback.emit(CoreEvents.PLAYBACK_PLAY_INTENT)
27
+ vi.spyOn(performance, 'now').mockReturnValue(255)
28
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
29
+ })
30
+ it('should measure', () => {
31
+ const metrics = stats.exportMetrics()
32
+ expect(metrics.chrono[Chronograph.Startup]).toBe(155)
33
+ // expect(metrics.times[Chronograph.Session]).toBe(155)
34
+ })
35
+ })
36
+ describe('watch', () => {
37
+ beforeEach(() => {
38
+ vi.spyOn(performance, 'now').mockReturnValue(100)
39
+ core.activeContainer.playback.emit(CoreEvents.PLAYBACK_PLAY_INTENT)
40
+ vi.spyOn(performance, 'now').mockReturnValue(150)
41
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
42
+ vi.spyOn(performance, 'now').mockReturnValue(3000)
43
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PAUSE)
44
+ vi.spyOn(performance, 'now').mockReturnValue(4900)
45
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
46
+ vi.spyOn(performance, 'now').mockReturnValue(5900)
47
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PAUSE)
48
+ vi.spyOn(performance, 'now').mockReturnValue(6900)
49
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
50
+ })
51
+ it('should measure cumulative play and pause durations', () => {
52
+ const metrics = stats.exportMetrics()
53
+ expect(metrics.chrono[Chronograph.Watch]).toBe(3850)
54
+ expect(metrics.chrono[Chronograph.Pause]).toBe(2900)
55
+ })
56
+ })
57
+ describe('buffering', () => {
58
+ beforeEach(() => {
59
+ vi.spyOn(performance, 'now').mockReturnValue(100)
60
+ core.activeContainer.playback.emit(CoreEvents.PLAYBACK_PLAY_INTENT)
61
+ vi.spyOn(performance, 'now').mockReturnValue(150)
62
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
63
+ vi.spyOn(performance, 'now').mockReturnValue(250)
64
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STATE_BUFFERING)
65
+ vi.spyOn(performance, 'now').mockReturnValue(350)
66
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STATE_BUFFERFULL)
67
+ vi.spyOn(performance, 'now').mockReturnValue(450)
68
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STATE_BUFFERING)
69
+ vi.spyOn(performance, 'now').mockReturnValue(550)
70
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STATE_BUFFERFULL)
71
+ })
72
+ it('should measure cumulative buffering durations', () => {
73
+ const metrics = stats.exportMetrics()
74
+ expect(metrics.chrono[Chronograph.Buffering]).toBe(200)
75
+ })
76
+ })
77
+ describe('session', () => {
78
+ beforeEach(() => {
79
+ onReport = vi.fn()
80
+ stats.on(ClapprStatsEvents.REPORT, onReport, null)
81
+ vi.spyOn(performance, 'now').mockReturnValue(100)
82
+ core.activeContainer.playback.emit(CoreEvents.PLAYBACK_PLAY_INTENT)
83
+ vi.spyOn(performance, 'now').mockReturnValue(200)
84
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
85
+ vi.spyOn(performance, 'now').mockReturnValue(60300)
86
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STOP)
87
+ })
88
+ it('should measure', () => {
89
+ expect(onReport).toHaveBeenCalledWith(expect.objectContaining({
90
+ chrono: expect.objectContaining({
91
+ [Chronograph.Session]: 60200,
92
+ }),
93
+ }))
94
+ })
95
+ })
96
+ })
97
+ describe('fps measurements', () => {
98
+ beforeEach(async () => {
99
+ onReport = vi.fn()
100
+ core.activePlayback.name = 'html5_video'
101
+ stats.on(ClapprStatsEvents.REPORT, onReport, null)
102
+ vi.spyOn(performance, 'now').mockReturnValue(100)
103
+ core.activeContainer.playback.emit(CoreEvents.PLAYBACK_PLAY_INTENT)
104
+ vi.spyOn(performance, 'now').mockReturnValue(200)
105
+ vi.spyOn(performance, 'now').mockReturnValue(200)
106
+ core.activeContainer.trigger(CoreEvents.CONTAINER_PLAY)
107
+ core.activeContainer.playback.el.webkitDecodedFrameCount = 126
108
+ core.activeContainer.playback.el.webkitDroppedFrameCount = 3
109
+ vi.spyOn(performance, 'now').mockReturnValue(5225)
110
+ await clock.tickAsync(5000)
111
+ core.activeContainer.playback.el.webkitDecodedFrameCount = 275
112
+ core.activeContainer.playback.el.webkitDroppedFrameCount = 4
113
+ vi.spyOn(performance, 'now').mockReturnValue(10225)
114
+ core.activeContainer.trigger(CoreEvents.CONTAINER_STOP)
115
+ })
116
+ it('should measure fps', () => {
117
+ expect(onReport).toHaveBeenNthCalledWith(1, expect.objectContaining({
118
+ counters: expect.objectContaining({
119
+ [Counter.DecodedFrames]: 126,
120
+ [Counter.DroppedFrames]: 3,
121
+ [Counter.Fps]: expect.closeTo(25, 0),
122
+ }),
123
+ }))
124
+ expect(onReport).toHaveBeenNthCalledWith(2, expect.objectContaining({
125
+ counters: expect.objectContaining({
126
+ [Counter.DecodedFrames]: 275,
127
+ [Counter.DroppedFrames]: 4,
128
+ [Counter.Fps]: expect.closeTo(30, 0),
129
+ }),
130
+ }))
131
+ })
132
+ })
133
+ })
@@ -1,28 +1,74 @@
1
+ /**
2
+ * @beta
3
+ */
4
+ export enum Chronograph {
5
+ Startup = 'startup',
6
+ Watch = 'watch',
7
+ Pause = 'pause',
8
+ Buffering = 'buffering',
9
+ Session = 'session',
10
+ // Latency = 'latency',
11
+ }
12
+
13
+ /**
14
+ * @beta
15
+ */
16
+ export enum Counter {
17
+ Play = 'play',
18
+ Pause = 'pause',
19
+ Error = 'error',
20
+ Buffering = 'buffering',
21
+ DecodedFrames = 'decodedFrames',
22
+ DroppedFrames = 'droppedFrames',
23
+ Fps = 'fps',
24
+ ChangeLevel = 'changeLevel',
25
+ Seek = 'seek',
26
+ Fullscreen = 'fullscreen',
27
+ DvrUsage = 'dvrUsage',
28
+ }
1
29
 
2
30
  /**
3
31
  * @beta
4
32
  */
5
33
  export type Metrics = {
34
+ /**
35
+ * Events count counters
36
+ */
6
37
  counters: {
7
- play: number;
8
- pause: number;
9
- error: number;
10
- buffering: number;
11
- decodedFrames: number;
12
- droppedFrames: number;
13
- fps: number;
14
- changeLevel: number;
15
- seek: number;
16
- fullscreen: number;
17
- dvrUsage: number;
38
+ /**
39
+ *
40
+ */
41
+ [Counter.Play]: number;
42
+ [Counter.Pause]: number;
43
+ [Counter.Error]: number;
44
+ [Counter.Buffering]: number;
45
+ [Counter.DecodedFrames]: number;
46
+ [Counter.DroppedFrames]: number;
47
+ [Counter.Fps]: number;
48
+ [Counter.ChangeLevel]: number;
49
+ [Counter.Seek]: number;
50
+ [Counter.Fullscreen]: number;
51
+ [Counter.DvrUsage]: number;
18
52
  };
19
- timers: {
20
- startup: number;
21
- watch: number;
22
- pause: number;
23
- buffering: number;
24
- session: number;
25
- latency: number;
53
+ /**
54
+ * Time measurements - accumulated duration of time-based activities
55
+ */
56
+ chrono: {
57
+ /**
58
+ * Time spent in the startup phase
59
+ */
60
+ [Chronograph.Startup]: number;
61
+ /**
62
+ * Total time spent in the watch phase
63
+ */
64
+ [Chronograph.Watch]: number;
65
+ /**
66
+ *
67
+ */
68
+ [Chronograph.Pause]: number;
69
+ [Chronograph.Buffering]: number;
70
+ [Chronograph.Session]: number;
71
+ // [Chronograph.Latency]: number;
26
72
  };
27
73
  extra: {
28
74
  playbackName: string;
@@ -51,15 +97,16 @@ export type BitrateTrackRecord = {
51
97
  bitrate: number;
52
98
  }
53
99
 
54
- /**
55
- * @beta
56
- */
57
- export type MetricsUpdateFn = (metrics: Metrics) => void;
58
-
59
100
  /**
60
101
  * @beta
61
102
  */
62
103
  export enum ClapprStatsEvents {
63
- REPORT_EVENT = 'clappr:stats:report',
64
- PERCENTAGE_EVENT = 'clappr:stats:percentage',
104
+ /**
105
+ * Emitted periodically with current measurements.
106
+ */
107
+ REPORT = 'clappr:stats:report',
108
+ /**
109
+ * Emitted when the playback reaches a certain percentage of the total duration.
110
+ */
111
+ // PERCENTAGE = 'clappr:stats:percentage',
65
112
  }
@@ -15,13 +15,12 @@ export function newMetrics(): Metrics {
15
15
  fullscreen: 0,
16
16
  dvrUsage: 0,
17
17
  },
18
- timers: {
18
+ chrono: {
19
19
  startup: 0,
20
20
  watch: 0,
21
21
  pause: 0,
22
22
  buffering: 0,
23
23
  session: 0,
24
- latency: 0,
25
24
  },
26
25
  extra: {
27
26
  playbackName: '',
@@ -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
+ })