@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.
- package/assets/clips/clips.ejs +1 -0
- package/assets/clips/clips.scss +23 -3
- package/assets/level-selector/list.ejs +9 -3
- package/assets/media-control/media-control.ejs +1 -9
- package/assets/media-control/media-control.scss +0 -25
- package/assets/media-control/width370.scss +4 -4
- package/dist/core.js +6 -8
- package/dist/index.css +855 -855
- package/dist/index.js +609 -664
- package/dist/player.d.ts +426 -302
- package/dist/plugins/index.css +551 -551
- package/dist/plugins/index.js +648 -703
- package/docs/api/{player.audioselector.md → player.audiotracks.md} +3 -3
- package/docs/api/player.clapprstats.exportmetrics.md +1 -1
- package/docs/api/player.clapprstats.md +5 -15
- package/docs/api/player.clapprstatssettings.md +13 -0
- package/docs/api/{player.contextmenupluginsettings.preventshowcontextmenu.md → player.clips.destroy.md} +7 -3
- package/docs/api/{player.contextmenupluginsettings.label.md → player.clips.disable.md} +7 -3
- package/docs/api/player.clips.enable.md +18 -0
- package/docs/api/player.clips.md +170 -0
- package/docs/api/player.clips.render.md +18 -0
- package/docs/api/player.clips.supportedversion.md +16 -0
- package/docs/api/player.clips.version.md +14 -0
- package/docs/api/player.clipspluginsettings.md +2 -2
- package/docs/api/player.clipspluginsettings.text.md +1 -1
- package/docs/api/player.contextmenu.md +2 -0
- package/docs/api/player.contextmenupluginsettings.md +2 -40
- package/docs/api/{player.contextmenupluginsettings.url.md → player.contextmenupluginsettings.options.md} +3 -3
- package/docs/api/player.md +101 -37
- package/docs/api/player.mediacontrol.md +9 -15
- package/docs/api/{player.mediacontrol.getelement.md → player.mediacontrol.mount.md} +20 -7
- package/docs/api/player.mediacontrolelement.md +4 -2
- package/docs/api/player.mediacontrollayerelement.md +16 -0
- package/docs/api/player.mediacontrolleftelement.md +16 -0
- package/docs/api/player.mediacontrolrightelement.md +16 -0
- package/docs/api/player.mediacontrolsettings.md +23 -0
- package/docs/api/player.menuoption.md +21 -0
- package/docs/api/{player.clapprnerdstats._constructor_.md → player.nerdstats._constructor_.md} +3 -3
- package/docs/api/{player.clapprnerdstats.md → player.nerdstats.md} +5 -5
- package/docs/api/player.playbackrate.md +1 -1
- package/docs/api/player.playerconfig.md +1 -1
- package/docs/api/player.playerconfig.playbacktype.md +1 -1
- package/docs/api/player.qualitylevel.height.md +1 -1
- package/docs/api/player.qualitylevel.level.md +1 -1
- package/docs/api/player.qualitylevel.md +4 -4
- package/docs/api/player.qualitylevel.width.md +1 -1
- package/docs/api/{player.levelselector.events.md → player.qualitylevels.events.md} +2 -2
- package/docs/api/{player.levelselector.md → player.qualitylevels.md} +6 -6
- package/docs/api/{player.levelselectorpluginsettings.labels.md → player.qualitylevelspluginsettings.labels.md} +2 -2
- package/docs/api/{player.levelselectorpluginsettings.md → player.qualitylevelspluginsettings.md} +6 -6
- package/docs/api/{player.levelselectorpluginsettings.restrictresolution.md → player.qualitylevelspluginsettings.restrictresolution.md} +2 -2
- package/docs/api/player.timeposition.current.md +1 -1
- package/docs/api/player.timeposition.md +2 -2
- package/docs/api/player.timeposition.total.md +1 -1
- package/docs/api/player.timeprogress.md +6 -4
- package/docs/api/player.timevalue.md +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/dash-playback/DashPlayback.d.ts.map +1 -1
- package/lib/playback/dash-playback/DashPlayback.js +5 -7
- package/lib/playback.types.d.ts +22 -9
- package/lib/playback.types.d.ts.map +1 -1
- package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.d.ts +4 -0
- package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.d.ts.map +1 -1
- package/lib/plugins/clappr-nerd-stats/ClapprNerdStats.js +20 -23
- package/lib/plugins/clappr-nerd-stats/NerdStats.d.ts +83 -0
- package/lib/plugins/clappr-nerd-stats/NerdStats.d.ts.map +1 -0
- package/lib/plugins/clappr-nerd-stats/NerdStats.js +339 -0
- package/lib/plugins/clappr-stats/ClapprStats.d.ts +27 -32
- package/lib/plugins/clappr-stats/ClapprStats.d.ts.map +1 -1
- package/lib/plugins/clappr-stats/ClapprStats.js +94 -202
- package/lib/plugins/clappr-stats/types.d.ts +65 -24
- package/lib/plugins/clappr-stats/types.d.ts.map +1 -1
- package/lib/plugins/clappr-stats/types.js +37 -2
- package/lib/plugins/clappr-stats/utils.d.ts.map +1 -1
- package/lib/plugins/clappr-stats/utils.js +1 -2
- package/lib/plugins/clips/Clips.d.ts +21 -16
- package/lib/plugins/clips/Clips.d.ts.map +1 -1
- package/lib/plugins/clips/Clips.js +96 -98
- package/lib/plugins/clips/types.d.ts +19 -0
- package/lib/plugins/clips/types.d.ts.map +1 -0
- package/lib/plugins/clips/types.js +1 -0
- package/lib/plugins/clips/utils.d.ts +4 -0
- package/lib/plugins/clips/utils.d.ts.map +1 -0
- package/lib/plugins/clips/utils.js +36 -0
- package/lib/plugins/media-control/MediaControl.d.ts +4 -7
- package/lib/plugins/media-control/MediaControl.d.ts.map +1 -1
- package/lib/plugins/media-control/MediaControl.js +19 -31
- package/lib/plugins/utils.d.ts +9 -1
- package/lib/plugins/utils.d.ts.map +1 -1
- package/lib/plugins/utils.js +9 -10
- package/lib/plugins/vast-ads/loaderxml.js +2 -2
- package/lib/testUtils.d.ts +2 -1
- package/lib/testUtils.d.ts.map +1 -1
- package/lib/testUtils.js +5 -7
- package/package.json +1 -1
- package/src/index.plugins.ts +2 -1
- package/src/playback/dash-playback/DashPlayback.ts +5 -8
- package/src/playback.types.ts +23 -8
- package/src/plugins/clappr-nerd-stats/{ClapprNerdStats.ts → NerdStats.ts} +25 -30
- package/src/plugins/clappr-stats/ClapprStats.ts +242 -306
- package/src/plugins/clappr-stats/__tests__/ClapprStats.test.ts +133 -0
- package/src/plugins/clappr-stats/types.ts +72 -25
- package/src/plugins/clappr-stats/utils.ts +1 -2
- package/src/plugins/clips/Clips.ts +116 -135
- package/src/plugins/clips/__tests__/Clips.test.ts +72 -0
- package/src/plugins/clips/__tests__/__snapshots__/Clips.test.ts.snap +14 -0
- package/src/plugins/clips/types.ts +22 -0
- package/src/plugins/clips/utils.ts +54 -0
- package/src/plugins/error-screen/__tests__/ErrorScreen.test.ts +3 -4
- package/src/plugins/level-selector/__tests__/__snapshots__/QualityLevels.test.ts.snap +18 -18
- package/src/plugins/media-control/MediaControl.ts +31 -58
- package/src/plugins/media-control/__tests__/__snapshots__/MediaControl.test.ts.snap +7 -35
- package/src/plugins/subtitles/__tests__/ClosedCaptions.test.ts +1 -0
- package/src/plugins/utils.ts +9 -7
- package/src/plugins/vast-ads/loaderxml.ts +2 -2
- package/src/testUtils.ts +5 -7
- package/temp/player.api.json +693 -471
- package/tsconfig.tsbuildinfo +1 -1
- package/docs/api/player.clapprstats.setupdatemetrics.md +0 -56
- package/docs/api/player.clipsplugin.gettext.md +0 -58
- package/docs/api/player.clipsplugin.md +0 -59
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
}
|
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
import { Container, Events, UICorePlugin,
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
17
|
-
start: number
|
|
18
|
-
text: string
|
|
19
|
-
}
|
|
12
|
+
const T = 'plugins.clips'
|
|
20
13
|
|
|
21
14
|
/**
|
|
22
|
-
* Configuration options for the {@link ClipsPlugin
|
|
15
|
+
* Configuration options for the {@link ClipsPlugin} plugin.
|
|
23
16
|
* @beta
|
|
24
17
|
*/
|
|
25
18
|
export interface ClipsPluginSettings {
|
|
26
19
|
/**
|
|
27
|
-
* The text
|
|
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
|
|
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
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
private duration: number = 0
|
|
41
|
+
export class Clips extends UICorePlugin {
|
|
42
|
+
private barStyle: HTMLStyleElement | null = null
|
|
46
43
|
|
|
47
|
-
private
|
|
44
|
+
private clips: ClipDesc[] = []
|
|
48
45
|
|
|
49
|
-
private
|
|
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:
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
Events.
|
|
80
|
-
this.
|
|
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
|
-
|
|
89
|
+
override render() {
|
|
90
|
+
trace(`${T} render`)
|
|
86
91
|
if (!this.options.clips) {
|
|
87
|
-
this
|
|
92
|
+
return this
|
|
93
|
+
}
|
|
94
|
+
this.$el.html(Clips.template())
|
|
95
|
+
this.$el.hide()
|
|
96
|
+
return this
|
|
97
|
+
}
|
|
88
98
|
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
this.
|
|
115
|
+
override enable() {
|
|
116
|
+
this.render()
|
|
117
|
+
return super.enable()
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
private
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
107
|
-
|
|
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.
|
|
140
|
+
this.oldContainer,
|
|
110
141
|
Events.CONTAINER_TIMEUPDATE,
|
|
111
142
|
this.onTimeUpdate,
|
|
112
143
|
)
|
|
113
144
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
this.oldContainer = this.core.activeContainer
|
|
146
|
+
if (this.svgMask) {
|
|
147
|
+
this.svgMask.remove()
|
|
148
|
+
this.svgMask = null
|
|
149
|
+
}
|
|
118
150
|
this.listenTo(
|
|
119
|
-
|
|
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.
|
|
127
|
-
this.duration = event.total
|
|
165
|
+
if (!this.svgMask) {
|
|
128
166
|
this.makeSvg(event.total)
|
|
129
|
-
this.durationGetting = true
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
+
})
|