@gcorevideo/player 2.23.3 → 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.
@@ -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
package/src/testUtils.ts CHANGED
@@ -41,7 +41,7 @@ export function createSpinnerPlugin() {
41
41
  })
42
42
  }
43
43
 
44
- export function createMockPlayback(name = 'mock') {
44
+ export function createMockPlayback(name = 'mock', options: Record<string, unknown> = {}) {
45
45
  const emitter = new Events()
46
46
  return Object.assign(emitter, {
47
47
  name,
@@ -51,6 +51,7 @@ export function createMockPlayback(name = 'mock') {
51
51
  dvrInUse: false,
52
52
  isAudioOnly: false,
53
53
  levels: [],
54
+ options: { ...options },
54
55
  consent: vi.fn(),
55
56
  play: vi.fn(),
56
57
  pause: vi.fn(),
@@ -80,7 +81,7 @@ export function createMockPlayback(name = 'mock') {
80
81
 
81
82
  export function createMockContainer(
82
83
  options: Record<string, unknown> = {},
83
- playback: any = createMockPlayback(),
84
+ playback: any = createMockPlayback('html5_video', options),
84
85
  ) {
85
86
  const el = playback.el
86
87
  const emitter = new Events()