unified-video-framework 1.4.411 → 1.4.412

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 (39) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +22 -0
  3. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  4. package/packages/core/src/interfaces/IVideoPlayer.ts +27 -0
  5. package/packages/web/dist/WebPlayer.d.ts +4 -0
  6. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  7. package/packages/web/dist/WebPlayer.js +67 -0
  8. package/packages/web/dist/WebPlayer.js.map +1 -1
  9. package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts +13 -0
  10. package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts.map +1 -0
  11. package/packages/web/dist/drm/BunnyCDNDRMProvider.js +65 -0
  12. package/packages/web/dist/drm/BunnyCDNDRMProvider.js.map +1 -0
  13. package/packages/web/dist/drm/DRMManager.d.ts +13 -0
  14. package/packages/web/dist/drm/DRMManager.d.ts.map +1 -0
  15. package/packages/web/dist/drm/DRMManager.js +59 -0
  16. package/packages/web/dist/drm/DRMManager.js.map +1 -0
  17. package/packages/web/dist/drm/FairPlayDRMHandler.d.ts +24 -0
  18. package/packages/web/dist/drm/FairPlayDRMHandler.d.ts.map +1 -0
  19. package/packages/web/dist/drm/FairPlayDRMHandler.js +190 -0
  20. package/packages/web/dist/drm/FairPlayDRMHandler.js.map +1 -0
  21. package/packages/web/dist/drm/WidevineDRMHandler.d.ts +21 -0
  22. package/packages/web/dist/drm/WidevineDRMHandler.d.ts.map +1 -0
  23. package/packages/web/dist/drm/WidevineDRMHandler.js +143 -0
  24. package/packages/web/dist/drm/WidevineDRMHandler.js.map +1 -0
  25. package/packages/web/dist/drm/types/DRMTypes.d.ts +52 -0
  26. package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -0
  27. package/packages/web/dist/drm/types/DRMTypes.js +15 -0
  28. package/packages/web/dist/drm/types/DRMTypes.js.map +1 -0
  29. package/packages/web/dist/react/WebPlayerView.d.ts +18 -1
  30. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  31. package/packages/web/dist/react/WebPlayerView.js +5 -0
  32. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  33. package/packages/web/src/WebPlayer.ts +95 -0
  34. package/packages/web/src/drm/BunnyCDNDRMProvider.ts +104 -0
  35. package/packages/web/src/drm/DRMManager.ts +99 -0
  36. package/packages/web/src/drm/FairPlayDRMHandler.ts +322 -0
  37. package/packages/web/src/drm/WidevineDRMHandler.ts +246 -0
  38. package/packages/web/src/drm/types/DRMTypes.ts +97 -0
  39. package/packages/web/src/react/WebPlayerView.tsx +28 -1
@@ -0,0 +1,99 @@
1
+ /**
2
+ * DRM Manager
3
+ * Main orchestrator for web DRM - detects browser and initializes appropriate handler
4
+ */
5
+
6
+ import { IDRMHandler, WebDRMConfig, DRMError } from './types/DRMTypes';
7
+ import { WidevineDRMHandler } from './WidevineDRMHandler';
8
+ import { FairPlayDRMHandler } from './FairPlayDRMHandler';
9
+
10
+ export class DRMManager {
11
+ private handler: IDRMHandler | null = null;
12
+ private config: WebDRMConfig;
13
+
14
+ constructor(config: WebDRMConfig) {
15
+ this.config = config;
16
+ }
17
+
18
+ /**
19
+ * Detect browser and return appropriate DRM handler
20
+ */
21
+ private detectDRMHandler(): IDRMHandler {
22
+ const userAgent = navigator.userAgent.toLowerCase();
23
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
24
+
25
+ if (isSafari) {
26
+ console.log('[DRM] Detected Safari - using FairPlay DRM');
27
+ return new FairPlayDRMHandler(this.config);
28
+ } else {
29
+ console.log('[DRM] Detected non-Safari browser - using Widevine DRM');
30
+ return new WidevineDRMHandler(this.config);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Initialize DRM system
36
+ */
37
+ async initialize(): Promise<void> {
38
+ console.log('[DRM] Initializing DRM Manager...');
39
+
40
+ try {
41
+ // Detect and create appropriate handler
42
+ this.handler = this.detectDRMHandler();
43
+
44
+ // Check if DRM is supported
45
+ if (!this.handler.isSupported()) {
46
+ throw new DRMError(
47
+ 'DRM is not supported in this browser',
48
+ 'DRM_NOT_SUPPORTED'
49
+ );
50
+ }
51
+
52
+ // Initialize the handler
53
+ await this.handler.initialize();
54
+ console.log('[DRM] DRM Manager initialized successfully');
55
+
56
+ } catch (error) {
57
+ console.error('[DRM] Failed to initialize DRM Manager:', error);
58
+ throw error;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Setup DRM for video element and manifest
64
+ */
65
+ async setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void> {
66
+ if (!this.handler) {
67
+ throw new DRMError('DRM Manager not initialized', 'DRM_NOT_INITIALIZED');
68
+ }
69
+
70
+ console.log('[DRM] Setting up DRM for video element');
71
+ await this.handler.setupDRM(videoElement, manifestUrl);
72
+ }
73
+
74
+ /**
75
+ * Check if DRM is currently active
76
+ */
77
+ isActive(): boolean {
78
+ return this.handler !== null;
79
+ }
80
+
81
+ /**
82
+ * Get current DRM handler type
83
+ */
84
+ getHandlerType(): 'widevine' | 'fairplay' | null {
85
+ if (!this.handler) return null;
86
+ return this.handler instanceof FairPlayDRMHandler ? 'fairplay' : 'widevine';
87
+ }
88
+
89
+ /**
90
+ * Cleanup DRM resources
91
+ */
92
+ destroy(): void {
93
+ if (this.handler) {
94
+ console.log('[DRM] Destroying DRM Manager');
95
+ this.handler.destroy();
96
+ this.handler = null;
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,322 @@
1
+ /**
2
+ * FairPlay DRM Handler
3
+ * Implements DRM for Safari using FairPlay Streaming
4
+ */
5
+
6
+ import { IDRMHandler, WebDRMConfig, DRMError } from './types/DRMTypes';
7
+ import { BunnyCDNDRMProvider } from './BunnyCDNDRMProvider';
8
+
9
+ export class FairPlayDRMHandler implements IDRMHandler {
10
+ private config: WebDRMConfig;
11
+ private bunnyProvider: BunnyCDNDRMProvider | null = null;
12
+ private videoElement: HTMLVideoElement | null = null;
13
+ private certificateData: ArrayBuffer | null = null;
14
+ private pendingSessions: Set<any> = new Set();
15
+
16
+ constructor(config: WebDRMConfig) {
17
+ this.config = config;
18
+
19
+ if (config.provider === 'bunny' && config.bunny) {
20
+ this.bunnyProvider = new BunnyCDNDRMProvider(config.bunny);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Check if FairPlay is supported (Safari only)
26
+ */
27
+ isSupported(): boolean {
28
+ // FairPlay is only supported in Safari
29
+ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
30
+ return !!(
31
+ isSafari &&
32
+ (window as any).WebKitMediaKeys &&
33
+ navigator.requestMediaKeySystemAccess
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Initialize FairPlay DRM system
39
+ */
40
+ async initialize(): Promise<void> {
41
+ if (!this.isSupported()) {
42
+ throw new DRMError(
43
+ 'FairPlay DRM is not supported in this browser',
44
+ 'FAIRPLAY_NOT_SUPPORTED'
45
+ );
46
+ }
47
+
48
+ console.log('[FairPlay] Initializing DRM system...');
49
+
50
+ try {
51
+ // Fetch FairPlay certificate
52
+ this.certificateData = await this.fetchCertificate();
53
+ console.log('[FairPlay] Certificate loaded:', this.certificateData.byteLength, 'bytes');
54
+
55
+ } catch (error) {
56
+ console.error('[FairPlay] Failed to initialize:', error);
57
+ throw new DRMError(
58
+ 'Failed to initialize FairPlay DRM',
59
+ 'FAIRPLAY_INIT_ERROR',
60
+ error
61
+ );
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Fetch FairPlay certificate from server
67
+ */
68
+ private async fetchCertificate(): Promise<ArrayBuffer> {
69
+ const certificateUrl = this.getCertificateUrl();
70
+ const headers = this.getCertificateHeaders();
71
+
72
+ console.log('[FairPlay] Fetching certificate from:', certificateUrl);
73
+
74
+ try {
75
+ const response = await fetch(certificateUrl, { headers });
76
+
77
+ if (!response.ok) {
78
+ throw new Error(`Certificate fetch failed: ${response.status} ${response.statusText}`);
79
+ }
80
+
81
+ const certificate = await response.arrayBuffer();
82
+ return certificate;
83
+
84
+ } catch (error) {
85
+ console.error('[FairPlay] Certificate fetch failed:', error);
86
+ throw new DRMError(
87
+ 'Failed to fetch FairPlay certificate',
88
+ 'CERTIFICATE_FETCH_ERROR',
89
+ error
90
+ );
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get certificate URL based on provider
96
+ */
97
+ private getCertificateUrl(): string {
98
+ if (this.config.provider === 'bunny' && this.bunnyProvider) {
99
+ return this.bunnyProvider.getFairPlayCertificateUrl();
100
+ } else if (this.config.provider === 'generic' && this.config.generic) {
101
+ if (!this.config.generic.certificateUrl) {
102
+ throw new DRMError('FairPlay certificate URL is required', 'NO_CERTIFICATE_URL');
103
+ }
104
+ return this.config.generic.certificateUrl;
105
+ }
106
+ throw new DRMError('No certificate URL configured', 'NO_CERTIFICATE_URL');
107
+ }
108
+
109
+ /**
110
+ * Get headers for certificate request
111
+ */
112
+ private getCertificateHeaders(): Record<string, string> {
113
+ if (this.config.provider === 'generic' && this.config.generic?.certificateHeaders) {
114
+ return this.config.generic.certificateHeaders;
115
+ }
116
+ return {};
117
+ }
118
+
119
+ /**
120
+ * Setup DRM for video element
121
+ */
122
+ async setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void> {
123
+ if (!this.certificateData) {
124
+ throw new DRMError('Certificate not loaded', 'CERTIFICATE_NOT_LOADED');
125
+ }
126
+
127
+ this.videoElement = videoElement;
128
+
129
+ // Listen for 'webkitneedkey' event (Safari specific)
130
+ videoElement.addEventListener('webkitneedkey', this.onWebkitNeedKey.bind(this) as any);
131
+
132
+ console.log('[FairPlay] DRM setup complete, waiting for key requests');
133
+ }
134
+
135
+ /**
136
+ * Handle 'webkitneedkey' event (Safari specific)
137
+ */
138
+ private async onWebkitNeedKey(event: any): Promise<void> {
139
+ console.log('[FairPlay] Key request event received');
140
+
141
+ try {
142
+ if (!this.certificateData || !this.videoElement) {
143
+ throw new DRMError('DRM not properly initialized', 'DRM_NOT_INITIALIZED');
144
+ }
145
+
146
+ // Extract content ID from init data
147
+ const contentId = this.extractContentId(event.initData);
148
+ const keySession = event.target.webkitKeys.createSession('video/mp4', event.initData);
149
+
150
+ if (!keySession) {
151
+ throw new DRMError('Failed to create key session', 'SESSION_CREATE_ERROR');
152
+ }
153
+
154
+ this.pendingSessions.add(keySession);
155
+
156
+ // Handle key request message
157
+ keySession.addEventListener('webkitkeymessage', async (messageEvent: any) => {
158
+ await this.onKeyMessage(keySession, messageEvent, contentId);
159
+ });
160
+
161
+ // Handle key added
162
+ keySession.addEventListener('webkitkeyadded', () => {
163
+ console.log('[FairPlay] Key added successfully');
164
+ this.pendingSessions.delete(keySession);
165
+ });
166
+
167
+ // Handle key error
168
+ keySession.addEventListener('webkitkeyerror', () => {
169
+ console.error('[FairPlay] Key error:', keySession.error);
170
+ this.pendingSessions.delete(keySession);
171
+ });
172
+
173
+ } catch (error) {
174
+ console.error('[FairPlay] Failed to handle key request:', error);
175
+ throw new DRMError(
176
+ 'Failed to process FairPlay key request',
177
+ 'KEY_REQUEST_ERROR',
178
+ error
179
+ );
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Extract content ID from init data
185
+ */
186
+ private extractContentId(initData: Uint8Array | ArrayBuffer): string {
187
+ // Convert init data to string to extract content ID
188
+ const initDataString = String.fromCharCode.apply(null, Array.from(new Uint8Array(initData)));
189
+
190
+ // Extract content ID from URI (format: skd://contentId)
191
+ const contentIdMatch = initDataString.match(/skd:\/\/([^"'\s]+)/);
192
+ if (contentIdMatch && contentIdMatch[1]) {
193
+ return contentIdMatch[1];
194
+ }
195
+
196
+ // Fallback: use entire init data as content ID
197
+ return initDataString;
198
+ }
199
+
200
+ /**
201
+ * Handle key message (license request)
202
+ */
203
+ private async onKeyMessage(
204
+ keySession: any,
205
+ event: any,
206
+ contentId: string
207
+ ): Promise<void> {
208
+ console.log('[FairPlay] Key message received, requesting license');
209
+
210
+ try {
211
+ // Prepare SPC (Server Playback Context) data
212
+ const spcData = event.message;
213
+
214
+ // Request CKC (Content Key Context) from license server
215
+ const ckcData = await this.requestLicense(spcData, contentId);
216
+
217
+ // Update key session with CKC
218
+ keySession.update(new Uint8Array(ckcData));
219
+ console.log('[FairPlay] License applied successfully');
220
+
221
+ } catch (error) {
222
+ console.error('[FairPlay] Failed to process license:', error);
223
+ throw new DRMError(
224
+ 'Failed to process FairPlay license',
225
+ 'LICENSE_PROCESS_ERROR',
226
+ error
227
+ );
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Request license from license server
233
+ */
234
+ private async requestLicense(spcData: Uint8Array, contentId: string): Promise<ArrayBuffer> {
235
+ const licenseUrl = this.getLicenseUrl(contentId);
236
+ const headers = await this.getLicenseHeaders();
237
+
238
+ console.log('[FairPlay] Requesting license from:', licenseUrl);
239
+
240
+ try {
241
+ const response = await fetch(licenseUrl, {
242
+ method: 'POST',
243
+ headers: {
244
+ ...headers,
245
+ 'Content-Type': 'application/octet-stream'
246
+ },
247
+ body: spcData
248
+ });
249
+
250
+ if (!response.ok) {
251
+ throw new Error(`License request failed: ${response.status} ${response.statusText}`);
252
+ }
253
+
254
+ const ckcData = await response.arrayBuffer();
255
+ console.log('[FairPlay] License received:', ckcData.byteLength, 'bytes');
256
+
257
+ return ckcData;
258
+
259
+ } catch (error) {
260
+ console.error('[FairPlay] License request failed:', error);
261
+ throw new DRMError(
262
+ 'Failed to retrieve FairPlay license',
263
+ 'LICENSE_FETCH_ERROR',
264
+ error
265
+ );
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Get license URL based on provider
271
+ */
272
+ private getLicenseUrl(contentId: string): string {
273
+ if (this.config.provider === 'bunny' && this.bunnyProvider) {
274
+ return this.bunnyProvider.getFairPlayLicenseUrl();
275
+ } else if (this.config.provider === 'generic' && this.config.generic) {
276
+ return this.config.generic.licenseUrl;
277
+ }
278
+ throw new DRMError('No license URL configured', 'NO_LICENSE_URL');
279
+ }
280
+
281
+ /**
282
+ * Get headers for license request
283
+ */
284
+ private async getLicenseHeaders(): Promise<Record<string, string>> {
285
+ if (this.config.provider === 'bunny' && this.bunnyProvider) {
286
+ return await this.bunnyProvider.getLicenseHeaders();
287
+ } else if (this.config.provider === 'generic' && this.config.generic) {
288
+ return this.config.generic.headers || {};
289
+ }
290
+ return {};
291
+ }
292
+
293
+ /**
294
+ * Handle license request (called by DRM manager)
295
+ */
296
+ async onLicenseRequest(licenseRequest: ArrayBuffer): Promise<ArrayBuffer> {
297
+ return await this.requestLicense(new Uint8Array(licenseRequest), '');
298
+ }
299
+
300
+ /**
301
+ * Cleanup resources
302
+ */
303
+ destroy(): void {
304
+ if (this.videoElement) {
305
+ this.videoElement.removeEventListener('webkitneedkey', this.onWebkitNeedKey.bind(this) as any);
306
+ this.videoElement = null;
307
+ }
308
+
309
+ // Close all pending sessions
310
+ this.pendingSessions.forEach(session => {
311
+ try {
312
+ session.close();
313
+ } catch (e) {
314
+ // Ignore errors during cleanup
315
+ }
316
+ });
317
+ this.pendingSessions.clear();
318
+
319
+ this.certificateData = null;
320
+ this.bunnyProvider = null;
321
+ }
322
+ }
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Widevine DRM Handler
3
+ * Implements DRM for Chrome, Edge, Firefox using Encrypted Media Extensions (EME)
4
+ */
5
+
6
+ import { IDRMHandler, WebDRMConfig, DRM_KEY_SYSTEMS, DRMError } from './types/DRMTypes';
7
+ import { BunnyCDNDRMProvider } from './BunnyCDNDRMProvider';
8
+
9
+ export class WidevineDRMHandler implements IDRMHandler {
10
+ private mediaKeys: MediaKeys | null = null;
11
+ private config: WebDRMConfig;
12
+ private bunnyProvider: BunnyCDNDRMProvider | null = null;
13
+ private videoElement: HTMLVideoElement | null = null;
14
+ private pendingLicenseRequests: Map<string, (license: ArrayBuffer) => void> = new Map();
15
+
16
+ constructor(config: WebDRMConfig) {
17
+ this.config = config;
18
+
19
+ if (config.provider === 'bunny' && config.bunny) {
20
+ this.bunnyProvider = new BunnyCDNDRMProvider(config.bunny);
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Check if Widevine is supported in current browser
26
+ */
27
+ isSupported(): boolean {
28
+ return !!(
29
+ typeof navigator.requestMediaKeySystemAccess === 'function' &&
30
+ typeof window.MediaKeySystemAccess !== 'undefined'
31
+ );
32
+ }
33
+
34
+ /**
35
+ * Initialize Widevine DRM system
36
+ */
37
+ async initialize(): Promise<void> {
38
+ if (!this.isSupported()) {
39
+ throw new DRMError(
40
+ 'Widevine DRM is not supported in this browser',
41
+ 'WIDEVINE_NOT_SUPPORTED'
42
+ );
43
+ }
44
+
45
+ console.log('[Widevine] Initializing DRM system...');
46
+
47
+ try {
48
+ // Request access to Widevine key system
49
+ const keySystemAccess = await navigator.requestMediaKeySystemAccess(
50
+ DRM_KEY_SYSTEMS.WIDEVINE,
51
+ this.getKeySystemConfiguration()
52
+ );
53
+
54
+ // Create MediaKeys instance
55
+ this.mediaKeys = await keySystemAccess.createMediaKeys();
56
+
57
+ console.log('[Widevine] MediaKeys created successfully');
58
+ } catch (error) {
59
+ console.error('[Widevine] Failed to initialize:', error);
60
+ throw new DRMError(
61
+ 'Failed to initialize Widevine DRM',
62
+ 'WIDEVINE_INIT_ERROR',
63
+ error
64
+ );
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get key system configuration for Widevine
70
+ */
71
+ private getKeySystemConfiguration(): MediaKeySystemConfiguration[] {
72
+ return [{
73
+ initDataTypes: ['cenc', 'webm'],
74
+ audioCapabilities: [
75
+ { contentType: 'audio/mp4; codecs="mp4a.40.2"' },
76
+ { contentType: 'audio/webm; codecs="opus"' }
77
+ ],
78
+ videoCapabilities: [
79
+ { contentType: 'video/mp4; codecs="avc1.42E01E"' },
80
+ { contentType: 'video/mp4; codecs="avc1.4D401E"' },
81
+ { contentType: 'video/mp4; codecs="avc1.640028"' },
82
+ { contentType: 'video/webm; codecs="vp9"' }
83
+ ],
84
+ distinctiveIdentifier: 'optional',
85
+ persistentState: 'optional',
86
+ sessionTypes: ['temporary']
87
+ }];
88
+ }
89
+
90
+ /**
91
+ * Setup DRM for video element
92
+ */
93
+ async setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void> {
94
+ if (!this.mediaKeys) {
95
+ throw new DRMError('MediaKeys not initialized', 'MEDIAKEYS_NOT_INITIALIZED');
96
+ }
97
+
98
+ this.videoElement = videoElement;
99
+
100
+ // Attach MediaKeys to video element
101
+ await videoElement.setMediaKeys(this.mediaKeys);
102
+ console.log('[Widevine] MediaKeys attached to video element');
103
+
104
+ // Listen for encrypted events
105
+ videoElement.addEventListener('encrypted', this.onEncrypted.bind(this));
106
+ }
107
+
108
+ /**
109
+ * Handle 'encrypted' event from video element
110
+ */
111
+ private async onEncrypted(event: MediaEncryptedEvent): Promise<void> {
112
+ console.log('[Widevine] Encrypted event received:', event.initDataType);
113
+
114
+ try {
115
+ if (!this.mediaKeys) {
116
+ throw new DRMError('MediaKeys not available', 'MEDIAKEYS_NOT_AVAILABLE');
117
+ }
118
+
119
+ // Create media key session
120
+ const session = this.mediaKeys.createSession();
121
+
122
+ // Listen for message event (license request)
123
+ session.addEventListener('message', async (messageEvent: MediaKeyMessageEvent) => {
124
+ await this.onMessage(session, messageEvent);
125
+ });
126
+
127
+ // Generate license request
128
+ await session.generateRequest(event.initDataType!, event.initData!);
129
+ console.log('[Widevine] License request generated');
130
+
131
+ } catch (error) {
132
+ console.error('[Widevine] Failed to handle encrypted event:', error);
133
+ throw new DRMError(
134
+ 'Failed to process encrypted media',
135
+ 'ENCRYPTED_EVENT_ERROR',
136
+ error
137
+ );
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Handle license request message
143
+ */
144
+ private async onMessage(
145
+ session: MediaKeySession,
146
+ event: MediaKeyMessageEvent
147
+ ): Promise<void> {
148
+ console.log('[Widevine] License request message:', event.messageType);
149
+
150
+ try {
151
+ // Get license from server
152
+ const license = await this.requestLicense(event.message);
153
+
154
+ // Update session with license
155
+ await session.update(license);
156
+ console.log('[Widevine] License applied successfully');
157
+
158
+ } catch (error) {
159
+ console.error('[Widevine] Failed to process license:', error);
160
+ throw new DRMError(
161
+ 'Failed to process DRM license',
162
+ 'LICENSE_REQUEST_ERROR',
163
+ error
164
+ );
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Request license from license server
170
+ */
171
+ private async requestLicense(challenge: ArrayBuffer): Promise<ArrayBuffer> {
172
+ const licenseUrl = this.getLicenseUrl();
173
+ const headers = await this.getLicenseHeaders();
174
+
175
+ console.log('[Widevine] Requesting license from:', licenseUrl);
176
+
177
+ try {
178
+ const response = await fetch(licenseUrl, {
179
+ method: 'POST',
180
+ headers,
181
+ body: challenge
182
+ });
183
+
184
+ if (!response.ok) {
185
+ throw new Error(`License request failed: ${response.status} ${response.statusText}`);
186
+ }
187
+
188
+ const license = await response.arrayBuffer();
189
+ console.log('[Widevine] License received:', license.byteLength, 'bytes');
190
+
191
+ return license;
192
+
193
+ } catch (error) {
194
+ console.error('[Widevine] License request failed:', error);
195
+ throw new DRMError(
196
+ 'Failed to retrieve DRM license',
197
+ 'LICENSE_FETCH_ERROR',
198
+ error
199
+ );
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Get license URL based on provider
205
+ */
206
+ private getLicenseUrl(): string {
207
+ if (this.config.provider === 'bunny' && this.bunnyProvider) {
208
+ return this.bunnyProvider.getWidevineLicenseUrl();
209
+ } else if (this.config.provider === 'generic' && this.config.generic) {
210
+ return this.config.generic.licenseUrl;
211
+ }
212
+ throw new DRMError('No license URL configured', 'NO_LICENSE_URL');
213
+ }
214
+
215
+ /**
216
+ * Get headers for license request
217
+ */
218
+ private async getLicenseHeaders(): Promise<Record<string, string>> {
219
+ if (this.config.provider === 'bunny' && this.bunnyProvider) {
220
+ return await this.bunnyProvider.getLicenseHeaders();
221
+ } else if (this.config.provider === 'generic' && this.config.generic) {
222
+ return this.config.generic.headers || { 'Content-Type': 'application/octet-stream' };
223
+ }
224
+ return { 'Content-Type': 'application/octet-stream' };
225
+ }
226
+
227
+ /**
228
+ * Handle license request (called by DRM manager)
229
+ */
230
+ async onLicenseRequest(licenseRequest: ArrayBuffer): Promise<ArrayBuffer> {
231
+ return await this.requestLicense(licenseRequest);
232
+ }
233
+
234
+ /**
235
+ * Cleanup resources
236
+ */
237
+ destroy(): void {
238
+ if (this.videoElement) {
239
+ this.videoElement.removeEventListener('encrypted', this.onEncrypted.bind(this));
240
+ this.videoElement = null;
241
+ }
242
+ this.mediaKeys = null;
243
+ this.bunnyProvider = null;
244
+ this.pendingLicenseRequests.clear();
245
+ }
246
+ }