unified-video-framework 1.4.387 → 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 -29
- package/packages/core/dist/interfaces.d.ts.map +1 -1
- package/packages/core/src/interfaces.ts +1 -56
- 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 -22
- 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/src/WebPlayer.ts +2 -120
- package/packages/web/src/index.ts +0 -10
- package/packages/web/src/react/WebPlayerView.tsx +28 -25
- 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 -508
|
@@ -432,31 +432,6 @@ export type WebPlayerViewProps = {
|
|
|
432
432
|
|
|
433
433
|
// Flash News Ticker
|
|
434
434
|
flashNewsTicker?: FlashNewsTickerConfig; // Flash news ticker configuration
|
|
435
|
-
|
|
436
|
-
// Screen Protection (Simple: boolean | Advanced: config object)
|
|
437
|
-
screenProtection?: boolean | {
|
|
438
|
-
enabled: boolean;
|
|
439
|
-
forensicWatermark?: {
|
|
440
|
-
userId?: string;
|
|
441
|
-
sessionId?: string;
|
|
442
|
-
invisible?: boolean;
|
|
443
|
-
};
|
|
444
|
-
canvasRendering?: {
|
|
445
|
-
enabled: boolean;
|
|
446
|
-
enableNoise?: boolean;
|
|
447
|
-
enableDynamicTransforms?: boolean;
|
|
448
|
-
};
|
|
449
|
-
aggressiveMode?: boolean;
|
|
450
|
-
onDetection?: 'warn' | 'pause' | 'degrade' | 'blackout';
|
|
451
|
-
blackoutDuration?: number;
|
|
452
|
-
warningMessage?: string;
|
|
453
|
-
trackingEndpoint?: string;
|
|
454
|
-
onScreenCaptureDetected?: (event: any) => void;
|
|
455
|
-
onDevToolsDetected?: () => void;
|
|
456
|
-
};
|
|
457
|
-
// Legacy callbacks (deprecated)
|
|
458
|
-
onScreenCaptureDetected?: (event: any) => void;
|
|
459
|
-
onDevToolsDetected?: () => void;
|
|
460
435
|
};
|
|
461
436
|
|
|
462
437
|
export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
@@ -1287,6 +1262,13 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1287
1262
|
midrollTimes: props.googleAds.midrollTimes,
|
|
1288
1263
|
companionAdSlots: props.googleAds.companionAdSlots,
|
|
1289
1264
|
onAdStart: () => {
|
|
1265
|
+
// Clear the fallback timeout since a pre-roll ad is actually starting
|
|
1266
|
+
if (prerollFallbackTimeout) {
|
|
1267
|
+
clearTimeout(prerollFallbackTimeout);
|
|
1268
|
+
prerollFallbackTimeout = null;
|
|
1269
|
+
console.log('✅ Pre-roll ad started, fallback timeout cleared');
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1290
1272
|
setIsAdPlaying(true);
|
|
1291
1273
|
// Notify player to block keyboard controls
|
|
1292
1274
|
if (typeof (player as any).setAdPlaying === 'function') {
|
|
@@ -1303,6 +1285,12 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1303
1285
|
props.googleAds?.onAdEnd?.();
|
|
1304
1286
|
},
|
|
1305
1287
|
onAdError: (error) => {
|
|
1288
|
+
// Clear the fallback timeout on ad error
|
|
1289
|
+
if (prerollFallbackTimeout) {
|
|
1290
|
+
clearTimeout(prerollFallbackTimeout);
|
|
1291
|
+
prerollFallbackTimeout = null;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1306
1294
|
setIsAdPlaying(false);
|
|
1307
1295
|
// Notify player to unblock keyboard controls
|
|
1308
1296
|
if (typeof (player as any).setAdPlaying === 'function') {
|
|
@@ -1344,6 +1332,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1344
1332
|
// Initialize ad display container on first user interaction
|
|
1345
1333
|
// Chrome requires this to be called on a user gesture
|
|
1346
1334
|
let adContainerInitialized = false;
|
|
1335
|
+
let prerollFallbackTimeout: NodeJS.Timeout | null = null;
|
|
1347
1336
|
|
|
1348
1337
|
const initAdsOnUserGesture = () => {
|
|
1349
1338
|
if (!adContainerInitialized && adsManagerRef.current) {
|
|
@@ -1360,6 +1349,20 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1360
1349
|
const handleFirstPlay = () => {
|
|
1361
1350
|
initAdsOnUserGesture();
|
|
1362
1351
|
if (adsManagerRef.current && adContainerInitialized) {
|
|
1352
|
+
// CRITICAL FIX: Pause video immediately to prevent content playback during ad loading
|
|
1353
|
+
// This prevents 2-4 seconds of video playing while pre-roll ads are being fetched
|
|
1354
|
+
console.log('🛑 Pausing video for pre-roll ads...');
|
|
1355
|
+
videoElement.pause();
|
|
1356
|
+
|
|
1357
|
+
// Safety mechanism: If no pre-roll ad starts within 1 second, resume video
|
|
1358
|
+
// This handles cases where only mid-roll or post-roll ads are configured
|
|
1359
|
+
prerollFallbackTimeout = setTimeout(() => {
|
|
1360
|
+
if (!adsManagerRef.current?.isPlayingAd()) {
|
|
1361
|
+
console.log('⏩ No pre-roll ad detected, resuming video playback');
|
|
1362
|
+
videoElement.play().catch(() => {});
|
|
1363
|
+
}
|
|
1364
|
+
}, 1000);
|
|
1365
|
+
|
|
1363
1366
|
adsManagerRef.current.requestAds();
|
|
1364
1367
|
}
|
|
1365
1368
|
videoElement.removeEventListener('play', handleFirstPlay);
|
package/scripts/fix-imports.js
CHANGED
|
@@ -28,24 +28,6 @@ function fixImports(filePath) {
|
|
|
28
28
|
'import "../../core/dist/index.js"'
|
|
29
29
|
);
|
|
30
30
|
|
|
31
|
-
// Fix imports to core/dist to use index.js (both with and without .js)
|
|
32
|
-
content = content.replace(
|
|
33
|
-
/from\s+["']\.\.\/\.\.\/\.\.\/core\/dist\.js["']/g,
|
|
34
|
-
'from "../../../core/dist/index.js"'
|
|
35
|
-
);
|
|
36
|
-
content = content.replace(
|
|
37
|
-
/from\s+["']\.\.\/\.\.\/\.\.\/core\/dist["']/g,
|
|
38
|
-
'from "../../../core/dist/index.js"'
|
|
39
|
-
);
|
|
40
|
-
content = content.replace(
|
|
41
|
-
/from\s+["']\.\.\/\.\.\/core\/dist\.js["']/g,
|
|
42
|
-
'from "../../core/dist/index.js"'
|
|
43
|
-
);
|
|
44
|
-
content = content.replace(
|
|
45
|
-
/from\s+["']\.\.\/\.\.\/core\/dist["']/g,
|
|
46
|
-
'from "../../core/dist/index.js"'
|
|
47
|
-
);
|
|
48
|
-
|
|
49
31
|
// Fix relative imports within the same package to include .js extension
|
|
50
32
|
content = content.replace(
|
|
51
33
|
/from\s+["']\.\/([^"']+)(?<!\.js)["']/g,
|
|
@@ -54,10 +36,6 @@ function fixImports(filePath) {
|
|
|
54
36
|
content = content.replace(
|
|
55
37
|
/from\s+["']\.\.?\/([^"']+)(?<!\.js)["']/g,
|
|
56
38
|
(match, p1) => {
|
|
57
|
-
// Skip if it's already been handled (ends with index.js)
|
|
58
|
-
if (p1.includes('index.js')) {
|
|
59
|
-
return match;
|
|
60
|
-
}
|
|
61
39
|
if (p1.includes('/')) {
|
|
62
40
|
return `from "../${p1.replace(/([^\/]+)$/, '$1.js')}"`;
|
|
63
41
|
}
|
|
@@ -80,24 +58,6 @@ function fixImports(filePath) {
|
|
|
80
58
|
'import "../../core/dist/index.js"'
|
|
81
59
|
);
|
|
82
60
|
|
|
83
|
-
// Fix imports to core/dist to use index.js (both with and without .js)
|
|
84
|
-
content = content.replace(
|
|
85
|
-
/from\s+["']\.\.\/\.\.\/\.\.\/core\/dist\.js["']/g,
|
|
86
|
-
'from "../../../core/dist/index.js"'
|
|
87
|
-
);
|
|
88
|
-
content = content.replace(
|
|
89
|
-
/from\s+["']\.\.\/\.\.\/\.\.\/core\/dist["']/g,
|
|
90
|
-
'from "../../../core/dist/index.js"'
|
|
91
|
-
);
|
|
92
|
-
content = content.replace(
|
|
93
|
-
/from\s+["']\.\.\/\.\.\/core\/dist\.js["']/g,
|
|
94
|
-
'from "../../core/dist/index.js"'
|
|
95
|
-
);
|
|
96
|
-
content = content.replace(
|
|
97
|
-
/from\s+["']\.\.\/\.\.\/core\/dist["']/g,
|
|
98
|
-
'from "../../core/dist/index.js"'
|
|
99
|
-
);
|
|
100
|
-
|
|
101
61
|
// Fix relative imports within the same package to include .js extension
|
|
102
62
|
content = content.replace(
|
|
103
63
|
/from\s+["']\.\/([^"']+)(?<!\.js)["']/g,
|
|
@@ -106,10 +66,6 @@ function fixImports(filePath) {
|
|
|
106
66
|
content = content.replace(
|
|
107
67
|
/from\s+["']\.\.?\/([^"']+)(?<!\.js)["']/g,
|
|
108
68
|
(match, p1) => {
|
|
109
|
-
// Skip if it's already been handled (ends with index.js)
|
|
110
|
-
if (p1.includes('index.js')) {
|
|
111
|
-
return match;
|
|
112
|
-
}
|
|
113
69
|
if (p1.includes('/')) {
|
|
114
70
|
return `from "../${p1.replace(/([^\/]+)$/, '$1.js')}"`;
|
|
115
71
|
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DRM Helper Utility
|
|
3
|
-
*
|
|
4
|
-
* Simplifies DRM configuration for users who need maximum security (true black screen protection).
|
|
5
|
-
* This is Option B - for advanced users who can encrypt their videos and set up license servers.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { DRMConfig, DRMType } from '../../../core/dist';
|
|
9
|
-
|
|
10
|
-
export interface DRMHelperOptions {
|
|
11
|
-
licenseServerUrl: string;
|
|
12
|
-
certificateUrl?: string; // Required for FairPlay (Safari)
|
|
13
|
-
headers?: Record<string, string>;
|
|
14
|
-
preferredDRM?: 'widevine' | 'playready' | 'fairplay' | 'auto';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class DRMHelper {
|
|
18
|
-
/**
|
|
19
|
-
* Detect the best DRM system for the current browser
|
|
20
|
-
*/
|
|
21
|
-
static detectBestDRM(): DRMType {
|
|
22
|
-
const ua = navigator.userAgent.toLowerCase();
|
|
23
|
-
|
|
24
|
-
// Safari - use FairPlay
|
|
25
|
-
if (ua.indexOf('safari') !== -1 && ua.indexOf('chrome') === -1) {
|
|
26
|
-
return DRMType.FAIRPLAY;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Edge - prefer PlayReady, fallback to Widevine
|
|
30
|
-
if (ua.indexOf('edg') !== -1) {
|
|
31
|
-
return DRMType.PLAYREADY;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Chrome, Firefox, Opera - use Widevine
|
|
35
|
-
return DRMType.WIDEVINE;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Create a simple DRM configuration
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* const drmConfig = DRMHelper.createConfig({
|
|
43
|
-
* licenseServerUrl: 'https://your-license-server.com/license',
|
|
44
|
-
* certificateUrl: 'https://your-license-server.com/cert', // FairPlay only
|
|
45
|
-
* headers: {
|
|
46
|
-
* 'Authorization': 'Bearer your-token'
|
|
47
|
-
* }
|
|
48
|
-
* });
|
|
49
|
-
*/
|
|
50
|
-
static createConfig(options: DRMHelperOptions): DRMConfig {
|
|
51
|
-
const preferredDRMMap: Record<string, DRMType> = {
|
|
52
|
-
'widevine': DRMType.WIDEVINE,
|
|
53
|
-
'playready': DRMType.PLAYREADY,
|
|
54
|
-
'fairplay': DRMType.FAIRPLAY
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const drmType = options.preferredDRM === 'auto' || !options.preferredDRM
|
|
58
|
-
? this.detectBestDRM()
|
|
59
|
-
: preferredDRMMap[options.preferredDRM];
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
type: drmType,
|
|
63
|
-
licenseUrl: options.licenseServerUrl,
|
|
64
|
-
certificateUrl: options.certificateUrl,
|
|
65
|
-
headers: options.headers || {},
|
|
66
|
-
// Widevine security level (L1 = hardware-backed, most secure)
|
|
67
|
-
...(drmType === DRMType.WIDEVINE && { securityLevel: 'L1' as any })
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Check if browser supports DRM
|
|
73
|
-
*/
|
|
74
|
-
static async checkDRMSupport(): Promise<{
|
|
75
|
-
widevine: boolean;
|
|
76
|
-
playready: boolean;
|
|
77
|
-
fairplay: boolean;
|
|
78
|
-
recommended: DRMType;
|
|
79
|
-
}> {
|
|
80
|
-
const keySystemMap = {
|
|
81
|
-
widevine: 'com.widevine.alpha',
|
|
82
|
-
playready: 'com.microsoft.playready',
|
|
83
|
-
fairplay: 'com.apple.fps'
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const support = {
|
|
87
|
-
widevine: false,
|
|
88
|
-
playready: false,
|
|
89
|
-
fairplay: false,
|
|
90
|
-
recommended: 'widevine' as DRMType
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (!navigator.requestMediaKeySystemAccess) {
|
|
94
|
-
return support;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Check Widevine
|
|
98
|
-
try {
|
|
99
|
-
await navigator.requestMediaKeySystemAccess(keySystemMap.widevine, [{
|
|
100
|
-
initDataTypes: ['cenc'],
|
|
101
|
-
videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
|
|
102
|
-
}]);
|
|
103
|
-
support.widevine = true;
|
|
104
|
-
} catch (e) {}
|
|
105
|
-
|
|
106
|
-
// Check PlayReady
|
|
107
|
-
try {
|
|
108
|
-
await navigator.requestMediaKeySystemAccess(keySystemMap.playready, [{
|
|
109
|
-
initDataTypes: ['cenc'],
|
|
110
|
-
videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
|
|
111
|
-
}]);
|
|
112
|
-
support.playready = true;
|
|
113
|
-
} catch (e) {}
|
|
114
|
-
|
|
115
|
-
// Check FairPlay
|
|
116
|
-
try {
|
|
117
|
-
await navigator.requestMediaKeySystemAccess(keySystemMap.fairplay, [{
|
|
118
|
-
initDataTypes: ['skd'],
|
|
119
|
-
videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
|
|
120
|
-
}]);
|
|
121
|
-
support.fairplay = true;
|
|
122
|
-
} catch (e) {}
|
|
123
|
-
|
|
124
|
-
// Set recommended based on what's supported
|
|
125
|
-
support.recommended = this.detectBestDRM();
|
|
126
|
-
|
|
127
|
-
return support;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Validate DRM configuration
|
|
132
|
-
*/
|
|
133
|
-
static validateConfig(config: DRMConfig): { valid: boolean; errors: string[] } {
|
|
134
|
-
const errors: string[] = [];
|
|
135
|
-
|
|
136
|
-
if (!config.type) {
|
|
137
|
-
errors.push('DRM type is required');
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (!config.licenseUrl) {
|
|
141
|
-
errors.push('License server URL is required');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (config.type === 'fairplay' && !config.certificateUrl) {
|
|
145
|
-
errors.push('Certificate URL is required for FairPlay DRM');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
new URL(config.licenseUrl || '');
|
|
150
|
-
} catch (e) {
|
|
151
|
-
errors.push('License URL must be a valid URL');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
valid: errors.length === 0,
|
|
156
|
-
errors
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Quick start examples for common DRM providers
|
|
163
|
-
*/
|
|
164
|
-
export const DRMProviderExamples = {
|
|
165
|
-
/**
|
|
166
|
-
* BuyDRM KeyOS
|
|
167
|
-
* https://www.buydrm.com/
|
|
168
|
-
*/
|
|
169
|
-
BuyDRM: (customerId: string, videoId: string): DRMHelperOptions => ({
|
|
170
|
-
licenseServerUrl: `https://wv.service.expressplay.com/hms/wv/rights/?ExpressPlayToken=${customerId}`,
|
|
171
|
-
preferredDRM: 'auto'
|
|
172
|
-
}),
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Irdeto
|
|
176
|
-
* https://irdeto.com/
|
|
177
|
-
*/
|
|
178
|
-
Irdeto: (licenseUrl: string): DRMHelperOptions => ({
|
|
179
|
-
licenseServerUrl: licenseUrl,
|
|
180
|
-
preferredDRM: 'auto'
|
|
181
|
-
}),
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Verimatrix
|
|
185
|
-
* https://www.verimatrix.com/
|
|
186
|
-
*/
|
|
187
|
-
Verimatrix: (licenseUrl: string, token: string): DRMHelperOptions => ({
|
|
188
|
-
licenseServerUrl: licenseUrl,
|
|
189
|
-
headers: {
|
|
190
|
-
'Authorization': `Bearer ${token}`
|
|
191
|
-
},
|
|
192
|
-
preferredDRM: 'auto'
|
|
193
|
-
}),
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Custom DRM server
|
|
197
|
-
*/
|
|
198
|
-
Custom: (licenseUrl: string, certificateUrl?: string): DRMHelperOptions => ({
|
|
199
|
-
licenseServerUrl: licenseUrl,
|
|
200
|
-
certificateUrl: certificateUrl,
|
|
201
|
-
preferredDRM: 'auto'
|
|
202
|
-
})
|
|
203
|
-
};
|
|
@@ -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
|
-
}
|