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.
- package/package.json +1 -1
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts +22 -0
- package/packages/core/dist/interfaces/IVideoPlayer.d.ts.map +1 -1
- package/packages/core/src/interfaces/IVideoPlayer.ts +27 -0
- package/packages/web/dist/WebPlayer.d.ts +4 -0
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +68 -4
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts +13 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.d.ts.map +1 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.js +65 -0
- package/packages/web/dist/drm/BunnyCDNDRMProvider.js.map +1 -0
- package/packages/web/dist/drm/DRMManager.d.ts +13 -0
- package/packages/web/dist/drm/DRMManager.d.ts.map +1 -0
- package/packages/web/dist/drm/DRMManager.js +59 -0
- package/packages/web/dist/drm/DRMManager.js.map +1 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.d.ts +24 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.js +190 -0
- package/packages/web/dist/drm/FairPlayDRMHandler.js.map +1 -0
- package/packages/web/dist/drm/WidevineDRMHandler.d.ts +21 -0
- package/packages/web/dist/drm/WidevineDRMHandler.d.ts.map +1 -0
- package/packages/web/dist/drm/WidevineDRMHandler.js +143 -0
- package/packages/web/dist/drm/WidevineDRMHandler.js.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts +52 -0
- package/packages/web/dist/drm/types/DRMTypes.d.ts.map +1 -0
- package/packages/web/dist/drm/types/DRMTypes.js +15 -0
- package/packages/web/dist/drm/types/DRMTypes.js.map +1 -0
- package/packages/web/dist/react/WebPlayerView.d.ts +18 -1
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +28 -0
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +96 -4
- package/packages/web/src/drm/BunnyCDNDRMProvider.ts +104 -0
- package/packages/web/src/drm/DRMManager.ts +99 -0
- package/packages/web/src/drm/FairPlayDRMHandler.ts +322 -0
- package/packages/web/src/drm/WidevineDRMHandler.ts +246 -0
- package/packages/web/src/drm/types/DRMTypes.ts +97 -0
- 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
|
+
}
|