unified-video-framework 1.4.410 → 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 +68 -4
  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 +28 -0
  32. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  33. package/packages/web/src/WebPlayer.ts +96 -4
  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 +55 -3
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Bunny CDN DRM Provider
3
+ * Handles Bunny CDN specific URL construction and token injection
4
+ */
5
+
6
+ import { BunnyCDNDRMConfig, DRMToken, DRMError } from './types/DRMTypes';
7
+
8
+ export class BunnyCDNDRMProvider {
9
+ constructor(private config: BunnyCDNDRMConfig) {}
10
+
11
+ /**
12
+ * Get FairPlay certificate URL for Bunny CDN
13
+ */
14
+ getFairPlayCertificateUrl(): string {
15
+ if (this.config.certificateUrl) {
16
+ return this.config.certificateUrl;
17
+ }
18
+ return `https://video.bunnycdn.com/FairPlay/${this.config.libraryId}/certificate`;
19
+ }
20
+
21
+ /**
22
+ * Get FairPlay license URL for Bunny CDN
23
+ */
24
+ getFairPlayLicenseUrl(): string {
25
+ if (this.config.licenseUrl) {
26
+ return this.config.licenseUrl;
27
+ }
28
+ return `https://video.bunnycdn.com/FairPlay/${this.config.libraryId}/license/?videoId=${this.config.videoId}`;
29
+ }
30
+
31
+ /**
32
+ * Get Widevine license URL for Bunny CDN
33
+ */
34
+ getWidevineLicenseUrl(): string {
35
+ if (this.config.licenseUrl) {
36
+ return this.config.licenseUrl;
37
+ }
38
+ // Bunny CDN Widevine license endpoint
39
+ return `https://video.bunnycdn.com/Widevine/${this.config.libraryId}/license/?videoId=${this.config.videoId}`;
40
+ }
41
+
42
+ /**
43
+ * Get HLS manifest URL with token
44
+ */
45
+ async getManifestUrl(): Promise<string> {
46
+ const baseUrl = `https://${this.config.pullZoneUrl}.b-cdn.net/${this.config.videoId}/playlist.m3u8`;
47
+ const token = await this.config.tokenProvider.getManifestToken();
48
+
49
+ if (!token) {
50
+ throw new DRMError('No manifest token available', 'MANIFEST_TOKEN_ERROR');
51
+ }
52
+
53
+ return this.injectToken(baseUrl, token);
54
+ }
55
+
56
+ /**
57
+ * Inject token into URL as query parameters
58
+ */
59
+ injectToken(url: string, token: DRMToken): string {
60
+ const urlObj = new URL(url);
61
+ urlObj.searchParams.set('token', token.token);
62
+ urlObj.searchParams.set('expires', token.expires.toString());
63
+ if (token.tokenVer) {
64
+ urlObj.searchParams.set('token_ver', token.tokenVer);
65
+ }
66
+ return urlObj.toString();
67
+ }
68
+
69
+ /**
70
+ * Get headers for license requests including token
71
+ */
72
+ async getLicenseHeaders(): Promise<Record<string, string>> {
73
+ const token = await this.config.tokenProvider.getLicenseToken();
74
+
75
+ if (!token) {
76
+ throw new DRMError('No license token available', 'LICENSE_TOKEN_ERROR');
77
+ }
78
+
79
+ const headers: Record<string, string> = {
80
+ 'Content-Type': 'application/octet-stream',
81
+ 'X-Bunny-Token': token.token,
82
+ 'X-Bunny-Expires': token.expires.toString(),
83
+ };
84
+
85
+ if (token.tokenVer) {
86
+ headers['X-Bunny-Token-Version'] = token.tokenVer;
87
+ }
88
+
89
+ if (this.config.enableReferrerProtection) {
90
+ headers['Referer'] = window.location.origin;
91
+ }
92
+
93
+ return headers;
94
+ }
95
+
96
+ /**
97
+ * Check if token needs renewal (within 5 minutes of expiry)
98
+ */
99
+ shouldRenewToken(token: DRMToken): boolean {
100
+ const nowSeconds = Math.floor(Date.now() / 1000);
101
+ const bufferSeconds = 300; // 5 minutes
102
+ return token.expires - nowSeconds < bufferSeconds;
103
+ }
104
+ }
@@ -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
+ }