unified-video-framework 1.4.386 → 1.4.388
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 +1 -28
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +1 -53
- package/packages/web/dist/WebPlayer.d.ts +0 -2
- package/packages/web/dist/WebPlayer.d.ts.map +1 -1
- package/packages/web/dist/WebPlayer.js +2 -89
- package/packages/web/dist/WebPlayer.js.map +1 -1
- package/packages/web/dist/index.d.ts +0 -6
- package/packages/web/dist/index.d.ts.map +1 -1
- package/packages/web/dist/index.js +0 -3
- package/packages/web/dist/index.js.map +1 -1
- package/packages/web/dist/react/WebPlayerView.d.ts +0 -21
- package/packages/web/dist/react/WebPlayerView.d.ts.map +1 -1
- package/packages/web/dist/react/WebPlayerView.js +18 -0
- package/packages/web/dist/react/WebPlayerView.js.map +1 -1
- package/packages/web/dist/security/ScreenProtectionController.d.ts +4 -0
- package/packages/web/dist/security/ScreenProtectionController.d.ts.map +1 -1
- package/packages/web/dist/security/ScreenProtectionController.js +90 -0
- package/packages/web/dist/security/ScreenProtectionController.js.map +1 -1
- package/packages/web/src/WebPlayer.ts +2 -120
- package/packages/web/src/index.ts +0 -10
- package/packages/web/src/react/WebPlayerView.tsx +28 -24
- package/scripts/fix-imports.js +0 -44
- package/packages/web/src/drm/DRMHelper.ts +0 -203
- package/packages/web/src/security/CanvasVideoRenderer.ts +0 -246
- package/packages/web/src/security/ScreenProtectionController.ts +0 -391
|
@@ -1,246 +0,0 @@
|
|
|
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,391 +0,0 @@
|
|
|
1
|
-
import { ScreenCaptureEvent, EnhancedScreenProtectionConfig } from '@unified-video/core';
|
|
2
|
-
import { CanvasVideoRenderer } from './CanvasVideoRenderer';
|
|
3
|
-
|
|
4
|
-
export interface ScreenProtectionOptions {
|
|
5
|
-
videoElement: HTMLVideoElement;
|
|
6
|
-
containerElement: HTMLElement;
|
|
7
|
-
config: EnhancedScreenProtectionConfig;
|
|
8
|
-
watermarkConfig?: any;
|
|
9
|
-
onDetection?: (event: ScreenCaptureEvent) => void;
|
|
10
|
-
onDevToolsDetected?: () => void;
|
|
11
|
-
onPause?: () => void; // Callback to pause video
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class ScreenProtectionController {
|
|
15
|
-
private opts: ScreenProtectionOptions;
|
|
16
|
-
private overlayElement: HTMLElement | null = null;
|
|
17
|
-
private canvasRenderer: CanvasVideoRenderer | null = null;
|
|
18
|
-
private detectionIntervals: NodeJS.Timeout[] = [];
|
|
19
|
-
private eventListeners: Array<{ target: any; event: string; handler: any }> = [];
|
|
20
|
-
private isActive: boolean = false;
|
|
21
|
-
private devToolsOpen: boolean = false;
|
|
22
|
-
private originalGetDisplayMedia: any = null;
|
|
23
|
-
|
|
24
|
-
constructor(opts: ScreenProtectionOptions) {
|
|
25
|
-
this.opts = opts;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
public activate(): void {
|
|
29
|
-
if (this.isActive) return;
|
|
30
|
-
|
|
31
|
-
console.log('[ScreenProtectionController] Activating screen protection...');
|
|
32
|
-
|
|
33
|
-
// Apply video element protection
|
|
34
|
-
this.applyVideoProtection();
|
|
35
|
-
|
|
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
|
-
}
|
|
43
|
-
|
|
44
|
-
// Setup behavioral detection
|
|
45
|
-
this.setupDetection();
|
|
46
|
-
|
|
47
|
-
this.isActive = true;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
public deactivate(): void {
|
|
51
|
-
if (!this.isActive) return;
|
|
52
|
-
|
|
53
|
-
console.log('[ScreenProtectionController] Deactivating screen protection...');
|
|
54
|
-
|
|
55
|
-
// Deactivate canvas renderer
|
|
56
|
-
if (this.canvasRenderer) {
|
|
57
|
-
this.canvasRenderer.deactivate();
|
|
58
|
-
this.canvasRenderer = null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Remove overlay
|
|
62
|
-
if (this.overlayElement && this.overlayElement.parentElement) {
|
|
63
|
-
this.overlayElement.parentElement.removeChild(this.overlayElement);
|
|
64
|
-
this.overlayElement = null;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Clear all intervals
|
|
68
|
-
this.detectionIntervals.forEach(interval => clearInterval(interval));
|
|
69
|
-
this.detectionIntervals = [];
|
|
70
|
-
|
|
71
|
-
// Remove all event listeners
|
|
72
|
-
this.eventListeners.forEach(({ target, event, handler }) => {
|
|
73
|
-
target.removeEventListener(event, handler);
|
|
74
|
-
});
|
|
75
|
-
this.eventListeners = [];
|
|
76
|
-
|
|
77
|
-
// Restore original getDisplayMedia
|
|
78
|
-
if (this.originalGetDisplayMedia && navigator.mediaDevices) {
|
|
79
|
-
navigator.mediaDevices.getDisplayMedia = this.originalGetDisplayMedia;
|
|
80
|
-
this.originalGetDisplayMedia = null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this.isActive = false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
public destroy(): void {
|
|
87
|
-
this.deactivate();
|
|
88
|
-
}
|
|
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
|
-
|
|
118
|
-
private applyVideoProtection(): void {
|
|
119
|
-
const { videoElement, containerElement } = this.opts;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
// Prevent Picture-in-Picture
|
|
123
|
-
videoElement.disablePictureInPicture = true;
|
|
124
|
-
(videoElement as any).autoPictureInPicture = false;
|
|
125
|
-
} catch (e) {
|
|
126
|
-
console.warn('[ScreenProtection] Could not disable PiP:', e);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
// Set controlsList to hide download button (if supported)
|
|
131
|
-
if ('controlsList' in videoElement) {
|
|
132
|
-
(videoElement as any).controlsList.add('nodownload');
|
|
133
|
-
}
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.warn('[ScreenProtection] controlsList not supported:', e);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Prevent text selection
|
|
139
|
-
containerElement.style.userSelect = 'none';
|
|
140
|
-
containerElement.style.webkitUserSelect = 'none';
|
|
141
|
-
(containerElement.style as any).webkitTouchCallout = 'none';
|
|
142
|
-
|
|
143
|
-
// Prevent right-click context menu
|
|
144
|
-
const contextMenuHandler = (e: Event): boolean => {
|
|
145
|
-
if (containerElement.contains(e.target as Node)) {
|
|
146
|
-
e.preventDefault();
|
|
147
|
-
e.stopPropagation();
|
|
148
|
-
return false;
|
|
149
|
-
}
|
|
150
|
-
return true;
|
|
151
|
-
};
|
|
152
|
-
document.addEventListener('contextmenu', contextMenuHandler);
|
|
153
|
-
this.eventListeners.push({ target: document, event: 'contextmenu', handler: contextMenuHandler });
|
|
154
|
-
|
|
155
|
-
console.log('[ScreenProtection] Video protection applied');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private createInterferenceOverlay(): void {
|
|
159
|
-
const { containerElement } = this.opts;
|
|
160
|
-
|
|
161
|
-
// Create transparent overlay
|
|
162
|
-
this.overlayElement = document.createElement('div');
|
|
163
|
-
this.overlayElement.className = 'uvf-screen-protection-overlay';
|
|
164
|
-
this.overlayElement.setAttribute('aria-hidden', 'true');
|
|
165
|
-
|
|
166
|
-
// Style the overlay
|
|
167
|
-
Object.assign(this.overlayElement.style, {
|
|
168
|
-
position: 'absolute',
|
|
169
|
-
top: '0',
|
|
170
|
-
left: '0',
|
|
171
|
-
width: '100%',
|
|
172
|
-
height: '100%',
|
|
173
|
-
pointerEvents: 'none', // Don't block user interactions
|
|
174
|
-
zIndex: '3', // Between video (1) and watermark (5)
|
|
175
|
-
mixBlendMode: 'screen',
|
|
176
|
-
opacity: '0.01',
|
|
177
|
-
background: 'transparent',
|
|
178
|
-
// Add subtle noise pattern
|
|
179
|
-
backgroundImage: 'url("data:image/svg+xml,%3Csvg viewBox=\'0 0 256 256\' xmlns=\'http://www.w3.org/2000/svg\'%3E%3Cfilter id=\'noise\'%3E%3CfeTurbulence type=\'fractalNoise\' baseFrequency=\'0.9\' numOctaves=\'4\' stitchTiles=\'stitch\'/%3E%3C/filter%3E%3Crect width=\'100%25\' height=\'100%25\' filter=\'url(%23noise)\' opacity=\'0.05\'/%3E%3C/svg%3E")',
|
|
180
|
-
transition: 'opacity 0.3s ease-in-out'
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// Insert into container
|
|
184
|
-
const watermarkCanvas = containerElement.querySelector('.uvf-watermark-layer');
|
|
185
|
-
if (watermarkCanvas) {
|
|
186
|
-
containerElement.insertBefore(this.overlayElement, watermarkCanvas);
|
|
187
|
-
} else {
|
|
188
|
-
containerElement.appendChild(this.overlayElement);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Start overlay animation
|
|
192
|
-
this.animateOverlay();
|
|
193
|
-
|
|
194
|
-
console.log('[ScreenProtection] Interference overlay created');
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
private animateOverlay(): void {
|
|
198
|
-
if (!this.overlayElement || !this.isActive) return;
|
|
199
|
-
|
|
200
|
-
// Subtle opacity pulsing
|
|
201
|
-
const baseOpacity = 0.01;
|
|
202
|
-
const variation = baseOpacity * 0.5;
|
|
203
|
-
const newOpacity = baseOpacity + (Math.random() * variation - variation / 2);
|
|
204
|
-
|
|
205
|
-
this.overlayElement.style.opacity = newOpacity.toString();
|
|
206
|
-
|
|
207
|
-
// Continue animation
|
|
208
|
-
setTimeout(() => this.animateOverlay(), 3000 + Math.random() * 2000);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
private setupDetection(): void {
|
|
212
|
-
// DevTools detection
|
|
213
|
-
this.detectDevTools();
|
|
214
|
-
|
|
215
|
-
// Focus loss detection
|
|
216
|
-
this.detectFocusLoss();
|
|
217
|
-
|
|
218
|
-
// Visibility change detection
|
|
219
|
-
this.detectVisibilityChange();
|
|
220
|
-
|
|
221
|
-
// Screen Capture API detection
|
|
222
|
-
this.detectScreenCaptureAPI();
|
|
223
|
-
|
|
224
|
-
console.log('[ScreenProtection] Detection mechanisms active');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private detectDevTools(): void {
|
|
228
|
-
const threshold = 160;
|
|
229
|
-
let previousState = false;
|
|
230
|
-
|
|
231
|
-
const checkDevTools = () => {
|
|
232
|
-
if (!this.isActive) return;
|
|
233
|
-
|
|
234
|
-
const widthDiff = window.outerWidth - window.innerWidth;
|
|
235
|
-
const heightDiff = window.outerHeight - window.innerHeight;
|
|
236
|
-
|
|
237
|
-
const isOpen = widthDiff > threshold || heightDiff > threshold;
|
|
238
|
-
|
|
239
|
-
if (isOpen && !previousState) {
|
|
240
|
-
this.devToolsOpen = true;
|
|
241
|
-
this.handleDetection({
|
|
242
|
-
type: 'devtools',
|
|
243
|
-
timestamp: Date.now(),
|
|
244
|
-
details: { widthDiff, heightDiff }
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
if (this.opts.onDevToolsDetected) {
|
|
248
|
-
this.opts.onDevToolsDetected();
|
|
249
|
-
}
|
|
250
|
-
} else if (!isOpen && previousState) {
|
|
251
|
-
this.devToolsOpen = false;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
previousState = isOpen;
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
const interval = setInterval(checkDevTools, 500);
|
|
258
|
-
this.detectionIntervals.push(interval);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private detectFocusLoss(): void {
|
|
262
|
-
const blurHandler = () => {
|
|
263
|
-
this.handleDetection({
|
|
264
|
-
type: 'focus-loss',
|
|
265
|
-
timestamp: Date.now(),
|
|
266
|
-
details: { documentHidden: document.hidden }
|
|
267
|
-
});
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
window.addEventListener('blur', blurHandler);
|
|
271
|
-
this.eventListeners.push({ target: window, event: 'blur', handler: blurHandler });
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
private detectVisibilityChange(): void {
|
|
275
|
-
const visibilityHandler = () => {
|
|
276
|
-
if (document.hidden) {
|
|
277
|
-
this.handleDetection({
|
|
278
|
-
type: 'visibility-change',
|
|
279
|
-
timestamp: Date.now(),
|
|
280
|
-
details: { visibilityState: document.visibilityState }
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
document.addEventListener('visibilitychange', visibilityHandler);
|
|
286
|
-
this.eventListeners.push({ target: document, event: 'visibilitychange', handler: visibilityHandler });
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
private detectScreenCaptureAPI(): void {
|
|
290
|
-
// Monkey-patch getDisplayMedia if it exists
|
|
291
|
-
if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
|
|
292
|
-
this.originalGetDisplayMedia = navigator.mediaDevices.getDisplayMedia.bind(
|
|
293
|
-
navigator.mediaDevices
|
|
294
|
-
);
|
|
295
|
-
|
|
296
|
-
const self = this;
|
|
297
|
-
navigator.mediaDevices.getDisplayMedia = async function (...args: any[]) {
|
|
298
|
-
// Detected screen capture attempt
|
|
299
|
-
self.handleDetection({
|
|
300
|
-
type: 'screen-capture-api',
|
|
301
|
-
timestamp: Date.now(),
|
|
302
|
-
details: { api: 'getDisplayMedia', args: args.length }
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// Still allow the call to proceed (can't block it)
|
|
306
|
-
return self.originalGetDisplayMedia(...args);
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
private handleDetection(event: ScreenCaptureEvent): void {
|
|
312
|
-
console.warn('[ScreenProtection] Detection event:', event);
|
|
313
|
-
|
|
314
|
-
// Trigger callback
|
|
315
|
-
if (this.opts.onDetection) {
|
|
316
|
-
this.opts.onDetection(event);
|
|
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);
|
|
390
|
-
}
|
|
391
|
-
}
|