unified-video-framework 1.4.376 → 1.4.378
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/IDRMProtection.d.ts +90 -0
- package/packages/core/dist/interfaces/IDRMProtection.d.ts.map +1 -0
- package/packages/core/dist/interfaces/IDRMProtection.js +15 -0
- package/packages/core/dist/interfaces/IDRMProtection.js.map +1 -0
- package/packages/core/dist/interfaces.d.ts +1 -0
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/dist/interfaces.js +1 -0
- package/packages/core/dist/interfaces.js.map +1 -1
- package/packages/core/src/interfaces/IDRMProtection.ts +285 -0
- package/packages/core/src/interfaces.ts +2 -0
- package/packages/react-native/dist/drm/AndroidDRMProtection.d.ts +21 -0
- package/packages/react-native/dist/drm/AndroidDRMProtection.d.ts.map +1 -0
- package/packages/react-native/dist/drm/AndroidDRMProtection.js +184 -0
- package/packages/react-native/dist/drm/AndroidDRMProtection.js.map +1 -0
- package/packages/react-native/dist/drm/iOSDRMProtection.d.ts +21 -0
- package/packages/react-native/dist/drm/iOSDRMProtection.d.ts.map +1 -0
- package/packages/react-native/dist/drm/iOSDRMProtection.js +172 -0
- package/packages/react-native/dist/drm/iOSDRMProtection.js.map +1 -0
- package/packages/react-native/src/drm/AndroidDRMProtection.ts +419 -0
- package/packages/react-native/src/drm/iOSDRMProtection.ts +415 -0
- package/packages/web/dist/drm/WebDRMProtection.d.ts +37 -0
- package/packages/web/dist/drm/WebDRMProtection.d.ts.map +1 -0
- package/packages/web/dist/drm/WebDRMProtection.js +378 -0
- package/packages/web/dist/drm/WebDRMProtection.js.map +1 -0
- package/packages/web/dist/index.d.ts +1 -0
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +1 -0
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +17 -42
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/src/drm/WebDRMProtection.ts +596 -0
- package/packages/web/src/index.ts +3 -0
- package/packages/web/src/react/WebPlayerView.tsx +27 -52
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web DRM Protection Implementation
|
|
3
|
+
*
|
|
4
|
+
* Implements Netflix-like content protection for web browsers using:
|
|
5
|
+
* - Encrypted Media Extensions (EME)
|
|
6
|
+
* - Widevine / PlayReady DRM
|
|
7
|
+
* - Screen recording detection
|
|
8
|
+
* - Tab capture blocking
|
|
9
|
+
* - Chromecast support
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
IDRMProtection,
|
|
14
|
+
IDRMProtectionConfig,
|
|
15
|
+
IDRMProtectionStatus,
|
|
16
|
+
CastDevice,
|
|
17
|
+
CastDeviceType,
|
|
18
|
+
DRMError,
|
|
19
|
+
DRMErrorCode,
|
|
20
|
+
DRMKeySystemConfig,
|
|
21
|
+
} from '@unified-video/core';
|
|
22
|
+
|
|
23
|
+
export class WebDRMProtection implements IDRMProtection {
|
|
24
|
+
private config: IDRMProtectionConfig;
|
|
25
|
+
private videoElement: HTMLVideoElement;
|
|
26
|
+
private mediaKeys: MediaKeys | null = null;
|
|
27
|
+
private status: IDRMProtectionStatus;
|
|
28
|
+
private screenRecordingCheckInterval: number | null = null;
|
|
29
|
+
private castSession: any = null;
|
|
30
|
+
private castContext: any = null;
|
|
31
|
+
|
|
32
|
+
// Key Systems in preference order
|
|
33
|
+
private readonly KEY_SYSTEMS = {
|
|
34
|
+
widevine: 'com.widevine.alpha',
|
|
35
|
+
playready: 'com.microsoft.playready',
|
|
36
|
+
clearkey: 'org.w3.clearkey', // Fallback for testing
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
constructor(videoElement: HTMLVideoElement) {
|
|
40
|
+
this.videoElement = videoElement;
|
|
41
|
+
this.config = { enabled: false };
|
|
42
|
+
this.status = {
|
|
43
|
+
isProtected: false,
|
|
44
|
+
drmSystem: 'none',
|
|
45
|
+
isScreenRecordingBlocked: false,
|
|
46
|
+
isAudioCaptureBlocked: false,
|
|
47
|
+
isScreenshotBlocked: false,
|
|
48
|
+
isCasting: false,
|
|
49
|
+
screenRecordingDetected: false,
|
|
50
|
+
mirroringDetected: false,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize DRM protection
|
|
56
|
+
*/
|
|
57
|
+
async initialize(config: IDRMProtectionConfig): Promise<void> {
|
|
58
|
+
this.config = {
|
|
59
|
+
blockScreenRecording: true,
|
|
60
|
+
blockAudioCapture: true,
|
|
61
|
+
blockScreenshots: true,
|
|
62
|
+
allowCasting: true,
|
|
63
|
+
blockMirroring: true,
|
|
64
|
+
widevineSecurityLevel: 'L1',
|
|
65
|
+
...config,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
if (!this.config.enabled) {
|
|
69
|
+
console.log('[DRM] Protection disabled by configuration');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('[DRM] Initializing web DRM protection...', this.config);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Check if EME is supported
|
|
77
|
+
if (!this.isEMESupported()) {
|
|
78
|
+
throw this.createError(
|
|
79
|
+
DRMErrorCode.DEVICE_NOT_SUPPORTED,
|
|
80
|
+
'Encrypted Media Extensions (EME) not supported'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Initialize DRM key system
|
|
85
|
+
if (this.config.licenseServerUrl) {
|
|
86
|
+
await this.initializeDRMKeySystem();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Start screen recording detection
|
|
90
|
+
if (this.config.blockScreenRecording) {
|
|
91
|
+
this.startScreenRecordingDetection();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Initialize casting support
|
|
95
|
+
if (this.config.allowCasting) {
|
|
96
|
+
await this.initializeCastSupport();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Block screenshot capture
|
|
100
|
+
if (this.config.blockScreenshots) {
|
|
101
|
+
this.blockScreenshots();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Prevent tab capture
|
|
105
|
+
if (this.config.blockMirroring) {
|
|
106
|
+
this.preventTabCapture();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.status.isProtected = true;
|
|
110
|
+
console.log('[DRM] Protection initialized successfully');
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error('[DRM] Initialization failed:', error);
|
|
113
|
+
this.config.onDRMError?.(error as DRMError);
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if EME is supported
|
|
120
|
+
*/
|
|
121
|
+
private isEMESupported(): boolean {
|
|
122
|
+
return !!(
|
|
123
|
+
window.navigator &&
|
|
124
|
+
(window.navigator as any).requestMediaKeySystemAccess &&
|
|
125
|
+
window.MediaKeys
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Initialize DRM Key System (Widevine/PlayReady)
|
|
131
|
+
*/
|
|
132
|
+
private async initializeDRMKeySystem(): Promise<void> {
|
|
133
|
+
console.log('[DRM] Initializing key system...');
|
|
134
|
+
|
|
135
|
+
// Try Widevine first, then PlayReady
|
|
136
|
+
const keySystemConfigs: DRMKeySystemConfig[] = [
|
|
137
|
+
{
|
|
138
|
+
keySystem: this.KEY_SYSTEMS.widevine,
|
|
139
|
+
licenseServerUrl: this.config.licenseServerUrl!,
|
|
140
|
+
certificateUrl: this.config.certificateUrl,
|
|
141
|
+
headers: this.config.licenseHeaders,
|
|
142
|
+
// Widevine robustness levels
|
|
143
|
+
videoRobustness: this.config.widevineSecurityLevel === 'L1' ? 'HW_SECURE_ALL' : 'SW_SECURE_CRYPTO',
|
|
144
|
+
audioRobustness: this.config.widevineSecurityLevel === 'L1' ? 'HW_SECURE_ALL' : 'SW_SECURE_CRYPTO',
|
|
145
|
+
persistentState: 'optional',
|
|
146
|
+
distinctiveIdentifier: 'optional',
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
keySystem: this.KEY_SYSTEMS.playready,
|
|
150
|
+
licenseServerUrl: this.config.licenseServerUrl!,
|
|
151
|
+
headers: this.config.licenseHeaders,
|
|
152
|
+
videoRobustness: '3000', // PlayReady SL3000
|
|
153
|
+
audioRobustness: '3000',
|
|
154
|
+
persistentState: 'optional',
|
|
155
|
+
distinctiveIdentifier: 'optional',
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
for (const config of keySystemConfigs) {
|
|
160
|
+
try {
|
|
161
|
+
await this.requestMediaKeySystemAccess(config);
|
|
162
|
+
console.log(`[DRM] Successfully initialized ${config.keySystem}`);
|
|
163
|
+
return;
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn(`[DRM] ${config.keySystem} not available:`, error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw this.createError(
|
|
170
|
+
DRMErrorCode.WIDEVINE_NOT_AVAILABLE,
|
|
171
|
+
'No supported DRM system available (tried Widevine and PlayReady)'
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Request Media Key System Access
|
|
177
|
+
*/
|
|
178
|
+
private async requestMediaKeySystemAccess(config: DRMKeySystemConfig): Promise<void> {
|
|
179
|
+
const keySystemConfig = [
|
|
180
|
+
{
|
|
181
|
+
initDataTypes: ['cenc', 'keyids', 'webm'],
|
|
182
|
+
audioCapabilities: [
|
|
183
|
+
{
|
|
184
|
+
contentType: 'audio/mp4; codecs="mp4a.40.2"',
|
|
185
|
+
robustness: config.audioRobustness || '',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
videoCapabilities: [
|
|
189
|
+
{
|
|
190
|
+
contentType: 'video/mp4; codecs="avc1.42E01E"',
|
|
191
|
+
robustness: config.videoRobustness || '',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
contentType: 'video/webm; codecs="vp9"',
|
|
195
|
+
robustness: config.videoRobustness || '',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
distinctiveIdentifier: config.distinctiveIdentifier || 'optional',
|
|
199
|
+
persistentState: config.persistentState || 'optional',
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const keySystemAccess = await navigator.requestMediaKeySystemAccess(
|
|
204
|
+
config.keySystem,
|
|
205
|
+
keySystemConfig
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
this.mediaKeys = await keySystemAccess.createMediaKeys();
|
|
209
|
+
await this.videoElement.setMediaKeys(this.mediaKeys);
|
|
210
|
+
|
|
211
|
+
// Set up license acquisition
|
|
212
|
+
this.setupLicenseAcquisition(config);
|
|
213
|
+
|
|
214
|
+
// Update status
|
|
215
|
+
if (config.keySystem === this.KEY_SYSTEMS.widevine) {
|
|
216
|
+
this.status.drmSystem = 'widevine';
|
|
217
|
+
this.status.securityLevel = config.videoRobustness === 'HW_SECURE_ALL' ? 'L1' : 'L3';
|
|
218
|
+
} else if (config.keySystem === this.KEY_SYSTEMS.playready) {
|
|
219
|
+
this.status.drmSystem = 'playready';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Setup license acquisition from license server
|
|
225
|
+
*/
|
|
226
|
+
private setupLicenseAcquisition(config: DRMKeySystemConfig): void {
|
|
227
|
+
this.videoElement.addEventListener('encrypted', async (event: any) => {
|
|
228
|
+
console.log('[DRM] Encrypted event received, requesting license...');
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
if (!this.mediaKeys) {
|
|
232
|
+
throw new Error('MediaKeys not initialized');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const session = this.mediaKeys.createSession();
|
|
236
|
+
|
|
237
|
+
// Listen for license messages
|
|
238
|
+
session.addEventListener('message', async (messageEvent: any) => {
|
|
239
|
+
console.log('[DRM] License request message received');
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(config.licenseServerUrl, {
|
|
243
|
+
method: 'POST',
|
|
244
|
+
headers: {
|
|
245
|
+
'Content-Type': 'application/octet-stream',
|
|
246
|
+
...config.headers,
|
|
247
|
+
},
|
|
248
|
+
body: messageEvent.message,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!response.ok) {
|
|
252
|
+
throw this.createError(
|
|
253
|
+
DRMErrorCode.LICENSE_REQUEST_FAILED,
|
|
254
|
+
`License server returned ${response.status}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const license = await response.arrayBuffer();
|
|
259
|
+
await session.update(license);
|
|
260
|
+
|
|
261
|
+
console.log('[DRM] License acquired successfully');
|
|
262
|
+
this.config.onLicenseAcquired?.();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('[DRM] License acquisition failed:', error);
|
|
265
|
+
this.config.onDRMError?.(error as DRMError);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Generate license request
|
|
270
|
+
await session.generateRequest(event.initDataType, event.initData);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('[DRM] Failed to generate license request:', error);
|
|
273
|
+
this.config.onDRMError?.(error as DRMError);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Start screen recording detection
|
|
280
|
+
* Uses multiple detection methods
|
|
281
|
+
*/
|
|
282
|
+
private startScreenRecordingDetection(): void {
|
|
283
|
+
console.log('[DRM] Starting screen recording detection...');
|
|
284
|
+
|
|
285
|
+
// Method 1: getDisplayMedia API detection
|
|
286
|
+
this.detectGetDisplayMedia();
|
|
287
|
+
|
|
288
|
+
// Method 2: Canvas fingerprinting detection
|
|
289
|
+
this.detectCanvasCapture();
|
|
290
|
+
|
|
291
|
+
// Method 3: Tab capture detection (Chrome)
|
|
292
|
+
this.detectTabCapture();
|
|
293
|
+
|
|
294
|
+
// Method 4: Periodic frame analysis
|
|
295
|
+
this.screenRecordingCheckInterval = window.setInterval(() => {
|
|
296
|
+
this.analyzeScreenRecordingSignals();
|
|
297
|
+
}, 1000);
|
|
298
|
+
|
|
299
|
+
this.status.isScreenRecordingBlocked = true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Detect getDisplayMedia screen capture
|
|
304
|
+
*/
|
|
305
|
+
private detectGetDisplayMedia(): void {
|
|
306
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Override getDisplayMedia to detect screen capture attempts
|
|
311
|
+
const originalGetDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(
|
|
312
|
+
navigator.mediaDevices
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
navigator.mediaDevices.getDisplayMedia = async function (constraints?: any) {
|
|
316
|
+
console.warn('[DRM] Screen capture attempt detected via getDisplayMedia');
|
|
317
|
+
// Block the request or notify
|
|
318
|
+
throw new DOMException('Screen capture blocked by DRM protection', 'NotAllowedError');
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Detect canvas-based capture
|
|
324
|
+
*/
|
|
325
|
+
private detectCanvasCapture(): void {
|
|
326
|
+
// Monitor canvas operations on the video
|
|
327
|
+
const observer = new MutationObserver((mutations) => {
|
|
328
|
+
mutations.forEach((mutation) => {
|
|
329
|
+
if (mutation.type === 'childList') {
|
|
330
|
+
mutation.addedNodes.forEach((node) => {
|
|
331
|
+
if (node.nodeName === 'CANVAS') {
|
|
332
|
+
console.warn('[DRM] Canvas element detected - potential screen capture');
|
|
333
|
+
this.handleScreenRecordingDetected();
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
observer.observe(document.body, {
|
|
341
|
+
childList: true,
|
|
342
|
+
subtree: true,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Detect tab capture (Chrome-specific)
|
|
348
|
+
*/
|
|
349
|
+
private detectTabCapture(): void {
|
|
350
|
+
// Chrome exposes tab capture status
|
|
351
|
+
if ('mediaSession' in navigator) {
|
|
352
|
+
document.addEventListener('visibilitychange', () => {
|
|
353
|
+
if (document.hidden) {
|
|
354
|
+
console.log('[DRM] Tab hidden - checking for capture...');
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Analyze various signals for screen recording
|
|
362
|
+
*/
|
|
363
|
+
private analyzeScreenRecordingSignals(): void {
|
|
364
|
+
// Check if video is playing in background tab
|
|
365
|
+
if (document.hidden && !this.videoElement.paused) {
|
|
366
|
+
console.warn('[DRM] Video playing in hidden tab - possible screen recording');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Check for unusual video element states
|
|
370
|
+
if ((this.videoElement as any).captureStream) {
|
|
371
|
+
console.warn('[DRM] captureStream API available - monitoring...');
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Handle screen recording detection
|
|
377
|
+
*/
|
|
378
|
+
private handleScreenRecordingDetected(): void {
|
|
379
|
+
console.error('[DRM] ⚠️ SCREEN RECORDING DETECTED!');
|
|
380
|
+
this.status.screenRecordingDetected = true;
|
|
381
|
+
|
|
382
|
+
// Black out video
|
|
383
|
+
this.videoElement.style.filter = 'brightness(0)';
|
|
384
|
+
this.videoElement.pause();
|
|
385
|
+
|
|
386
|
+
// Notify callback
|
|
387
|
+
this.config.onScreenRecordingDetected?.();
|
|
388
|
+
|
|
389
|
+
// Show warning overlay
|
|
390
|
+
this.showProtectionWarning('Screen recording detected. Playback blocked.');
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Block screenshots
|
|
395
|
+
*/
|
|
396
|
+
private blockScreenshots(): void {
|
|
397
|
+
// CSS-based screenshot prevention
|
|
398
|
+
this.videoElement.style.webkitUserSelect = 'none';
|
|
399
|
+
this.videoElement.style.userSelect = 'none';
|
|
400
|
+
this.videoElement.setAttribute('oncontextmenu', 'return false');
|
|
401
|
+
|
|
402
|
+
// Prevent drag-and-drop
|
|
403
|
+
this.videoElement.addEventListener('dragstart', (e) => e.preventDefault());
|
|
404
|
+
|
|
405
|
+
// Detect PrintScreen key
|
|
406
|
+
document.addEventListener('keyup', (e) => {
|
|
407
|
+
if (e.key === 'PrintScreen') {
|
|
408
|
+
console.warn('[DRM] Screenshot attempt detected (PrintScreen key)');
|
|
409
|
+
this.config.onScreenshotAttempted?.();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
this.status.isScreenshotBlocked = true;
|
|
414
|
+
console.log('[DRM] Screenshot blocking enabled');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Prevent tab capture and mirroring
|
|
419
|
+
*/
|
|
420
|
+
private preventTabCapture(): void {
|
|
421
|
+
// Detect mirroring via multiple screens
|
|
422
|
+
if (window.screen && (window.screen as any).isExtended) {
|
|
423
|
+
console.warn('[DRM] Extended display detected');
|
|
424
|
+
this.status.mirroringDetected = true;
|
|
425
|
+
this.config.onMirroringDetected?.();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Monitor presentation API
|
|
429
|
+
if ('presentation' in navigator) {
|
|
430
|
+
console.log('[DRM] Monitoring presentation API for mirroring...');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Initialize Chromecast support
|
|
436
|
+
*/
|
|
437
|
+
private async initializeCastSupport(): Promise<void> {
|
|
438
|
+
if (!(window as any).chrome || !(window as any).chrome.cast) {
|
|
439
|
+
console.warn('[DRM] Google Cast API not available');
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
console.log('[DRM] Initializing Chromecast support...');
|
|
444
|
+
|
|
445
|
+
// Load Cast SDK
|
|
446
|
+
const script = document.createElement('script');
|
|
447
|
+
script.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
|
|
448
|
+
document.head.appendChild(script);
|
|
449
|
+
|
|
450
|
+
await new Promise((resolve) => {
|
|
451
|
+
script.onload = resolve;
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Initialize Cast API
|
|
455
|
+
(window as any).__onGCastApiAvailable = (isAvailable: boolean) => {
|
|
456
|
+
if (isAvailable) {
|
|
457
|
+
this.castContext = (window as any).cast.framework.CastContext.getInstance();
|
|
458
|
+
this.castContext.setOptions({
|
|
459
|
+
receiverApplicationId: (window as any).chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
|
|
460
|
+
autoJoinPolicy: (window as any).chrome.cast.AutoJoinPolicy.ORIGIN_SCOPED,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
console.log('[DRM] Chromecast initialized');
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Get current protection status
|
|
470
|
+
*/
|
|
471
|
+
getStatus(): IDRMProtectionStatus {
|
|
472
|
+
return { ...this.status };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Enable/disable DRM protection
|
|
477
|
+
*/
|
|
478
|
+
setEnabled(enabled: boolean): void {
|
|
479
|
+
this.config.enabled = enabled;
|
|
480
|
+
if (!enabled) {
|
|
481
|
+
this.dispose();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Enable/disable specific feature
|
|
487
|
+
*/
|
|
488
|
+
setFeature(feature: keyof IDRMProtectionConfig, enabled: boolean): void {
|
|
489
|
+
(this.config as any)[feature] = enabled;
|
|
490
|
+
|
|
491
|
+
// Re-apply feature if changing during playback
|
|
492
|
+
switch (feature) {
|
|
493
|
+
case 'blockScreenRecording':
|
|
494
|
+
if (enabled) {
|
|
495
|
+
this.startScreenRecordingDetection();
|
|
496
|
+
} else if (this.screenRecordingCheckInterval) {
|
|
497
|
+
clearInterval(this.screenRecordingCheckInterval);
|
|
498
|
+
}
|
|
499
|
+
break;
|
|
500
|
+
case 'blockScreenshots':
|
|
501
|
+
this.status.isScreenshotBlocked = enabled;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Start casting
|
|
508
|
+
*/
|
|
509
|
+
async startCasting(deviceId: string): Promise<void> {
|
|
510
|
+
if (!this.castContext) {
|
|
511
|
+
throw new Error('Cast not initialized');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Cast implementation
|
|
515
|
+
console.log('[DRM] Starting cast to device:', deviceId);
|
|
516
|
+
this.status.isCasting = true;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Stop casting
|
|
521
|
+
*/
|
|
522
|
+
async stopCasting(): Promise<void> {
|
|
523
|
+
if (this.castSession) {
|
|
524
|
+
this.castSession.endSession(true);
|
|
525
|
+
this.castSession = null;
|
|
526
|
+
}
|
|
527
|
+
this.status.isCasting = false;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Get available cast devices
|
|
532
|
+
*/
|
|
533
|
+
async getAvailableCastDevices(): Promise<CastDevice[]> {
|
|
534
|
+
// Return available Chromecast devices
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Renew DRM license
|
|
540
|
+
*/
|
|
541
|
+
async renewLicense(): Promise<void> {
|
|
542
|
+
console.log('[DRM] Renewing license...');
|
|
543
|
+
// Re-trigger license acquisition
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Show protection warning overlay
|
|
548
|
+
*/
|
|
549
|
+
private showProtectionWarning(message: string): void {
|
|
550
|
+
const overlay = document.createElement('div');
|
|
551
|
+
overlay.style.cssText = `
|
|
552
|
+
position: absolute;
|
|
553
|
+
top: 0;
|
|
554
|
+
left: 0;
|
|
555
|
+
width: 100%;
|
|
556
|
+
height: 100%;
|
|
557
|
+
background: #000;
|
|
558
|
+
color: #fff;
|
|
559
|
+
display: flex;
|
|
560
|
+
align-items: center;
|
|
561
|
+
justify-content: center;
|
|
562
|
+
font-size: 18px;
|
|
563
|
+
z-index: 9999999;
|
|
564
|
+
`;
|
|
565
|
+
overlay.textContent = message;
|
|
566
|
+
this.videoElement.parentElement?.appendChild(overlay);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Create DRM error
|
|
571
|
+
*/
|
|
572
|
+
private createError(code: DRMErrorCode, message: string): DRMError {
|
|
573
|
+
return {
|
|
574
|
+
code,
|
|
575
|
+
message,
|
|
576
|
+
platform: 'web',
|
|
577
|
+
recoverable: false,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Cleanup resources
|
|
583
|
+
*/
|
|
584
|
+
dispose(): void {
|
|
585
|
+
if (this.screenRecordingCheckInterval) {
|
|
586
|
+
clearInterval(this.screenRecordingCheckInterval);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (this.castSession) {
|
|
590
|
+
this.stopCasting();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
this.status.isProtected = false;
|
|
594
|
+
console.log('[DRM] Protection disposed');
|
|
595
|
+
}
|
|
596
|
+
}
|
|
@@ -11,6 +11,9 @@ export { WebPlayer } from './WebPlayer';
|
|
|
11
11
|
export { WebPlayerView } from './react/WebPlayerView';
|
|
12
12
|
export { SecureVideoPlayer } from './SecureVideoPlayer';
|
|
13
13
|
|
|
14
|
+
// Export DRM Protection
|
|
15
|
+
export { WebDRMProtection } from './drm/WebDRMProtection';
|
|
16
|
+
|
|
14
17
|
// Export EPG (Electronic Program Guide) components
|
|
15
18
|
export * from './react/EPG';
|
|
16
19
|
|
|
@@ -534,45 +534,6 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
534
534
|
const adContainerRef = useRef<HTMLDivElement | null>(null);
|
|
535
535
|
const [isAdPlaying, setIsAdPlaying] = useState(false);
|
|
536
536
|
|
|
537
|
-
// Create ad container element programmatically
|
|
538
|
-
useEffect(() => {
|
|
539
|
-
if (!props.googleAds || adContainerRef.current) return;
|
|
540
|
-
|
|
541
|
-
const adContainer = document.createElement('div');
|
|
542
|
-
adContainer.className = 'uvf-ad-container';
|
|
543
|
-
adContainer.style.cssText = `
|
|
544
|
-
position: absolute;
|
|
545
|
-
top: 0;
|
|
546
|
-
left: 0;
|
|
547
|
-
right: 0;
|
|
548
|
-
bottom: 0;
|
|
549
|
-
z-index: 999999999;
|
|
550
|
-
pointer-events: none;
|
|
551
|
-
visibility: hidden;
|
|
552
|
-
opacity: 0;
|
|
553
|
-
transition: opacity 0.2s ease, visibility 0.2s ease;
|
|
554
|
-
`;
|
|
555
|
-
|
|
556
|
-
adContainerRef.current = adContainer;
|
|
557
|
-
console.log('✅ Ad container element created (will be injected into player wrapper)');
|
|
558
|
-
|
|
559
|
-
return () => {
|
|
560
|
-
// Cleanup: remove ad container on unmount
|
|
561
|
-
if (adContainer.parentElement) {
|
|
562
|
-
adContainer.parentElement.removeChild(adContainer);
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
}, [props.googleAds]);
|
|
566
|
-
|
|
567
|
-
// Update ad container visibility when isAdPlaying changes
|
|
568
|
-
useEffect(() => {
|
|
569
|
-
if (!adContainerRef.current) return;
|
|
570
|
-
|
|
571
|
-
adContainerRef.current.style.pointerEvents = isAdPlaying ? 'auto' : 'none';
|
|
572
|
-
adContainerRef.current.style.visibility = isAdPlaying ? 'visible' : 'hidden';
|
|
573
|
-
adContainerRef.current.style.opacity = isAdPlaying ? '1' : '0';
|
|
574
|
-
}, [isAdPlaying]);
|
|
575
|
-
|
|
576
537
|
/**
|
|
577
538
|
* Generate ad chapter segments from googleAds cue points
|
|
578
539
|
* Handles pre-roll (0), mid-rolls, and post-roll (-1)
|
|
@@ -1287,21 +1248,9 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1287
1248
|
return;
|
|
1288
1249
|
}
|
|
1289
1250
|
|
|
1290
|
-
// First, inject ad container into player wrapper before initializing Google Ads
|
|
1291
|
-
const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
|
|
1292
|
-
if (playerWrapper) {
|
|
1293
|
-
// Add ad container to player wrapper
|
|
1294
|
-
playerWrapper.appendChild(adContainer);
|
|
1295
|
-
console.log('✅ Ad container injected into player wrapper before IMA SDK initialization');
|
|
1296
|
-
} else {
|
|
1297
|
-
console.error('❌ Player wrapper not found - cannot inject ad container');
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1251
|
console.log('✅ Initializing Google Ads...', {
|
|
1302
1252
|
adContainer: adContainer.className,
|
|
1303
1253
|
videoElement: videoElement.tagName,
|
|
1304
|
-
adContainerParent: adContainer.parentElement?.className,
|
|
1305
1254
|
adContainerInDOM: document.body.contains(adContainer)
|
|
1306
1255
|
});
|
|
1307
1256
|
|
|
@@ -1359,6 +1308,14 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1359
1308
|
|
|
1360
1309
|
console.log('✅ Google Ads initialized successfully');
|
|
1361
1310
|
|
|
1311
|
+
// Move ad container into player wrapper for fullscreen support
|
|
1312
|
+
const playerWrapper = containerRef.current?.querySelector('.uvf-player-wrapper');
|
|
1313
|
+
if (playerWrapper && adContainerRef.current && adContainerRef.current.parentElement) {
|
|
1314
|
+
// Move ad container from player-container to player-wrapper
|
|
1315
|
+
playerWrapper.appendChild(adContainerRef.current);
|
|
1316
|
+
console.log('✅ Ad container moved into player wrapper for fullscreen support');
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1362
1319
|
// Initialize ad display container on first user interaction
|
|
1363
1320
|
// Chrome requires this to be called on a user gesture
|
|
1364
1321
|
let adContainerInitialized = false;
|
|
@@ -1731,7 +1688,25 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1731
1688
|
style={responsiveStyle}
|
|
1732
1689
|
/>
|
|
1733
1690
|
|
|
1734
|
-
{/*
|
|
1691
|
+
{/* Google Ads Container - positioned over the video player */}
|
|
1692
|
+
{props.googleAds && (
|
|
1693
|
+
<div
|
|
1694
|
+
ref={adContainerRef}
|
|
1695
|
+
className="uvf-ad-container"
|
|
1696
|
+
style={{
|
|
1697
|
+
position: 'absolute',
|
|
1698
|
+
top: 0,
|
|
1699
|
+
left: 0,
|
|
1700
|
+
right: 0,
|
|
1701
|
+
bottom: 0,
|
|
1702
|
+
zIndex: 999999999,
|
|
1703
|
+
pointerEvents: isAdPlaying ? 'auto' : 'none',
|
|
1704
|
+
visibility: isAdPlaying ? 'visible' : 'hidden',
|
|
1705
|
+
opacity: isAdPlaying ? 1 : 0,
|
|
1706
|
+
transition: 'opacity 0.2s ease, visibility 0.2s ease',
|
|
1707
|
+
}}
|
|
1708
|
+
/>
|
|
1709
|
+
)}
|
|
1735
1710
|
|
|
1736
1711
|
|
|
1737
1712
|
{/* EPG Overlay - Full-screen glassmorphic overlay */}
|