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.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/packages/core/dist/interfaces.d.ts +20 -1
  3. package/packages/core/dist/interfaces.d.ts.map +1 -1
  4. package/packages/core/src/interfaces.ts +41 -2
  5. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  6. package/packages/web/dist/WebPlayer.js +23 -1
  7. package/packages/web/dist/WebPlayer.js.map +1 -1
  8. package/packages/web/dist/drm/DRMHelper.d.ts +28 -0
  9. package/packages/web/dist/drm/DRMHelper.d.ts.map +1 -0
  10. package/packages/web/dist/drm/DRMHelper.js +117 -0
  11. package/packages/web/dist/drm/DRMHelper.js.map +1 -0
  12. package/packages/web/dist/react/WebPlayerView.d.ts +19 -1
  13. package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
  14. package/packages/web/dist/react/WebPlayerView.js.map +1 -1
  15. package/packages/web/dist/security/CanvasVideoRenderer.d.ts +26 -0
  16. package/packages/web/dist/security/CanvasVideoRenderer.d.ts.map +1 -0
  17. package/packages/web/dist/security/CanvasVideoRenderer.js +143 -0
  18. package/packages/web/dist/security/CanvasVideoRenderer.js.map +1 -0
  19. package/packages/web/dist/security/ScreenProtectionController.d.ts +9 -1
  20. package/packages/web/dist/security/ScreenProtectionController.d.ts.map +1 -1
  21. package/packages/web/dist/security/ScreenProtectionController.js +86 -1
  22. package/packages/web/dist/security/ScreenProtectionController.js.map +1 -1
  23. package/packages/web/src/WebPlayer.ts +34 -1
  24. package/packages/web/src/drm/DRMHelper.ts +203 -0
  25. package/packages/web/src/react/WebPlayerView.tsx +23 -4
  26. package/packages/web/src/security/CanvasVideoRenderer.ts +246 -0
  27. 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
- // Create interference overlay
33
- this.createInterferenceOverlay();
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
  }