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,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
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * DRM Types and Interfaces
3
+ * Core types for the web DRM system supporting Bunny CDN and generic DRM providers
4
+ */
5
+
6
+ // Token provider interface - allows flexible token injection strategies
7
+ export interface DRMTokenProvider {
8
+ // Get token for manifest/playlist requests
9
+ getManifestToken(): Promise<DRMToken | null>;
10
+
11
+ // Get token for media segment requests
12
+ getSegmentToken(): Promise<DRMToken | null>;
13
+
14
+ // Get token for license server requests
15
+ getLicenseToken(): Promise<DRMToken | null>;
16
+
17
+ // Check if token needs renewal
18
+ shouldRenewToken(token: DRMToken): boolean;
19
+
20
+ // Refresh/renew token
21
+ renewToken(token: DRMToken): Promise<DRMToken>;
22
+ }
23
+
24
+ // Token structure
25
+ export interface DRMToken {
26
+ token: string; // The actual token value
27
+ expires: number; // Unix timestamp (seconds)
28
+ tokenVer?: string; // Optional token version
29
+ }
30
+
31
+ // Bunny CDN specific configuration
32
+ export interface BunnyCDNDRMConfig {
33
+ libraryId: string; // Bunny CDN library ID
34
+ videoId: string; // Video ID on Bunny CDN
35
+ pullZoneUrl: string; // e.g., "stream.example.com"
36
+ tokenProvider: DRMTokenProvider; // Token provider implementation
37
+ certificateUrl?: string; // Override default FairPlay certificate URL
38
+ licenseUrl?: string; // Override default license URL
39
+ enableReferrerProtection?: boolean; // Enable referrer header validation
40
+ }
41
+
42
+ // Generic DRM configuration (for non-Bunny providers)
43
+ export interface GenericDRMConfig {
44
+ type: 'widevine' | 'fairplay';
45
+ licenseUrl: string;
46
+ certificateUrl?: string; // Required for FairPlay
47
+ headers?: Record<string, string>; // Custom headers for license requests
48
+ certificateHeaders?: Record<string, string>; // Custom headers for certificate requests
49
+ }
50
+
51
+ // Unified DRM configuration
52
+ export interface WebDRMConfig {
53
+ // Provider type
54
+ provider: 'bunny' | 'generic';
55
+
56
+ // Provider-specific config
57
+ bunny?: BunnyCDNDRMConfig;
58
+ generic?: GenericDRMConfig;
59
+ }
60
+
61
+ // DRM Handler interface (implemented by Widevine and FairPlay handlers)
62
+ export interface IDRMHandler {
63
+ // Initialize DRM system
64
+ initialize(): Promise<void>;
65
+
66
+ // Setup DRM for video element
67
+ setupDRM(videoElement: HTMLVideoElement, manifestUrl: string): Promise<void>;
68
+
69
+ // Handle license requests
70
+ onLicenseRequest(licenseRequest: ArrayBuffer): Promise<ArrayBuffer>;
71
+
72
+ // Cleanup resources
73
+ destroy(): void;
74
+
75
+ // Check if DRM is supported in current browser
76
+ isSupported(): boolean;
77
+ }
78
+
79
+ // EME key system identifiers
80
+ export const DRM_KEY_SYSTEMS = {
81
+ WIDEVINE: 'com.widevine.alpha',
82
+ FAIRPLAY: 'com.apple.fps.1_0',
83
+ PLAYREADY: 'com.microsoft.playready',
84
+ CLEARKEY: 'org.w3.clearkey'
85
+ } as const;
86
+
87
+ // Error types
88
+ export class DRMError extends Error {
89
+ constructor(
90
+ message: string,
91
+ public code: string,
92
+ public details?: any
93
+ ) {
94
+ super(message);
95
+ this.name = 'DRMError';
96
+ }
97
+ }
@@ -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 } from '../../core/dist';
4
+ import type { VideoSource, SubtitleTrack, VideoMetadata, PlayerConfig, DRMTokenProvider, DRMToken, BunnyCDNDRMConfig } from '../../core/dist';
5
5
  import { WebPlayer } from '../WebPlayer';
6
6
  import { GoogleAdsManager } from '../ads/GoogleAdsManager';
7
7
  // EPG imports - conditionally loaded
@@ -224,6 +224,26 @@ 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
+
227
247
  subtitles?: SubtitleTrack[];
228
248
  metadata?: VideoMetadata;
229
249
 
@@ -1020,6 +1040,13 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1020
1040
  fallbackRetryDelay: props.fallbackRetryDelay,
1021
1041
  fallbackRetryAttempts: props.fallbackRetryAttempts,
1022
1042
  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
1023
1050
  };
1024
1051
 
1025
1052
  await player.load(source);
@@ -1049,17 +1076,40 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1049
1076
  // Helper function to inject ad markers
1050
1077
  const injectAdMarkersFromTimes = (adTimes: number[]) => {
1051
1078
  if (!adTimes || adTimes.length === 0) return;
1052
-
1079
+
1053
1080
  const duration = (player as any).getDuration?.() || 0;
1081
+
1082
+ // If duration is available, inject immediately
1054
1083
  if (duration > 0) {
1055
1084
  const adMarkers = generateAdMarkers(adTimes, duration);
1056
1085
  const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, duration);
1057
-
1086
+
1058
1087
  // Inject ad markers into the chapter system
1059
1088
  if (typeof (player as any).loadChapters === 'function' && mergedChapters.data) {
1060
1089
  (player as any).loadChapters(mergedChapters.data);
1061
1090
  console.log('✅ Ad markers injected:', adMarkers.length, 'markers at times:', adTimes);
1062
1091
  }
1092
+ } else {
1093
+ // Duration not available yet - wait for loadedmetadata event
1094
+ console.log('⏳ Duration not available yet, waiting for metadata to inject ad markers...');
1095
+ const videoElement = (player as any).video || (player as any).getVideoElement?.();
1096
+ if (videoElement) {
1097
+ const retryInject = () => {
1098
+ const retryDuration = (player as any).getDuration?.() || 0;
1099
+ if (retryDuration > 0) {
1100
+ const adMarkers = generateAdMarkers(adTimes, retryDuration);
1101
+ const mergedChapters = mergeAdMarkersWithChapters(props.chapters, adMarkers, retryDuration);
1102
+
1103
+ if (typeof (player as any).loadChapters === 'function' && mergedChapters.data) {
1104
+ (player as any).loadChapters(mergedChapters.data);
1105
+ console.log('✅ Ad markers injected (after metadata):', adMarkers.length, 'markers at times:', adTimes);
1106
+ }
1107
+ } else {
1108
+ console.warn('⚠️ Duration still not available after metadata loaded');
1109
+ }
1110
+ };
1111
+ videoElement.addEventListener('loadedmetadata', retryInject, { once: true });
1112
+ }
1063
1113
  }
1064
1114
  };
1065
1115
 
@@ -1373,7 +1423,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
1373
1423
 
1374
1424
  // Inject markers from VMAP cue points (if midrollTimes not provided)
1375
1425
  if (!props.googleAds?.midrollTimes || props.googleAds.midrollTimes.length === 0) {
1426
+ const currentDuration = (player as any).getDuration?.() || 0;
1376
1427
  console.log('🔵 Using VMAP cue points for ad markers:', cuePoints);
1428
+ console.log('📊 Current video duration:', currentDuration, 'seconds');
1377
1429
  injectAdMarkersFromTimes(cuePoints);
1378
1430
  }
1379
1431
  },