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.
@@ -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);
@@ -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
- }