@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.
Files changed (38) hide show
  1. package/dist/core.js +1 -1
  2. package/dist/index.css +747 -747
  3. package/dist/index.js +149 -18
  4. package/dist/player.d.ts +6 -3
  5. package/lib/index.plugins.d.ts +1 -0
  6. package/lib/index.plugins.d.ts.map +1 -1
  7. package/lib/index.plugins.js +1 -0
  8. package/lib/plugins/big-mute-button/BigMuteButton.d.ts +1 -1
  9. package/lib/plugins/big-mute-button/BigMuteButton.d.ts.map +1 -1
  10. package/lib/plugins/big-mute-button/BigMuteButton.js +3 -2
  11. package/lib/plugins/bottom-gear/BottomGear.d.ts +1 -1
  12. package/lib/plugins/bottom-gear/BottomGear.d.ts.map +1 -1
  13. package/lib/plugins/bottom-gear/BottomGear.js +15 -8
  14. package/lib/plugins/cmcd-config/CmcdConfig.d.ts +45 -0
  15. package/lib/plugins/cmcd-config/CmcdConfig.d.ts.map +1 -0
  16. package/lib/plugins/cmcd-config/CmcdConfig.js +114 -0
  17. package/lib/plugins/cmcd-config/utils.d.ts +3 -0
  18. package/lib/plugins/cmcd-config/utils.d.ts.map +1 -0
  19. package/lib/plugins/cmcd-config/utils.js +12 -0
  20. package/lib/plugins/thumbnails/Thumbnails.d.ts.map +1 -1
  21. package/lib/plugins/thumbnails/Thumbnails.js +3 -6
  22. package/lib/testUtils.d.ts +5 -1
  23. package/lib/testUtils.d.ts.map +1 -1
  24. package/lib/testUtils.js +6 -2
  25. package/package.json +1 -1
  26. package/src/index.plugins.ts +1 -0
  27. package/src/plugins/big-mute-button/BigMuteButton.ts +3 -2
  28. package/src/plugins/bottom-gear/BottomGear.ts +17 -14
  29. package/src/plugins/bottom-gear/__tests__/BottomGear.test.ts +51 -14
  30. package/src/plugins/cmcd-config/CmcdConfig.ts +148 -0
  31. package/src/plugins/cmcd-config/__tests__/CmcdConfig.test.ts +162 -0
  32. package/src/plugins/cmcd-config/utils.ts +13 -0
  33. package/src/plugins/media-control/__tests__/MediaControl.test.ts +4 -3
  34. package/src/plugins/thumbnails/Thumbnails.ts +3 -6
  35. package/src/plugins/thumbnails/__tests__/Thumbnails.test.ts +20 -3
  36. package/src/plugins/thumbnails/__tests__/__snapshots__/Thumbnails.test.ts.snap +1 -1
  37. package/src/testUtils.ts +6 -2
  38. 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
- if (Math.abs(pos - this.hoverPosition) >= 0.01) {
178
- this.hoverPosition = pos;
179
- this.showing = true;
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();
@@ -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>;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmC/C;AAED,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACrC,QAAQ,GAAE,GAA0B;;;;;;;;;;;;;;;;;;;;;;;EA4BrC;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,GAAG,gBAe/C;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,GAAG,OAe7C"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gcorevideo/player",
3
- "version": "2.23.2",
3
+ "version": "2.24.0",
4
4
  "description": "Gcore JavaScript video player",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -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': 'handleBigMuteBtnClick',
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 handleBigMuteBtnClick(e: MouseEvent) {
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, $ } from '@clappr/core'
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 onActiveContainerChanged() {
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
- this.core.activeContainer,
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} hide`)
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
- mediaControl = createMockMediaControl(core)
21
- core.getPlugin = vi
22
- .fn()
23
- .mockImplementation((name) =>
24
- name === 'media_control' ? mediaControl : null,
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(bottomGear.$el.find('#gear-options-wrapper').css('display')).toBe(
96
- 'none',
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.enable('*')
17
- setTracer(new LogTracer('MediaControl.test'))
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