unified-video-framework 1.4.387 → 1.4.389
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 +22 -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 +35 -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,16 @@ 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
|
+
|
|
1272
|
+
// Hide loading since ad is now playing
|
|
1273
|
+
setIsLoading(false);
|
|
1274
|
+
|
|
1290
1275
|
setIsAdPlaying(true);
|
|
1291
1276
|
// Notify player to block keyboard controls
|
|
1292
1277
|
if (typeof (player as any).setAdPlaying === 'function') {
|
|
@@ -1303,6 +1288,15 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1303
1288
|
props.googleAds?.onAdEnd?.();
|
|
1304
1289
|
},
|
|
1305
1290
|
onAdError: (error) => {
|
|
1291
|
+
// Clear the fallback timeout on ad error
|
|
1292
|
+
if (prerollFallbackTimeout) {
|
|
1293
|
+
clearTimeout(prerollFallbackTimeout);
|
|
1294
|
+
prerollFallbackTimeout = null;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Hide loading on error
|
|
1298
|
+
setIsLoading(false);
|
|
1299
|
+
|
|
1306
1300
|
setIsAdPlaying(false);
|
|
1307
1301
|
// Notify player to unblock keyboard controls
|
|
1308
1302
|
if (typeof (player as any).setAdPlaying === 'function') {
|
|
@@ -1344,6 +1338,7 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1344
1338
|
// Initialize ad display container on first user interaction
|
|
1345
1339
|
// Chrome requires this to be called on a user gesture
|
|
1346
1340
|
let adContainerInitialized = false;
|
|
1341
|
+
let prerollFallbackTimeout: NodeJS.Timeout | null = null;
|
|
1347
1342
|
|
|
1348
1343
|
const initAdsOnUserGesture = () => {
|
|
1349
1344
|
if (!adContainerInitialized && adsManagerRef.current) {
|
|
@@ -1360,6 +1355,21 @@ export const WebPlayerView: React.FC<WebPlayerViewProps> = (props) => {
|
|
|
1360
1355
|
const handleFirstPlay = () => {
|
|
1361
1356
|
initAdsOnUserGesture();
|
|
1362
1357
|
if (adsManagerRef.current && adContainerInitialized) {
|
|
1358
|
+
// Show loading while pre-roll ads are being fetched and prepared
|
|
1359
|
+
console.log('⏳ Loading pre-roll ads...');
|
|
1360
|
+
setIsLoading(true);
|
|
1361
|
+
videoElement.pause();
|
|
1362
|
+
|
|
1363
|
+
// Safety mechanism: If no pre-roll ad starts within 2 seconds, hide loading and resume video
|
|
1364
|
+
// This handles cases where only mid-roll or post-roll ads are configured
|
|
1365
|
+
prerollFallbackTimeout = setTimeout(() => {
|
|
1366
|
+
if (!adsManagerRef.current?.isPlayingAd()) {
|
|
1367
|
+
console.log('⏩ No pre-roll ad detected, resuming normal playback');
|
|
1368
|
+
setIsLoading(false);
|
|
1369
|
+
videoElement.play().catch(() => {});
|
|
1370
|
+
}
|
|
1371
|
+
}, 2000);
|
|
1372
|
+
|
|
1363
1373
|
adsManagerRef.current.requestAds();
|
|
1364
1374
|
}
|
|
1365
1375
|
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
|
-
}
|