@gcorevideo/player 2.23.2 → 2.24.0
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/dist/core.js +1 -1
- package/dist/index.css +747 -747
- package/dist/index.js +149 -18
- package/dist/player.d.ts +6 -3
- package/lib/index.plugins.d.ts +1 -0
- package/lib/index.plugins.d.ts.map +1 -1
- package/lib/index.plugins.js +1 -0
- package/lib/plugins/big-mute-button/BigMuteButton.d.ts +1 -1
- package/lib/plugins/big-mute-button/BigMuteButton.d.ts.map +1 -1
- package/lib/plugins/big-mute-button/BigMuteButton.js +3 -2
- package/lib/plugins/bottom-gear/BottomGear.d.ts +1 -1
- package/lib/plugins/bottom-gear/BottomGear.d.ts.map +1 -1
- package/lib/plugins/bottom-gear/BottomGear.js +15 -8
- package/lib/plugins/cmcd-config/CmcdConfig.d.ts +45 -0
- package/lib/plugins/cmcd-config/CmcdConfig.d.ts.map +1 -0
- package/lib/plugins/cmcd-config/CmcdConfig.js +114 -0
- package/lib/plugins/cmcd-config/utils.d.ts +3 -0
- package/lib/plugins/cmcd-config/utils.d.ts.map +1 -0
- package/lib/plugins/cmcd-config/utils.js +12 -0
- package/lib/plugins/thumbnails/Thumbnails.d.ts.map +1 -1
- package/lib/plugins/thumbnails/Thumbnails.js +3 -6
- package/lib/testUtils.d.ts +5 -1
- package/lib/testUtils.d.ts.map +1 -1
- package/lib/testUtils.js +6 -2
- package/package.json +1 -1
- package/src/index.plugins.ts +1 -0
- package/src/plugins/big-mute-button/BigMuteButton.ts +3 -2
- package/src/plugins/bottom-gear/BottomGear.ts +17 -14
- package/src/plugins/bottom-gear/__tests__/BottomGear.test.ts +51 -14
- package/src/plugins/cmcd-config/CmcdConfig.ts +148 -0
- package/src/plugins/cmcd-config/__tests__/CmcdConfig.test.ts +162 -0
- package/src/plugins/cmcd-config/utils.ts +13 -0
- package/src/plugins/media-control/__tests__/MediaControl.test.ts +4 -3
- package/src/plugins/thumbnails/Thumbnails.ts +3 -6
- package/src/plugins/thumbnails/__tests__/Thumbnails.test.ts +20 -3
- package/src/plugins/thumbnails/__tests__/__snapshots__/Thumbnails.test.ts.snap +1 -1
- package/src/testUtils.ts +6 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -174,11 +174,9 @@ export class Thumbnails extends UICorePlugin {
|
|
|
174
174
|
mediaControl.$el.first().after(this.$el);
|
|
175
175
|
}
|
|
176
176
|
onMouseMoveSeekbar(_, pos) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.update();
|
|
181
|
-
}
|
|
177
|
+
this.hoverPosition = pos;
|
|
178
|
+
this.showing = true;
|
|
179
|
+
this.update();
|
|
182
180
|
}
|
|
183
181
|
onMouseLeave() {
|
|
184
182
|
this.showing = false;
|
|
@@ -290,7 +288,6 @@ export class Thumbnails extends UICorePlugin {
|
|
|
290
288
|
// determine which thumbnail applies to the current time
|
|
291
289
|
const thumbIndex = this.getThumbIndexForTime(hoverTime);
|
|
292
290
|
const thumb = this.thumbs[thumbIndex];
|
|
293
|
-
// update thumbnail
|
|
294
291
|
const $spotlight = this.$el.find('#thumbnails-spotlight');
|
|
295
292
|
this.buildThumbImage(thumb, this.spotlightHeight, $spotlight.find('.thumbnail-container')).appendTo($spotlight);
|
|
296
293
|
const elWidth = this.$el.width();
|
package/lib/testUtils.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function createSpinnerPlugin(): Events<string | symbol, any> & {
|
|
|
28
28
|
show: import("vitest").Mock<(...args: any[]) => any>;
|
|
29
29
|
hide: import("vitest").Mock<(...args: any[]) => any>;
|
|
30
30
|
};
|
|
31
|
-
export declare function createMockPlayback(name?: string): Events<string | symbol, any> & {
|
|
31
|
+
export declare function createMockPlayback(name?: string, options?: Record<string, unknown>): Events<string | symbol, any> & {
|
|
32
32
|
name: string;
|
|
33
33
|
currentLevel: number;
|
|
34
34
|
el: HTMLVideoElement;
|
|
@@ -36,6 +36,9 @@ export declare function createMockPlayback(name?: string): Events<string | symbo
|
|
|
36
36
|
dvrInUse: boolean;
|
|
37
37
|
isAudioOnly: boolean;
|
|
38
38
|
levels: never[];
|
|
39
|
+
options: {
|
|
40
|
+
[x: string]: unknown;
|
|
41
|
+
};
|
|
39
42
|
consent: import("vitest").Mock<(...args: any[]) => any>;
|
|
40
43
|
play: import("vitest").Mock<(...args: any[]) => any>;
|
|
41
44
|
pause: import("vitest").Mock<(...args: any[]) => any>;
|
|
@@ -75,6 +78,7 @@ export declare function createMockContainer(options?: Record<string, unknown>, p
|
|
|
75
78
|
getDuration: import("vitest").Mock<(...args: any[]) => any>;
|
|
76
79
|
getPlugin: import("vitest").Mock<(...args: any[]) => any>;
|
|
77
80
|
getPlaybackType: import("vitest").Mock<(...args: any[]) => any>;
|
|
81
|
+
getStartTimeOffset: import("vitest").Mock<(...args: any[]) => any>;
|
|
78
82
|
isDvrInUse: import("vitest").Mock<(...args: any[]) => any>;
|
|
79
83
|
isDvrEnabled: import("vitest").Mock<(...args: any[]) => any>;
|
|
80
84
|
isHighDefinitionInUse: import("vitest").Mock<(...args: any[]) => any>;
|
package/lib/testUtils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testUtils.d.ts","sourceRoot":"","sources":["../src/testUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,MAAM,MAAM,eAAe,CAAA;AAGlC,wBAAgB,cAAc,CAC5B,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,SAAS,GAAE,GAAkC;;;;;;;;;;;;;;;;EAqB9C;AAED,wBAAgB,gBAAgB;;;EAK/B;AAED,wBAAgB,mBAAmB;;;;;;EAKlC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAS
|
|
1
|
+
{"version":3,"file":"testUtils.d.ts","sourceRoot":"","sources":["../src/testUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,MAAM,MAAM,eAAe,CAAA;AAGlC,wBAAgB,cAAc,CAC5B,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,SAAS,GAAE,GAAkC;;;;;;;;;;;;;;;;EAqB9C;AAED,wBAAgB,gBAAgB;;;EAK/B;AAED,wBAAgB,mBAAmB;;;;;;EAKlC;AAED,wBAAgB,kBAAkB,CAAC,IAAI,SAAS,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoCtF;AAED,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,QAAQ,GAAE,GAAgD;;;;;;;;;;;;;;;;;;;;;;;;EA6B3D;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,GAAG,gBAiB/C;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,GAAG,OAe7C"}
|
package/lib/testUtils.js
CHANGED
|
@@ -34,7 +34,7 @@ export function createSpinnerPlugin() {
|
|
|
34
34
|
hide: vi.fn(),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
export function createMockPlayback(name = 'mock') {
|
|
37
|
+
export function createMockPlayback(name = 'mock', options = {}) {
|
|
38
38
|
const emitter = new Events();
|
|
39
39
|
return Object.assign(emitter, {
|
|
40
40
|
name,
|
|
@@ -44,6 +44,7 @@ export function createMockPlayback(name = 'mock') {
|
|
|
44
44
|
dvrInUse: false,
|
|
45
45
|
isAudioOnly: false,
|
|
46
46
|
levels: [],
|
|
47
|
+
options: { ...options },
|
|
47
48
|
consent: vi.fn(),
|
|
48
49
|
play: vi.fn(),
|
|
49
50
|
pause: vi.fn(),
|
|
@@ -70,7 +71,7 @@ export function createMockPlayback(name = 'mock') {
|
|
|
70
71
|
trigger: emitter.emit,
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
|
-
export function createMockContainer(options = {}, playback = createMockPlayback()) {
|
|
74
|
+
export function createMockContainer(options = {}, playback = createMockPlayback('html5_video', options)) {
|
|
74
75
|
const el = playback.el;
|
|
75
76
|
const emitter = new Events();
|
|
76
77
|
return Object.assign(emitter, {
|
|
@@ -87,6 +88,7 @@ export function createMockContainer(options = {}, playback = createMockPlayback(
|
|
|
87
88
|
getDuration: vi.fn().mockReturnValue(0),
|
|
88
89
|
getPlugin: vi.fn(),
|
|
89
90
|
getPlaybackType: vi.fn(),
|
|
91
|
+
getStartTimeOffset: vi.fn().mockReturnValue(0),
|
|
90
92
|
isDvrInUse: vi.fn().mockReturnValue(false),
|
|
91
93
|
isDvrEnabled: vi.fn().mockReturnValue(false),
|
|
92
94
|
isHighDefinitionInUse: vi.fn().mockReturnValue(false),
|
|
@@ -107,6 +109,8 @@ export function createMockMediaControl(core) {
|
|
|
107
109
|
// @ts-ignore
|
|
108
110
|
mediaControl.mount = vi.fn();
|
|
109
111
|
// @ts-ignore
|
|
112
|
+
mediaControl.container = core.activeContainer;
|
|
113
|
+
// @ts-ignore
|
|
110
114
|
mediaControl.toggleElement = vi.fn();
|
|
111
115
|
vi.spyOn(mediaControl, 'trigger');
|
|
112
116
|
core.$el.append(mediaControl.$el);
|
package/package.json
CHANGED
package/src/index.plugins.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { AudioTracks as AudioSelector } from "./plugins/audio-selector/AudioTrac
|
|
|
5
5
|
export * from "./plugins/big-mute-button/BigMuteButton.js";
|
|
6
6
|
export * from "./plugins/bottom-gear/BottomGear.js";
|
|
7
7
|
export * from "./plugins/clappr-stats/ClapprStats.js";
|
|
8
|
+
export * from "./plugins/cmcd-config/CmcdConfig.js";
|
|
8
9
|
export * from "./plugins/clappr-nerd-stats/NerdStats.js";
|
|
9
10
|
export { NerdStats as ClapprNerdStats } from "./plugins/clappr-nerd-stats/NerdStats.js";
|
|
10
11
|
export * from "./plugins/click-to-pause/ClickToPause.js";
|
|
@@ -47,7 +47,7 @@ export class BigMuteButton extends UICorePlugin {
|
|
|
47
47
|
*/
|
|
48
48
|
override get events() {
|
|
49
49
|
return {
|
|
50
|
-
'click .big-mute-icon': '
|
|
50
|
+
'click .big-mute-icon': 'clicked',
|
|
51
51
|
'click .big-mute-icon-wrapper': 'destroyBigMuteBtn',
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -62,6 +62,7 @@ export class BigMuteButton extends UICorePlugin {
|
|
|
62
62
|
trace(`${T} bindEvents`, {
|
|
63
63
|
mediacontrol: !!this.core.mediaControl,
|
|
64
64
|
})
|
|
65
|
+
// TOOD use core.getPlugin('media_control')
|
|
65
66
|
this.listenTo(
|
|
66
67
|
this.core.mediaControl,
|
|
67
68
|
Events.MEDIACONTROL_RENDERED,
|
|
@@ -199,7 +200,7 @@ export class BigMuteButton extends UICorePlugin {
|
|
|
199
200
|
this.destroy()
|
|
200
201
|
}
|
|
201
202
|
|
|
202
|
-
private
|
|
203
|
+
private clicked(e: MouseEvent) {
|
|
203
204
|
const localVolume = Utils.Config.restore('volume')
|
|
204
205
|
const volume = !isNaN(localVolume) ? localVolume : 100
|
|
205
206
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UICorePlugin, template, Events as ClapprEvents,
|
|
1
|
+
import { UICorePlugin, template, Events as ClapprEvents, $, Container } from '@clappr/core'
|
|
2
2
|
import { trace } from '@gcorevideo/utils'
|
|
3
3
|
import assert from 'assert'
|
|
4
4
|
|
|
@@ -150,11 +150,6 @@ export class BottomGear extends UICorePlugin {
|
|
|
150
150
|
*/
|
|
151
151
|
override bindEvents() {
|
|
152
152
|
this.listenToOnce(this.core, ClapprEvents.CORE_READY, this.onCoreReady)
|
|
153
|
-
this.listenTo(
|
|
154
|
-
this.core,
|
|
155
|
-
ClapprEvents.CORE_ACTIVE_CONTAINER_CHANGED,
|
|
156
|
-
this.onActiveContainerChanged,
|
|
157
|
-
)
|
|
158
153
|
}
|
|
159
154
|
|
|
160
155
|
/**
|
|
@@ -208,18 +203,16 @@ export class BottomGear extends UICorePlugin {
|
|
|
208
203
|
return $item
|
|
209
204
|
}
|
|
210
205
|
|
|
211
|
-
private
|
|
212
|
-
trace(`${T} onActiveContainerChanged`)
|
|
213
|
-
this.bindContainerEvents()
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private bindContainerEvents() {
|
|
206
|
+
private bindContainerEvents(container: Container) {
|
|
217
207
|
trace(`${T} bindContainerEvents`)
|
|
218
208
|
this.listenTo(
|
|
219
|
-
|
|
209
|
+
container,
|
|
220
210
|
ClapprEvents.CONTAINER_HIGHDEFINITIONUPDATE,
|
|
221
211
|
this.highDefinitionUpdate,
|
|
222
212
|
)
|
|
213
|
+
this.listenTo(container, ClapprEvents.CONTAINER_CLICK, () => {
|
|
214
|
+
this.collapse()
|
|
215
|
+
})
|
|
223
216
|
}
|
|
224
217
|
|
|
225
218
|
private highDefinitionUpdate(isHd: boolean) {
|
|
@@ -259,6 +252,10 @@ export class BottomGear extends UICorePlugin {
|
|
|
259
252
|
* Should be called by the UI plugin that added a gear item with a submenu when the latter is closed (e.g., when a "back" button is clicked).
|
|
260
253
|
*/
|
|
261
254
|
refresh() {
|
|
255
|
+
this.collapseSubmenus()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private collapseSubmenus() {
|
|
262
259
|
this.$el.find('.gear-sub-menu-wrapper').hide()
|
|
263
260
|
this.$el.find('#gear-options').show()
|
|
264
261
|
}
|
|
@@ -278,10 +275,12 @@ export class BottomGear extends UICorePlugin {
|
|
|
278
275
|
}
|
|
279
276
|
|
|
280
277
|
private collapse() {
|
|
281
|
-
trace(`${T}
|
|
278
|
+
trace(`${T} collapse`)
|
|
282
279
|
this.collapsed = true;
|
|
283
280
|
this.$el.find('#gear-options-wrapper').hide()
|
|
284
281
|
this.$el.find('#gear-button').attr('aria-expanded', 'false')
|
|
282
|
+
// TODO hide submenus
|
|
283
|
+
this.collapseSubmenus()
|
|
285
284
|
}
|
|
286
285
|
|
|
287
286
|
private onCoreReady() {
|
|
@@ -294,6 +293,9 @@ export class BottomGear extends UICorePlugin {
|
|
|
294
293
|
this.onMediaControlRendered,
|
|
295
294
|
)
|
|
296
295
|
this.listenTo(mediaControl, ClapprEvents.MEDIACONTROL_HIDE, this.collapse)
|
|
296
|
+
this.listenTo(mediaControl, ClapprEvents.MEDIACONTROL_CONTAINERCHANGED, () => {
|
|
297
|
+
this.bindContainerEvents(mediaControl.container)
|
|
298
|
+
})
|
|
297
299
|
this.listenTo(
|
|
298
300
|
mediaControl,
|
|
299
301
|
ExtendedEvents.MEDIACONTROL_MENU_COLLAPSE,
|
|
@@ -303,6 +305,7 @@ export class BottomGear extends UICorePlugin {
|
|
|
303
305
|
}
|
|
304
306
|
},
|
|
305
307
|
)
|
|
308
|
+
this.bindContainerEvents(mediaControl.container)
|
|
306
309
|
}
|
|
307
310
|
|
|
308
311
|
private onMediaControlRendered() {
|
|
@@ -2,7 +2,7 @@ import { MockedFunction, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
import { BottomGear, GearEvents } from '../BottomGear'
|
|
4
4
|
import { createMockCore, createMockMediaControl } from '../../../testUtils'
|
|
5
|
-
import { Events } from '@clappr/core'
|
|
5
|
+
import { $, Events } from '@clappr/core'
|
|
6
6
|
import { ExtendedEvents } from '../../media-control/MediaControl'
|
|
7
7
|
|
|
8
8
|
// import { LogTracer, Logger, setTracer } from '@gcorevideo/utils'
|
|
@@ -17,19 +17,18 @@ describe('BottomGear', () => {
|
|
|
17
17
|
let onGearRendered: MockedFunction<() => void>
|
|
18
18
|
beforeEach(() => {
|
|
19
19
|
core = createMockCore()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
mediaControl = createMockMediaControl(core)
|
|
21
|
+
core.getPlugin = vi
|
|
22
|
+
.fn()
|
|
23
|
+
.mockImplementation((name) =>
|
|
24
|
+
name === 'media_control' ? mediaControl : null,
|
|
25
|
+
)
|
|
26
26
|
})
|
|
27
27
|
describe('basically', () => {
|
|
28
28
|
beforeEach(() => {
|
|
29
29
|
bottomGear = new BottomGear(core)
|
|
30
30
|
onGearRendered = vi.fn()
|
|
31
31
|
bottomGear.on(GearEvents.RENDERED, onGearRendered, null)
|
|
32
|
-
bottomGear.render()
|
|
33
32
|
core.emit(Events.CORE_READY)
|
|
34
33
|
bottomGear.addItem('test', null).html('<button>test</button>')
|
|
35
34
|
})
|
|
@@ -92,9 +91,9 @@ describe('BottomGear', () => {
|
|
|
92
91
|
bottomGear.$el.find('#gear-button').click()
|
|
93
92
|
})
|
|
94
93
|
it('should collapse the gear menu', () => {
|
|
95
|
-
expect(
|
|
96
|
-
'
|
|
97
|
-
)
|
|
94
|
+
expect(
|
|
95
|
+
bottomGear.$el.find('#gear-options-wrapper').css('display'),
|
|
96
|
+
).toBe('none')
|
|
98
97
|
expect(bottomGear.$el.find('#gear-button').attr('aria-expanded')).toBe(
|
|
99
98
|
'false',
|
|
100
99
|
)
|
|
@@ -104,9 +103,6 @@ describe('BottomGear', () => {
|
|
|
104
103
|
describe('when there are no items', () => {
|
|
105
104
|
beforeEach(() => {
|
|
106
105
|
bottomGear = new BottomGear(core)
|
|
107
|
-
onGearRendered = vi.fn()
|
|
108
|
-
bottomGear.on(GearEvents.RENDERED, onGearRendered, null)
|
|
109
|
-
bottomGear.render()
|
|
110
106
|
core.emit(Events.CORE_READY)
|
|
111
107
|
})
|
|
112
108
|
it('should render hidden', () => {
|
|
@@ -117,4 +113,45 @@ describe('BottomGear', () => {
|
|
|
117
113
|
expect(bottomGear.$el.css('display')).not.toBe('none')
|
|
118
114
|
})
|
|
119
115
|
})
|
|
116
|
+
describe('when container is clicked', () => {
|
|
117
|
+
beforeEach(async () => {
|
|
118
|
+
bottomGear = new BottomGear(core)
|
|
119
|
+
core.emit(Events.CORE_READY)
|
|
120
|
+
bottomGear
|
|
121
|
+
.addItem('test', $('<ul id="test-options"><li>Item</li></ul>'))
|
|
122
|
+
.html('<button id="test-button">test</button>')
|
|
123
|
+
bottomGear.$el.find('#gear-button').click()
|
|
124
|
+
})
|
|
125
|
+
describe('basically', () => {
|
|
126
|
+
beforeEach(async () => {
|
|
127
|
+
mediaControl.container.trigger(Events.CONTAINER_CLICK)
|
|
128
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
129
|
+
})
|
|
130
|
+
it('should collapse the gear menu', () => {
|
|
131
|
+
expect(bottomGear.$el.find('#gear-options-wrapper').css('display')).toBe(
|
|
132
|
+
'none',
|
|
133
|
+
)
|
|
134
|
+
expect(bottomGear.$el.find('#gear-button').attr('aria-expanded')).toBe(
|
|
135
|
+
'false',
|
|
136
|
+
)
|
|
137
|
+
expect(bottomGear.$el.find('#test-options').css('display')).toBe('none')
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
describe('when submenu is open', () => {
|
|
141
|
+
beforeEach(async () => {
|
|
142
|
+
// bottomGear.$el.find('#test-submenu').click()
|
|
143
|
+
bottomGear.$el.find('#test-options').show(); // as if it was clicked
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
145
|
+
mediaControl.container.trigger(Events.CONTAINER_CLICK)
|
|
146
|
+
})
|
|
147
|
+
it('should collapse it as well', () => {
|
|
148
|
+
expect(bottomGear.$el.find('#test-options').css('display')).toBe(
|
|
149
|
+
'none',
|
|
150
|
+
)
|
|
151
|
+
expect(bottomGear.$el.find('#gear-options').css('display')).not.toBe(
|
|
152
|
+
'none',
|
|
153
|
+
)
|
|
154
|
+
})
|
|
155
|
+
})
|
|
156
|
+
})
|
|
120
157
|
})
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Core,
|
|
3
|
+
CorePlugin,
|
|
4
|
+
Events,
|
|
5
|
+
} from '@clappr/core'
|
|
6
|
+
|
|
7
|
+
import { generateContentId, generateSessionId } from './utils'
|
|
8
|
+
|
|
9
|
+
const CMCD_KEYS = [
|
|
10
|
+
'br',
|
|
11
|
+
'd',
|
|
12
|
+
'ot',
|
|
13
|
+
'tb',
|
|
14
|
+
'bl',
|
|
15
|
+
'dl',
|
|
16
|
+
'mtp',
|
|
17
|
+
'nor',
|
|
18
|
+
'nrr',
|
|
19
|
+
'su',
|
|
20
|
+
'bs',
|
|
21
|
+
'rtp',
|
|
22
|
+
'cid',
|
|
23
|
+
'pr',
|
|
24
|
+
'sf',
|
|
25
|
+
'sid',
|
|
26
|
+
'st',
|
|
27
|
+
'v',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @beta
|
|
32
|
+
*/
|
|
33
|
+
export type CmcdConfigPluginSettings = {
|
|
34
|
+
/**
|
|
35
|
+
* Session ID. If ommitted, a random UUID will be generated
|
|
36
|
+
*/
|
|
37
|
+
sessionId: string
|
|
38
|
+
/**
|
|
39
|
+
* Content ID, either constant or derived from current source.
|
|
40
|
+
* If ommitted, a SHA-1 hash of current source URL will be used
|
|
41
|
+
*/
|
|
42
|
+
contentId?: string | ((sourceUrl: string, mimeType?: string) => (string | Promise<string>))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A `PLUGIN` that configures CMCD for playback
|
|
47
|
+
* @beta
|
|
48
|
+
* @remarks
|
|
49
|
+
* Configuration options
|
|
50
|
+
* `cmcd`: {@link CmcdConfigPluginSettings}
|
|
51
|
+
*/
|
|
52
|
+
export class CmcdConfig extends CorePlugin {
|
|
53
|
+
private sid: string
|
|
54
|
+
|
|
55
|
+
private cid = ''
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @inheritdocs
|
|
59
|
+
*/
|
|
60
|
+
get name() {
|
|
61
|
+
return 'cmcd'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
constructor(core: Core) {
|
|
65
|
+
super(core)
|
|
66
|
+
this.sid = this.options.cmcd?.sessionId ?? generateSessionId()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @inheritdocs
|
|
71
|
+
*/
|
|
72
|
+
override bindEvents() {
|
|
73
|
+
this.listenTo(this.core, Events.CORE_ACTIVE_CONTAINER_CHANGED, () =>
|
|
74
|
+
this.updateSettings(),
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getIds(): Promise<{ sid: string; cid: string }> {
|
|
79
|
+
return {
|
|
80
|
+
sid: this.sid,
|
|
81
|
+
cid: await this.ensureContentId(),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private updateSettings() {
|
|
86
|
+
switch (this.core.activeContainer.playback.name) {
|
|
87
|
+
case 'dash':
|
|
88
|
+
this.updateDashjsSettings()
|
|
89
|
+
break
|
|
90
|
+
case 'hls':
|
|
91
|
+
this.updateHlsjsSettings()
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async updateDashjsSettings() {
|
|
97
|
+
const {cid, sid} = await this.getIds()
|
|
98
|
+
const options = this.core.activePlayback.options
|
|
99
|
+
this.core.activePlayback.options = {
|
|
100
|
+
...options,
|
|
101
|
+
dash: {
|
|
102
|
+
...(options.dash ?? {}),
|
|
103
|
+
cmcd: {
|
|
104
|
+
enabled: true,
|
|
105
|
+
enabledKeys: CMCD_KEYS,
|
|
106
|
+
sid,
|
|
107
|
+
cid,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async updateHlsjsSettings() {
|
|
114
|
+
const { cid, sid } = await this.getIds()
|
|
115
|
+
const options = this.core.activePlayback.options
|
|
116
|
+
this.core.activePlayback.options = {
|
|
117
|
+
...options,
|
|
118
|
+
playback: {
|
|
119
|
+
hlsjsConfig: {
|
|
120
|
+
...(options.playback?.hlsjsConfig ?? {}),
|
|
121
|
+
cmcd: {
|
|
122
|
+
includeKeys: CMCD_KEYS,
|
|
123
|
+
sessionId: sid,
|
|
124
|
+
contentId: cid,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async ensureContentId(): Promise<string> {
|
|
132
|
+
if (!this.cid) {
|
|
133
|
+
this.cid = await this.evalContentId()
|
|
134
|
+
}
|
|
135
|
+
return this.cid
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private async evalContentId(): Promise<string> {
|
|
139
|
+
if (!this.core.activeContainer.options.cmcd?.contentId) {
|
|
140
|
+
return generateContentId(this.core.activePlayback.options.src)
|
|
141
|
+
}
|
|
142
|
+
const contentId = this.core.activeContainer.options.cmcd.contentId
|
|
143
|
+
if (typeof contentId === 'string') {
|
|
144
|
+
return contentId
|
|
145
|
+
}
|
|
146
|
+
return Promise.resolve(contentId(this.core.activePlayback.options.src))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { CmcdConfig } from '../CmcdConfig'
|
|
4
|
+
import { createMockCore } from '../../../testUtils'
|
|
5
|
+
import { Events } from '@clappr/core'
|
|
6
|
+
import { generateContentId } from '../utils'
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'node:crypto'
|
|
9
|
+
|
|
10
|
+
vi.mock('../utils', () => ({
|
|
11
|
+
generateSessionId: vi.fn().mockReturnValue('123'),
|
|
12
|
+
generateContentId: vi.fn().mockResolvedValue('deadbeef'),
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
const CMCD_KEYS = [
|
|
16
|
+
'br',
|
|
17
|
+
'd',
|
|
18
|
+
'ot',
|
|
19
|
+
'tb',
|
|
20
|
+
'bl',
|
|
21
|
+
'dl',
|
|
22
|
+
'mtp',
|
|
23
|
+
'nor',
|
|
24
|
+
'nrr',
|
|
25
|
+
'su',
|
|
26
|
+
'bs',
|
|
27
|
+
'rtp',
|
|
28
|
+
'cid',
|
|
29
|
+
'pr',
|
|
30
|
+
'sf',
|
|
31
|
+
'sid',
|
|
32
|
+
'st',
|
|
33
|
+
'v',
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
describe('CmcdConfig', () => {
|
|
37
|
+
let core: any
|
|
38
|
+
let plugin: CmcdConfig
|
|
39
|
+
describe('basically', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
core = createMockCore({})
|
|
42
|
+
})
|
|
43
|
+
describe('when active container is changed', () => {
|
|
44
|
+
describe('dash.js', () => {
|
|
45
|
+
beforeEach(async () => {
|
|
46
|
+
core.activeContainer.playback.name = 'dash'
|
|
47
|
+
core.activeContainer.playback.options.src = 'https://123.mpd'
|
|
48
|
+
plugin = new CmcdConfig(core)
|
|
49
|
+
core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED)
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
51
|
+
})
|
|
52
|
+
it('should update DASH.js CMCD settings', () => {
|
|
53
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
54
|
+
dash: expect.objectContaining({
|
|
55
|
+
cmcd: expect.objectContaining({
|
|
56
|
+
enabled: true,
|
|
57
|
+
enabledKeys: CMCD_KEYS,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
}))
|
|
61
|
+
})
|
|
62
|
+
it('should generate unique session ID', () => {
|
|
63
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
64
|
+
dash: expect.objectContaining({
|
|
65
|
+
cmcd: expect.objectContaining({
|
|
66
|
+
sid: '123',
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
}))
|
|
70
|
+
})
|
|
71
|
+
it('should compute content ID from source URL', () => {
|
|
72
|
+
expect(generateContentId).toHaveBeenCalledWith('https://123.mpd')
|
|
73
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
74
|
+
dash: expect.objectContaining({
|
|
75
|
+
cmcd: expect.objectContaining({
|
|
76
|
+
cid: 'deadbeef',
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
}))
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
describe('hls.js', () => {
|
|
83
|
+
beforeEach(async () => {
|
|
84
|
+
core.activeContainer.playback.name = 'hls'
|
|
85
|
+
core.activeContainer.playback.options.src = 'https://123.m3u8'
|
|
86
|
+
plugin = new CmcdConfig(core)
|
|
87
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
88
|
+
core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED)
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
90
|
+
})
|
|
91
|
+
it('should update HLS.js CMCD settings', () => {
|
|
92
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
93
|
+
playback: expect.objectContaining({
|
|
94
|
+
hlsjsConfig: expect.objectContaining({
|
|
95
|
+
cmcd: expect.objectContaining({
|
|
96
|
+
includeKeys: CMCD_KEYS,
|
|
97
|
+
contentId: 'deadbeef',
|
|
98
|
+
sessionId: '123',
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
}))
|
|
103
|
+
expect(generateContentId).toHaveBeenCalledWith('https://123.m3u8')
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
describe('custom content ID', () => {
|
|
109
|
+
beforeEach(async () => {
|
|
110
|
+
core = createMockCore({
|
|
111
|
+
cmcd: {
|
|
112
|
+
contentId: (src: string) => new Promise(resolve => {
|
|
113
|
+
const h = createHash('sha256')
|
|
114
|
+
h.on('readable', () => {
|
|
115
|
+
const data = h.read()
|
|
116
|
+
if (data) {
|
|
117
|
+
resolve(data.toString('hex'))
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
h.update(Buffer.from('1$' + src))
|
|
121
|
+
h.end()
|
|
122
|
+
}),
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
core.activePlayback.name = 'dash'
|
|
126
|
+
core.activePlayback.options.src = 'https://123.mpd'
|
|
127
|
+
plugin = new CmcdConfig(core)
|
|
128
|
+
core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED)
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
130
|
+
})
|
|
131
|
+
it('should use custom content ID', () => {
|
|
132
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
133
|
+
dash: expect.objectContaining({
|
|
134
|
+
cmcd: expect.objectContaining({
|
|
135
|
+
cid: 'e287ea99b57c09b7a185aaaf36e075f2c0b346ce90aeced72976b1732678a8c6',
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
}))
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
describe('custom session ID', () => {
|
|
142
|
+
beforeEach(async () => {
|
|
143
|
+
core = createMockCore({
|
|
144
|
+
cmcd: { sessionId: '456' },
|
|
145
|
+
})
|
|
146
|
+
core.activePlayback.name = 'dash'
|
|
147
|
+
core.activePlayback.options.src = 'https://123.mpd'
|
|
148
|
+
plugin = new CmcdConfig(core)
|
|
149
|
+
core.trigger(Events.CORE_ACTIVE_CONTAINER_CHANGED)
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
151
|
+
})
|
|
152
|
+
it('should use custom session ID', () => {
|
|
153
|
+
expect(core.activePlayback.options).toEqual(expect.objectContaining({
|
|
154
|
+
dash: expect.objectContaining({
|
|
155
|
+
cmcd: expect.objectContaining({
|
|
156
|
+
sid: '456',
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}))
|
|
160
|
+
})
|
|
161
|
+
})
|
|
162
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function generateSessionId(): string {
|
|
2
|
+
return window.crypto.randomUUID()
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function generateContentId(sourceUrl: string): Promise<string> {
|
|
6
|
+
return window.crypto.subtle.digest('SHA-1', new TextEncoder().encode(sourceUrl))
|
|
7
|
+
.then(buffer => {
|
|
8
|
+
const hex = Array.from(new Uint8Array(buffer))
|
|
9
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
10
|
+
.join('')
|
|
11
|
+
return hex
|
|
12
|
+
})
|
|
13
|
+
}
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
MediaControlSettings,
|
|
6
6
|
} from '../MediaControl'
|
|
7
7
|
import { createMockCore } from '../../../testUtils'
|
|
8
|
-
import { LogTracer, Logger, setTracer } from '@gcorevideo/utils'
|
|
9
8
|
import { $, Events, Playback } from '@clappr/core'
|
|
10
9
|
|
|
11
10
|
vi.mock('../../utils/fullscreen', () => ({
|
|
@@ -13,8 +12,10 @@ vi.mock('../../utils/fullscreen', () => ({
|
|
|
13
12
|
isFullscreen: vi.fn().mockReturnValue(false),
|
|
14
13
|
}))
|
|
15
14
|
|
|
16
|
-
Logger
|
|
17
|
-
|
|
15
|
+
// import { LogTracer, Logger, setTracer } from '@gcorevideo/utils'
|
|
16
|
+
|
|
17
|
+
// Logger.enable('*')
|
|
18
|
+
// setTracer(new LogTracer('MediaControl.test'))
|
|
18
19
|
|
|
19
20
|
describe('MediaControl', () => {
|
|
20
21
|
let core: any
|