unified-video-framework 1.4.412 → 1.4.414

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 (147) hide show
  1. package/package.json +6 -1
  2. package/packages/core/dist/interfaces/IVideoPlayer.d.ts +0 -22
  3. package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
  4. package/packages/core/src/BasePlayer.d.ts +61 -0
  5. package/packages/core/src/BasePlayer.d.ts.map +1 -0
  6. package/packages/core/src/BasePlayer.js +175 -0
  7. package/packages/core/src/BasePlayer.js.map +1 -0
  8. package/packages/core/src/VideoPlayerFactory.d.ts +8 -0
  9. package/packages/core/src/VideoPlayerFactory.d.ts.map +1 -0
  10. package/packages/core/src/VideoPlayerFactory.js +95 -0
  11. package/packages/core/src/VideoPlayerFactory.js.map +1 -0
  12. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.d.ts +18 -0
  13. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.d.ts.map +1 -0
  14. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.js +117 -0
  15. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.js.map +1 -0
  16. package/packages/core/src/analytics/core/AnalyticsProvider.d.ts +18 -0
  17. package/packages/core/src/analytics/core/AnalyticsProvider.d.ts.map +1 -0
  18. package/packages/core/src/analytics/core/AnalyticsProvider.js +99 -0
  19. package/packages/core/src/analytics/core/AnalyticsProvider.js.map +1 -0
  20. package/packages/core/src/analytics/core/DynamicAnalyticsManager.d.ts +20 -0
  21. package/packages/core/src/analytics/core/DynamicAnalyticsManager.d.ts.map +1 -0
  22. package/packages/core/src/analytics/core/DynamicAnalyticsManager.js +161 -0
  23. package/packages/core/src/analytics/core/DynamicAnalyticsManager.js.map +1 -0
  24. package/packages/core/src/analytics/core/EventBatcher.d.ts +32 -0
  25. package/packages/core/src/analytics/core/EventBatcher.d.ts.map +1 -0
  26. package/packages/core/src/analytics/core/EventBatcher.js +98 -0
  27. package/packages/core/src/analytics/core/EventBatcher.js.map +1 -0
  28. package/packages/core/src/analytics/core/PlayerAnalytics.d.ts +19 -0
  29. package/packages/core/src/analytics/core/PlayerAnalytics.d.ts.map +1 -0
  30. package/packages/core/src/analytics/core/PlayerAnalytics.js +80 -0
  31. package/packages/core/src/analytics/core/PlayerAnalytics.js.map +1 -0
  32. package/packages/core/src/analytics/index.d.ts +13 -0
  33. package/packages/core/src/analytics/index.d.ts.map +1 -0
  34. package/packages/core/src/analytics/index.js +13 -0
  35. package/packages/core/src/analytics/index.js.map +1 -0
  36. package/packages/core/src/analytics/types/AnalyticsTypes.d.ts +239 -0
  37. package/packages/core/src/analytics/types/AnalyticsTypes.d.ts.map +1 -0
  38. package/packages/core/src/analytics/types/AnalyticsTypes.js +8 -0
  39. package/packages/core/src/analytics/types/AnalyticsTypes.js.map +1 -0
  40. package/packages/core/src/analytics/utils/DeviceDetection.d.ts +27 -0
  41. package/packages/core/src/analytics/utils/DeviceDetection.d.ts.map +1 -0
  42. package/packages/core/src/analytics/utils/DeviceDetection.js +184 -0
  43. package/packages/core/src/analytics/utils/DeviceDetection.js.map +1 -0
  44. package/packages/core/src/chapter-manager.d.ts +39 -0
  45. package/packages/core/src/chapter-manager.d.ts.map +1 -0
  46. package/packages/core/src/chapter-manager.js +173 -0
  47. package/packages/core/src/chapter-manager.js.map +1 -0
  48. package/packages/core/src/index.d.ts +10 -0
  49. package/packages/core/src/index.d.ts.map +1 -0
  50. package/packages/core/src/index.js +8 -0
  51. package/packages/core/src/index.js.map +1 -0
  52. package/packages/core/src/interfaces/IVideoPlayer.d.ts +229 -0
  53. package/packages/core/src/interfaces/IVideoPlayer.d.ts.map +1 -0
  54. package/packages/core/src/interfaces/IVideoPlayer.js +2 -0
  55. package/packages/core/src/interfaces/IVideoPlayer.js.map +1 -0
  56. package/packages/core/src/interfaces/IVideoPlayer.ts +0 -27
  57. package/packages/core/src/interfaces.d.ts +455 -0
  58. package/packages/core/src/interfaces.d.ts.map +1 -0
  59. package/packages/core/src/interfaces.js +32 -0
  60. package/packages/core/src/interfaces.js.map +1 -0
  61. package/packages/core/src/utils/EventEmitter.d.ts +14 -0
  62. package/packages/core/src/utils/EventEmitter.d.ts.map +1 -0
  63. package/packages/core/src/utils/EventEmitter.js +55 -0
  64. package/packages/core/src/utils/EventEmitter.js.map +1 -0
  65. package/packages/web/dist/WebPlayer.d.ts +0 -4
  66. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  67. package/packages/web/dist/WebPlayer.js +62 -73
  68. package/packages/web/dist/WebPlayer.js.map +1 -1
  69. package/packages/web/dist/drm/DRMManager.d.ts +16 -9
  70. package/packages/web/dist/drm/DRMManager.d.ts.map +1 -1
  71. package/packages/web/dist/drm/DRMManager.js +109 -42
  72. package/packages/web/dist/drm/DRMManager.js.map +1 -1
  73. package/packages/web/dist/drm/index.d.ts +15 -0
  74. package/packages/web/dist/drm/index.d.ts.map +1 -0
  75. package/packages/web/dist/drm/index.js +13 -0
  76. package/packages/web/dist/drm/index.js.map +1 -0
  77. package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts +19 -0
  78. package/packages/web/dist/drm/providers/BunnyNetProvider.d.ts.map +1 -0
  79. package/packages/web/dist/drm/providers/BunnyNetProvider.js +112 -0
  80. package/packages/web/dist/drm/providers/BunnyNetProvider.js.map +1 -0
  81. package/packages/web/dist/drm/providers/GenericProvider.d.ts +30 -0
  82. package/packages/web/dist/drm/providers/GenericProvider.d.ts.map +1 -0
  83. package/packages/web/dist/drm/providers/GenericProvider.js +104 -0
  84. package/packages/web/dist/drm/providers/GenericProvider.js.map +1 -0
  85. package/packages/web/dist/drm/systems/BaseDRM.d.ts +18 -0
  86. package/packages/web/dist/drm/systems/BaseDRM.d.ts.map +1 -0
  87. package/packages/web/dist/drm/systems/BaseDRM.js +29 -0
  88. package/packages/web/dist/drm/systems/BaseDRM.js.map +1 -0
  89. package/packages/web/dist/drm/systems/FairPlayDRM.d.ts +32 -0
  90. package/packages/web/dist/drm/systems/FairPlayDRM.d.ts.map +1 -0
  91. package/packages/web/dist/drm/systems/FairPlayDRM.js +198 -0
  92. package/packages/web/dist/drm/systems/FairPlayDRM.js.map +1 -0
  93. package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts +9 -0
  94. package/packages/web/dist/drm/systems/PlayReadyDRM.d.ts.map +1 -0
  95. package/packages/web/dist/drm/systems/PlayReadyDRM.js +92 -0
  96. package/packages/web/dist/drm/systems/PlayReadyDRM.js.map +1 -0
  97. package/packages/web/dist/drm/systems/WidevineDRM.d.ts +9 -0
  98. package/packages/web/dist/drm/systems/WidevineDRM.d.ts.map +1 -0
  99. package/packages/web/dist/drm/systems/WidevineDRM.js +73 -0
  100. package/packages/web/dist/drm/systems/WidevineDRM.js.map +1 -0
  101. package/packages/web/dist/drm/types/BunnyNetTypes.d.ts +20 -0
  102. package/packages/web/dist/drm/types/BunnyNetTypes.d.ts.map +1 -0
  103. package/packages/web/dist/drm/types/BunnyNetTypes.js +8 -0
  104. package/packages/web/dist/drm/types/BunnyNetTypes.js.map +1 -0
  105. package/packages/web/dist/drm/types/DRMTypes.d.ts +56 -49
  106. package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -1
  107. package/packages/web/dist/drm/types/DRMTypes.js +19 -13
  108. package/packages/web/dist/drm/types/DRMTypes.js.map +1 -1
  109. package/packages/web/dist/drm/utils/BrowserDetector.d.ts +20 -0
  110. package/packages/web/dist/drm/utils/BrowserDetector.d.ts.map +1 -0
  111. package/packages/web/dist/drm/utils/BrowserDetector.js +168 -0
  112. package/packages/web/dist/drm/utils/BrowserDetector.js.map +1 -0
  113. package/packages/web/dist/drm/utils/CertificateManager.d.ts +15 -0
  114. package/packages/web/dist/drm/utils/CertificateManager.d.ts.map +1 -0
  115. package/packages/web/dist/drm/utils/CertificateManager.js +46 -0
  116. package/packages/web/dist/drm/utils/CertificateManager.js.map +1 -0
  117. package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts +7 -0
  118. package/packages/web/dist/drm/utils/DRMErrorHandler.d.ts.map +1 -0
  119. package/packages/web/dist/drm/utils/DRMErrorHandler.js +49 -0
  120. package/packages/web/dist/drm/utils/DRMErrorHandler.js.map +1 -0
  121. package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts +15 -0
  122. package/packages/web/dist/drm/utils/LicenseRequestHandler.d.ts.map +1 -0
  123. package/packages/web/dist/drm/utils/LicenseRequestHandler.js +110 -0
  124. package/packages/web/dist/drm/utils/LicenseRequestHandler.js.map +1 -0
  125. package/packages/web/dist/react/WebPlayerView.d.ts +1 -18
  126. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  127. package/packages/web/dist/react/WebPlayerView.js +0 -5
  128. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  129. package/packages/web/src/WebPlayer.ts +79 -106
  130. package/packages/web/src/drm/DRMManager.ts +214 -99
  131. package/packages/web/src/drm/index.ts +37 -0
  132. package/packages/web/src/drm/providers/BunnyNetProvider.ts +171 -0
  133. package/packages/web/src/drm/providers/GenericProvider.ts +151 -0
  134. package/packages/web/src/drm/systems/BaseDRM.ts +68 -0
  135. package/packages/web/src/drm/systems/FairPlayDRM.ts +306 -0
  136. package/packages/web/src/drm/systems/PlayReadyDRM.ts +132 -0
  137. package/packages/web/src/drm/systems/WidevineDRM.ts +106 -0
  138. package/packages/web/src/drm/types/BunnyNetTypes.ts +35 -0
  139. package/packages/web/src/drm/types/DRMTypes.ts +92 -97
  140. package/packages/web/src/drm/utils/BrowserDetector.ts +233 -0
  141. package/packages/web/src/drm/utils/CertificateManager.ts +86 -0
  142. package/packages/web/src/drm/utils/DRMErrorHandler.ts +84 -0
  143. package/packages/web/src/drm/utils/LicenseRequestHandler.ts +180 -0
  144. package/packages/web/src/react/WebPlayerView.tsx +1 -28
  145. package/packages/web/src/drm/BunnyCDNDRMProvider.ts +0 -104
  146. package/packages/web/src/drm/FairPlayDRMHandler.ts +0 -322
  147. package/packages/web/src/drm/WidevineDRMHandler.ts +0 -246
@@ -0,0 +1,180 @@
1
+ /**
2
+ * License Request Handler
3
+ * Centralized HTTP request handling for license servers with retry logic
4
+ */
5
+
6
+ import { ExtendedDRMConfig, LicenseRequest, LicenseResponse, DRMErrorCode } from '../types/DRMTypes';
7
+
8
+ export class LicenseRequestHandler {
9
+ private config: ExtendedDRMConfig;
10
+ private debug: boolean;
11
+
12
+ constructor(config: ExtendedDRMConfig, debug: boolean = false) {
13
+ this.config = config;
14
+ this.debug = debug;
15
+ }
16
+
17
+ /**
18
+ * Request license from server with retry logic
19
+ */
20
+ async requestLicense(request: LicenseRequest): Promise<LicenseResponse> {
21
+ const maxRetries = this.config.retryConfig?.maxRetries || 3;
22
+ const retryDelay = this.config.retryConfig?.retryDelay || 1000;
23
+
24
+ let lastError: any;
25
+
26
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
27
+ try {
28
+ if (attempt > 0) {
29
+ this.log(`Retry attempt ${attempt}/${maxRetries}`);
30
+ await this.delay(retryDelay * attempt); // Exponential backoff
31
+ }
32
+
33
+ const response = await this.makeRequest(request);
34
+ return response;
35
+ } catch (error: any) {
36
+ lastError = error;
37
+ this.log(`License request failed (attempt ${attempt + 1}/${maxRetries + 1}):`, error);
38
+
39
+ // Don't retry on certain errors
40
+ if (this.isFatalError(error)) {
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+
46
+ // All retries exhausted
47
+ throw this.createError(
48
+ DRMErrorCode.LICENSE_REQUEST_FAILED,
49
+ `Failed to request license after ${maxRetries + 1} attempts`,
50
+ lastError
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Make HTTP request to license server
56
+ */
57
+ private async makeRequest(request: LicenseRequest): Promise<LicenseResponse> {
58
+ this.log('Requesting license from:', request.url);
59
+
60
+ try {
61
+ const response = await fetch(request.url, {
62
+ method: request.method,
63
+ headers: request.headers,
64
+ body: request.body,
65
+ });
66
+
67
+ if (!response.ok) {
68
+ throw this.createError(
69
+ DRMErrorCode.LICENSE_REQUEST_FAILED,
70
+ `License server returned ${response.status}: ${response.statusText}`,
71
+ { status: response.status, statusText: response.statusText }
72
+ );
73
+ }
74
+
75
+ // Parse response based on type
76
+ let license: ArrayBuffer;
77
+
78
+ if (request.responseType === 'arraybuffer') {
79
+ license = await response.arrayBuffer();
80
+ } else if (request.responseType === 'text') {
81
+ const text = await response.text();
82
+ license = this.textToArrayBuffer(text);
83
+ } else if (request.responseType === 'json') {
84
+ const json = await response.json();
85
+ // Assume license is in a 'license' field or is base64-encoded
86
+ if (json.license) {
87
+ license = this.base64ToArrayBuffer(json.license);
88
+ } else {
89
+ throw this.createError(
90
+ DRMErrorCode.LICENSE_INVALID,
91
+ 'License response does not contain license field',
92
+ json
93
+ );
94
+ }
95
+ } else {
96
+ license = await response.arrayBuffer();
97
+ }
98
+
99
+ this.log('License received, size:', license.byteLength, 'bytes');
100
+
101
+ return {
102
+ license,
103
+ metadata: response.headers,
104
+ };
105
+ } catch (error: any) {
106
+ if (error.code && error.code.startsWith('DRM_')) {
107
+ throw error;
108
+ }
109
+
110
+ throw this.createError(
111
+ DRMErrorCode.NETWORK_ERROR,
112
+ `Network error requesting license: ${error.message}`,
113
+ error
114
+ );
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Check if error is fatal (should not retry)
120
+ */
121
+ private isFatalError(error: any): boolean {
122
+ // Don't retry authentication errors (401, 403)
123
+ if (error.details?.status === 401 || error.details?.status === 403) {
124
+ return true;
125
+ }
126
+
127
+ // Don't retry invalid license errors
128
+ if (error.code === DRMErrorCode.LICENSE_INVALID) {
129
+ return true;
130
+ }
131
+
132
+ return false;
133
+ }
134
+
135
+ /**
136
+ * Convert text to ArrayBuffer
137
+ */
138
+ private textToArrayBuffer(text: string): ArrayBuffer {
139
+ const encoder = new TextEncoder();
140
+ return encoder.encode(text).buffer;
141
+ }
142
+
143
+ /**
144
+ * Convert base64 string to ArrayBuffer
145
+ */
146
+ private base64ToArrayBuffer(base64: string): ArrayBuffer {
147
+ const binaryString = atob(base64);
148
+ const bytes = new Uint8Array(binaryString.length);
149
+ for (let i = 0; i < binaryString.length; i++) {
150
+ bytes[i] = binaryString.charCodeAt(i);
151
+ }
152
+ return bytes.buffer;
153
+ }
154
+
155
+ /**
156
+ * Delay helper for retry logic
157
+ */
158
+ private delay(ms: number): Promise<void> {
159
+ return new Promise(resolve => setTimeout(resolve, ms));
160
+ }
161
+
162
+ /**
163
+ * Create standardized DRM error
164
+ */
165
+ private createError(code: DRMErrorCode, message: string, details?: any): any {
166
+ const error = new Error(message);
167
+ (error as any).code = code;
168
+ (error as any).details = details;
169
+ return error;
170
+ }
171
+
172
+ /**
173
+ * Debug logging
174
+ */
175
+ private log(...args: any[]): void {
176
+ if (this.debug) {
177
+ console.log('[LicenseRequestHandler]', ...args);
178
+ }
179
+ }
180
+ }
@@ -1,7 +1,7 @@
1
1
  // @ts-nocheck
2
2
  import React, { useEffect, useRef, useState, useCallback } from 'react';
3
3
  import type { CSSProperties } from 'react';
4
- import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig, DRMTokenProvider, DRMToken, BunnyCDNDRMConfig } from '../../core/dist';
4
+ import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig } from '../../core/dist';
5
5
  import { WebPlayer } from '../WebPlayer';
6
6
  import { GoogleAdsManager } from '../ads/GoogleAdsManager';
7
7
  // EPG imports - conditionally loaded
@@ -224,26 +224,6 @@ export type WebPlayerViewProps = {
224
224
  // Source config
225
225
  url: string;
226
226
  type?: 'mp4' | 'hls' | 'dash' | 'webm' | 'auto';
227
-
228
- // DRM configuration (optional)
229
- drm?: {
230
- bunny?: {
231
- libraryId: string;
232
- videoId: string;
233
- pullZoneUrl: string;
234
- tokenProvider: DRMTokenProvider;
235
- certificateUrl?: string;
236
- licenseUrl?: string;
237
- enableReferrerProtection?: boolean;
238
- };
239
- generic?: {
240
- type: 'widevine' | 'fairplay';
241
- licenseUrl: string;
242
- certificateUrl?: string;
243
- headers?: Record<string, string>;
244
- };
245
- };
246
-
247
227
  subtitles?: SubtitleTrack[];
248
228
  metadata?: VideoMetadata;
249
229
 
@@ -1040,13 +1020,6 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1040
1020
  fallbackRetryDelay: props.fallbackRetryDelay,
1041
1021
  fallbackRetryAttempts: props.fallbackRetryAttempts,
1042
1022
  onAllSourcesFailed: props.onAllSourcesFailed,
1043
-
1044
- // Pass DRM config if provided
1045
- drm: props.drm ? {
1046
- type: props.drm.bunny ? 'widevine' : (props.drm.generic?.type || 'widevine'),
1047
- licenseUrl: props.drm.bunny ? '' : (props.drm.generic?.licenseUrl || ''),
1048
- bunny: props.drm.bunny
1049
- } as any : undefined
1050
1023
  };
1051
1024
 
1052
1025
  await player.load(source);
@@ -1,104 +0,0 @@
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
- }
@@ -1,322 +0,0 @@
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
- }