unified-video-framework 1.4.383 → 1.4.384
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.d.ts +20 -1
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +41 -2
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +23 -1
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/drm/DRMHelper.d.ts +28 -0
- package/packages/web/dist/drm/DRMHelper.d.ts.map +1 -0
- package/packages/web/dist/drm/DRMHelper.js +117 -0
- package/packages/web/dist/drm/DRMHelper.js.map +1 -0
- package/packages/web/dist/react/WebPlayerView.d.ts +19 -1
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/security/CanvasVideoRenderer.d.ts +26 -0
- package/packages/web/dist/security/CanvasVideoRenderer.d.ts.map +1 -0
- package/packages/web/dist/security/CanvasVideoRenderer.js +143 -0
- package/packages/web/dist/security/CanvasVideoRenderer.js.map +1 -0
- package/packages/web/dist/security/ScreenProtectionController.d.ts +9 -1
- package/packages/web/dist/security/ScreenProtectionController.d.ts.map +1 -1
- package/packages/web/dist/security/ScreenProtectionController.js +86 -1
- package/packages/web/dist/security/ScreenProtectionController.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +34 -1
- package/packages/web/src/drm/DRMHelper.ts +203 -0
- package/packages/web/src/react/WebPlayerView.tsx +23 -4
- package/packages/web/src/security/CanvasVideoRenderer.ts +246 -0
- package/packages/web/src/security/ScreenProtectionController.ts +118 -3
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Video Renderer
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT LIMITATION:
|
|
5
|
+
* This canvas-based approach does NOT prevent screenshots/recordings from showing content.
|
|
6
|
+
* - Screenshots capture the canvas output (not black screen)
|
|
7
|
+
* - Screen recorders capture the rendered canvas frames
|
|
8
|
+
* - Only DRM provides true black screen protection
|
|
9
|
+
*
|
|
10
|
+
* What this DOES provide:
|
|
11
|
+
* - Obfuscation layer (makes automated piracy tools slightly harder)
|
|
12
|
+
* - Dynamic noise/watermarking that's harder to remove
|
|
13
|
+
* - Video element is hidden (minor deterrent only)
|
|
14
|
+
*
|
|
15
|
+
* Use this only as an ADDITIONAL layer on top of other protections, not as primary protection.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface CanvasRendererOptions {
|
|
19
|
+
sourceVideo: HTMLVideoElement;
|
|
20
|
+
containerElement: HTMLElement;
|
|
21
|
+
watermarkText?: string;
|
|
22
|
+
enableNoise?: boolean;
|
|
23
|
+
enableDynamicTransforms?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class CanvasVideoRenderer {
|
|
27
|
+
private opts: CanvasRendererOptions;
|
|
28
|
+
private canvas: HTMLCanvasElement;
|
|
29
|
+
private ctx: CanvasRenderingContext2D;
|
|
30
|
+
private animationFrameId: number | null = null;
|
|
31
|
+
private isActive: boolean = false;
|
|
32
|
+
private noiseOffset: number = 0;
|
|
33
|
+
|
|
34
|
+
constructor(opts: CanvasRendererOptions) {
|
|
35
|
+
this.opts = opts;
|
|
36
|
+
|
|
37
|
+
// Create canvas element
|
|
38
|
+
this.canvas = document.createElement('canvas');
|
|
39
|
+
this.canvas.className = 'uvf-canvas-video-renderer';
|
|
40
|
+
this.canvas.style.cssText = `
|
|
41
|
+
position: absolute;
|
|
42
|
+
top: 0;
|
|
43
|
+
left: 0;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
object-fit: contain;
|
|
47
|
+
z-index: 2;
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const context = this.canvas.getContext('2d', {
|
|
51
|
+
alpha: false,
|
|
52
|
+
desynchronized: true, // Hint for better performance
|
|
53
|
+
willReadFrequently: false
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!context) {
|
|
57
|
+
throw new Error('[CanvasRenderer] Failed to get 2D context');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.ctx = context;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public activate(): void {
|
|
64
|
+
if (this.isActive) return;
|
|
65
|
+
|
|
66
|
+
console.log('[CanvasRenderer] Activating canvas-based video rendering...');
|
|
67
|
+
console.warn('[CanvasRenderer] WARNING: Canvas rendering does NOT prevent screenshots showing content.');
|
|
68
|
+
console.warn('[CanvasRenderer] This only adds obfuscation. Use DRM for true protection.');
|
|
69
|
+
|
|
70
|
+
// Hide the original video element
|
|
71
|
+
this.opts.sourceVideo.style.opacity = '0';
|
|
72
|
+
this.opts.sourceVideo.style.pointerEvents = 'none';
|
|
73
|
+
|
|
74
|
+
// Add canvas to container
|
|
75
|
+
this.opts.containerElement.appendChild(this.canvas);
|
|
76
|
+
|
|
77
|
+
// Set canvas dimensions to match video
|
|
78
|
+
this.updateCanvasSize();
|
|
79
|
+
|
|
80
|
+
// Start rendering loop
|
|
81
|
+
this.isActive = true;
|
|
82
|
+
this.renderFrame();
|
|
83
|
+
|
|
84
|
+
// Update canvas size on video resize
|
|
85
|
+
this.opts.sourceVideo.addEventListener('loadedmetadata', () => {
|
|
86
|
+
this.updateCanvasSize();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
console.log('[CanvasRenderer] Canvas rendering active');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public deactivate(): void {
|
|
93
|
+
if (!this.isActive) return;
|
|
94
|
+
|
|
95
|
+
console.log('[CanvasRenderer] Deactivating canvas rendering...');
|
|
96
|
+
|
|
97
|
+
this.isActive = false;
|
|
98
|
+
|
|
99
|
+
// Stop animation loop
|
|
100
|
+
if (this.animationFrameId !== null) {
|
|
101
|
+
cancelAnimationFrame(this.animationFrameId);
|
|
102
|
+
this.animationFrameId = null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Show original video
|
|
106
|
+
this.opts.sourceVideo.style.opacity = '1';
|
|
107
|
+
this.opts.sourceVideo.style.pointerEvents = 'auto';
|
|
108
|
+
|
|
109
|
+
// Remove canvas
|
|
110
|
+
if (this.canvas.parentElement) {
|
|
111
|
+
this.canvas.parentElement.removeChild(this.canvas);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public destroy(): void {
|
|
116
|
+
this.deactivate();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private updateCanvasSize(): void {
|
|
120
|
+
const video = this.opts.sourceVideo;
|
|
121
|
+
|
|
122
|
+
// Set internal canvas resolution to match video
|
|
123
|
+
this.canvas.width = video.videoWidth || 1920;
|
|
124
|
+
this.canvas.height = video.videoHeight || 1080;
|
|
125
|
+
|
|
126
|
+
console.log(`[CanvasRenderer] Canvas size: ${this.canvas.width}x${this.canvas.height}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private renderFrame = (): void => {
|
|
130
|
+
if (!this.isActive) return;
|
|
131
|
+
|
|
132
|
+
const video = this.opts.sourceVideo;
|
|
133
|
+
|
|
134
|
+
// Only render if video has content and is playing
|
|
135
|
+
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
|
136
|
+
// Clear canvas
|
|
137
|
+
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
138
|
+
|
|
139
|
+
// Apply dynamic transforms (makes automated extraction slightly harder)
|
|
140
|
+
if (this.opts.enableDynamicTransforms) {
|
|
141
|
+
this.ctx.save();
|
|
142
|
+
|
|
143
|
+
// Subtle random positioning (imperceptible to humans, confuses some bots)
|
|
144
|
+
const jitterX = Math.sin(Date.now() / 1000) * 0.5;
|
|
145
|
+
const jitterY = Math.cos(Date.now() / 1000) * 0.5;
|
|
146
|
+
this.ctx.translate(jitterX, jitterY);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Draw video frame to canvas
|
|
150
|
+
this.ctx.drawImage(video, 0, 0, this.canvas.width, this.canvas.height);
|
|
151
|
+
|
|
152
|
+
// Apply noise layer (makes pixel-perfect extraction harder)
|
|
153
|
+
if (this.opts.enableNoise) {
|
|
154
|
+
this.applyNoiseLayer();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Draw watermark overlay
|
|
158
|
+
if (this.opts.watermarkText) {
|
|
159
|
+
this.drawDynamicWatermark();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.opts.enableDynamicTransforms) {
|
|
163
|
+
this.ctx.restore();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Continue rendering loop
|
|
168
|
+
this.animationFrameId = requestAnimationFrame(this.renderFrame);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
private applyNoiseLayer(): void {
|
|
172
|
+
// Create subtle noise that changes over time
|
|
173
|
+
const imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
174
|
+
const data = imageData.data;
|
|
175
|
+
|
|
176
|
+
// Very subtle noise (imperceptible to humans, confuses some automated tools)
|
|
177
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
178
|
+
const noise = (Math.random() - 0.5) * 2; // -1 to 1
|
|
179
|
+
data[i] += noise; // R
|
|
180
|
+
data[i + 1] += noise; // G
|
|
181
|
+
data[i + 2] += noise; // B
|
|
182
|
+
// Alpha (i+3) unchanged
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.ctx.putImageData(imageData, 0, 0);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private drawDynamicWatermark(): void {
|
|
189
|
+
const text = this.opts.watermarkText!;
|
|
190
|
+
const fontSize = Math.floor(this.canvas.height / 30);
|
|
191
|
+
|
|
192
|
+
this.ctx.save();
|
|
193
|
+
|
|
194
|
+
// Configure text style
|
|
195
|
+
this.ctx.font = `${fontSize}px Arial, sans-serif`;
|
|
196
|
+
this.ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
197
|
+
this.ctx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
|
|
198
|
+
this.ctx.lineWidth = 1;
|
|
199
|
+
this.ctx.textAlign = 'center';
|
|
200
|
+
this.ctx.textBaseline = 'middle';
|
|
201
|
+
|
|
202
|
+
// Rotating watermark (harder to crop out)
|
|
203
|
+
const time = Date.now() / 1000;
|
|
204
|
+
const positions = [
|
|
205
|
+
{ x: this.canvas.width * 0.25, y: this.canvas.height * 0.25 },
|
|
206
|
+
{ x: this.canvas.width * 0.75, y: this.canvas.height * 0.25 },
|
|
207
|
+
{ x: this.canvas.width * 0.25, y: this.canvas.height * 0.75 },
|
|
208
|
+
{ x: this.canvas.width * 0.75, y: this.canvas.height * 0.75 },
|
|
209
|
+
{ x: this.canvas.width * 0.5, y: this.canvas.height * 0.5 }
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
positions.forEach((pos, index) => {
|
|
213
|
+
this.ctx.save();
|
|
214
|
+
this.ctx.translate(pos.x, pos.y);
|
|
215
|
+
|
|
216
|
+
// Rotate each watermark slightly
|
|
217
|
+
const rotation = (Math.sin(time + index) * 15 * Math.PI) / 180;
|
|
218
|
+
this.ctx.rotate(rotation);
|
|
219
|
+
|
|
220
|
+
// Pulsing opacity
|
|
221
|
+
const opacity = 0.2 + Math.sin(time * 2 + index) * 0.1;
|
|
222
|
+
this.ctx.globalAlpha = opacity;
|
|
223
|
+
|
|
224
|
+
this.ctx.strokeText(text, 0, 0);
|
|
225
|
+
this.ctx.fillText(text, 0, 0);
|
|
226
|
+
|
|
227
|
+
this.ctx.restore();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
this.ctx.restore();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the canvas element (for external manipulation if needed)
|
|
235
|
+
*/
|
|
236
|
+
public getCanvas(): HTMLCanvasElement {
|
|
237
|
+
return this.canvas;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if rendering is active
|
|
242
|
+
*/
|
|
243
|
+
public isRendering(): boolean {
|
|
244
|
+
return this.isActive;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
-
import { ScreenCaptureEvent } from '@unified-video/core';
|
|
1
|
+
import { ScreenCaptureEvent, EnhancedScreenProtectionConfig } from '@unified-video/core';
|
|
2
|
+
import { CanvasVideoRenderer } from './CanvasVideoRenderer';
|
|
2
3
|
|
|
3
4
|
export interface ScreenProtectionOptions {
|
|
4
5
|
videoElement: HTMLVideoElement;
|
|
5
6
|
containerElement: HTMLElement;
|
|
7
|
+
config: EnhancedScreenProtectionConfig;
|
|
6
8
|
watermarkConfig?: any;
|
|
7
9
|
onDetection?: (event: ScreenCaptureEvent) => void;
|
|
8
10
|
onDevToolsDetected?: () => void;
|
|
11
|
+
onPause?: () => void; // Callback to pause video
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export class ScreenProtectionController {
|
|
12
15
|
private opts: ScreenProtectionOptions;
|
|
13
16
|
private overlayElement: HTMLElement | null = null;
|
|
17
|
+
private canvasRenderer: CanvasVideoRenderer | null = null;
|
|
14
18
|
private detectionIntervals: NodeJS.Timeout[] = [];
|
|
15
19
|
private eventListeners: Array<{ target: any; event: string; handler: any }> = [];
|
|
16
20
|
private isActive: boolean = false;
|
|
@@ -29,8 +33,13 @@ export class ScreenProtectionController {
|
|
|
29
33
|
// Apply video element protection
|
|
30
34
|
this.applyVideoProtection();
|
|
31
35
|
|
|
32
|
-
//
|
|
33
|
-
this.
|
|
36
|
+
// Initialize canvas rendering if enabled
|
|
37
|
+
this.initializeCanvasRendering();
|
|
38
|
+
|
|
39
|
+
// Create interference overlay (only if not using canvas)
|
|
40
|
+
if (!this.canvasRenderer) {
|
|
41
|
+
this.createInterferenceOverlay();
|
|
42
|
+
}
|
|
34
43
|
|
|
35
44
|
// Setup behavioral detection
|
|
36
45
|
this.setupDetection();
|
|
@@ -43,6 +52,12 @@ export class ScreenProtectionController {
|
|
|
43
52
|
|
|
44
53
|
console.log('[ScreenProtectionController] Deactivating screen protection...');
|
|
45
54
|
|
|
55
|
+
// Deactivate canvas renderer
|
|
56
|
+
if (this.canvasRenderer) {
|
|
57
|
+
this.canvasRenderer.deactivate();
|
|
58
|
+
this.canvasRenderer = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
// Remove overlay
|
|
47
62
|
if (this.overlayElement && this.overlayElement.parentElement) {
|
|
48
63
|
this.overlayElement.parentElement.removeChild(this.overlayElement);
|
|
@@ -72,6 +87,34 @@ export class ScreenProtectionController {
|
|
|
72
87
|
this.deactivate();
|
|
73
88
|
}
|
|
74
89
|
|
|
90
|
+
private initializeCanvasRendering(): void {
|
|
91
|
+
// Check if canvas rendering is enabled
|
|
92
|
+
if (!this.opts.config.canvasRendering?.enabled) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('[ScreenProtectionController] Initializing canvas-based video rendering...');
|
|
97
|
+
console.warn('[ScreenProtectionController] WARNING: Canvas rendering does NOT prevent screenshots.');
|
|
98
|
+
console.warn('[ScreenProtectionController] This only adds obfuscation. Use DRM for true protection.');
|
|
99
|
+
|
|
100
|
+
// Get watermark text
|
|
101
|
+
const watermarkText = this.opts.config.forensicWatermark?.userId
|
|
102
|
+
? `PROTECTED - ${this.opts.config.forensicWatermark.userId}`
|
|
103
|
+
: 'PROTECTED CONTENT';
|
|
104
|
+
|
|
105
|
+
// Create canvas renderer
|
|
106
|
+
this.canvasRenderer = new CanvasVideoRenderer({
|
|
107
|
+
sourceVideo: this.opts.videoElement,
|
|
108
|
+
containerElement: this.opts.containerElement,
|
|
109
|
+
watermarkText: watermarkText,
|
|
110
|
+
enableNoise: this.opts.config.canvasRendering.enableNoise ?? true,
|
|
111
|
+
enableDynamicTransforms: this.opts.config.canvasRendering.enableDynamicTransforms ?? true
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Activate canvas rendering
|
|
115
|
+
this.canvasRenderer.activate();
|
|
116
|
+
}
|
|
117
|
+
|
|
75
118
|
private applyVideoProtection(): void {
|
|
76
119
|
const { videoElement, containerElement } = this.opts;
|
|
77
120
|
|
|
@@ -272,5 +315,77 @@ export class ScreenProtectionController {
|
|
|
272
315
|
if (this.opts.onDetection) {
|
|
273
316
|
this.opts.onDetection(event);
|
|
274
317
|
}
|
|
318
|
+
|
|
319
|
+
// Send to backend tracking endpoint if configured
|
|
320
|
+
if (this.opts.config.trackingEndpoint) {
|
|
321
|
+
this.sendTrackingEvent(event);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Execute configured action
|
|
325
|
+
const action = this.opts.config.onDetection || 'warn';
|
|
326
|
+
|
|
327
|
+
switch (action) {
|
|
328
|
+
case 'warn':
|
|
329
|
+
this.executeWarnAction();
|
|
330
|
+
break;
|
|
331
|
+
case 'pause':
|
|
332
|
+
this.executePauseAction();
|
|
333
|
+
break;
|
|
334
|
+
case 'degrade':
|
|
335
|
+
this.executeDegradeAction();
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async sendTrackingEvent(event: ScreenCaptureEvent): Promise<void> {
|
|
341
|
+
try {
|
|
342
|
+
await fetch(this.opts.config.trackingEndpoint!, {
|
|
343
|
+
method: 'POST',
|
|
344
|
+
headers: {
|
|
345
|
+
'Content-Type': 'application/json'
|
|
346
|
+
},
|
|
347
|
+
body: JSON.stringify({
|
|
348
|
+
event,
|
|
349
|
+
userAgent: navigator.userAgent,
|
|
350
|
+
timestamp: new Date().toISOString(),
|
|
351
|
+
forensicData: this.opts.config.forensicWatermark
|
|
352
|
+
})
|
|
353
|
+
});
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('[ScreenProtection] Failed to send tracking event:', error);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private executeWarnAction(): void {
|
|
360
|
+
const message = this.opts.config.warningMessage ||
|
|
361
|
+
'Screen recording or screenshot detected. This content is protected.';
|
|
362
|
+
|
|
363
|
+
console.warn(`[ScreenProtection] ${message}`);
|
|
364
|
+
|
|
365
|
+
// Could also show visual warning in player (future enhancement)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private executePauseAction(): void {
|
|
369
|
+
console.warn('[ScreenProtection] Pausing playback due to suspicious activity');
|
|
370
|
+
|
|
371
|
+
if (this.opts.onPause) {
|
|
372
|
+
this.opts.onPause();
|
|
373
|
+
} else {
|
|
374
|
+
this.opts.videoElement.pause();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private executeDegradeAction(): void {
|
|
379
|
+
console.warn('[ScreenProtection] Degrading video quality due to suspicious activity');
|
|
380
|
+
|
|
381
|
+
// Apply visual degradation
|
|
382
|
+
this.opts.videoElement.style.filter = 'blur(10px) brightness(0.5)';
|
|
383
|
+
|
|
384
|
+
// Restore after 5 seconds
|
|
385
|
+
setTimeout(() => {
|
|
386
|
+
if (this.opts.videoElement.style.filter) {
|
|
387
|
+
this.opts.videoElement.style.filter = '';
|
|
388
|
+
}
|
|
389
|
+
}, 5000);
|
|
275
390
|
}
|
|
276
391
|
}
|