unified-video-framework 1.0.0
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/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecureVideoPlayer - VdoCipher-like implementation with DRM, watermarking, and security features
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { WebPlayer } from './WebPlayer';
|
|
6
|
+
import {
|
|
7
|
+
VideoSource,
|
|
8
|
+
PlayerConfig,
|
|
9
|
+
PlayerError,
|
|
10
|
+
Quality
|
|
11
|
+
} from '@unified-video/core';
|
|
12
|
+
|
|
13
|
+
// Extended configuration for secure player
|
|
14
|
+
export interface SecurePlayerConfig extends PlayerConfig {
|
|
15
|
+
// DRM Configuration
|
|
16
|
+
drm?: {
|
|
17
|
+
widevine?: {
|
|
18
|
+
licenseUrl: string;
|
|
19
|
+
certificateUrl?: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
fairplay?: {
|
|
23
|
+
licenseUrl: string;
|
|
24
|
+
certificateUrl: string;
|
|
25
|
+
headers?: Record<string, string>;
|
|
26
|
+
};
|
|
27
|
+
playready?: {
|
|
28
|
+
licenseUrl: string;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Security Configuration
|
|
34
|
+
security?: {
|
|
35
|
+
token: string;
|
|
36
|
+
otp?: string;
|
|
37
|
+
preventScreenCapture?: boolean;
|
|
38
|
+
preventInspect?: boolean;
|
|
39
|
+
domainLock?: string[];
|
|
40
|
+
ipWhitelist?: string[];
|
|
41
|
+
maxConcurrentStreams?: number;
|
|
42
|
+
sessionTimeout?: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Watermark Configuration
|
|
46
|
+
watermark?: {
|
|
47
|
+
text?: string;
|
|
48
|
+
email?: string;
|
|
49
|
+
userId?: string;
|
|
50
|
+
ip?: string;
|
|
51
|
+
opacity?: number;
|
|
52
|
+
fontSize?: number;
|
|
53
|
+
fontColor?: string;
|
|
54
|
+
position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'random';
|
|
55
|
+
moving?: boolean;
|
|
56
|
+
interval?: number;
|
|
57
|
+
blinking?: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Analytics Configuration
|
|
61
|
+
analytics?: {
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
endpoint?: string;
|
|
64
|
+
interval?: number;
|
|
65
|
+
customData?: Record<string, any>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Advanced Features
|
|
69
|
+
features?: {
|
|
70
|
+
speedControl?: boolean;
|
|
71
|
+
qualitySelector?: boolean;
|
|
72
|
+
chapters?: boolean;
|
|
73
|
+
thumbnailPreview?: boolean;
|
|
74
|
+
keyboardShortcuts?: boolean;
|
|
75
|
+
gestureControl?: boolean;
|
|
76
|
+
chromecast?: boolean;
|
|
77
|
+
airplay?: boolean;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DRMConfig {
|
|
82
|
+
server: string;
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
withCredentials?: boolean;
|
|
85
|
+
certificateUrl?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface WatermarkLayer {
|
|
89
|
+
canvas: HTMLCanvasElement;
|
|
90
|
+
context: CanvasRenderingContext2D;
|
|
91
|
+
animationFrame?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AnalyticsEvent {
|
|
95
|
+
eventType: string;
|
|
96
|
+
timestamp: number;
|
|
97
|
+
sessionId: string;
|
|
98
|
+
videoId?: string;
|
|
99
|
+
userId?: string;
|
|
100
|
+
data: Record<string, any>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export class SecureVideoPlayer extends WebPlayer {
|
|
104
|
+
private secureConfig: SecurePlayerConfig;
|
|
105
|
+
private watermarkLayer?: WatermarkLayer;
|
|
106
|
+
private analyticsTimer?: number;
|
|
107
|
+
private sessionId: string;
|
|
108
|
+
private heartbeatTimer?: number;
|
|
109
|
+
private qualityMenu?: HTMLElement;
|
|
110
|
+
private customControls?: HTMLElement;
|
|
111
|
+
private thumbnailPreview?: HTMLElement;
|
|
112
|
+
private analyticsData: AnalyticsEvent[] = [];
|
|
113
|
+
private watchStartTime: number = 0;
|
|
114
|
+
private totalWatchTime: number = 0;
|
|
115
|
+
private lastSeekPosition: number = 0;
|
|
116
|
+
private bufferingStartTime: number = 0;
|
|
117
|
+
private totalBufferingTime: number = 0;
|
|
118
|
+
private screenRecordingProtection?: MutationObserver;
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
super();
|
|
122
|
+
this.sessionId = this.generateSessionId();
|
|
123
|
+
this.secureConfig = {} as SecurePlayerConfig;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected async setupPlayer(): Promise<void> {
|
|
127
|
+
await super.setupPlayer();
|
|
128
|
+
|
|
129
|
+
// Apply security measures
|
|
130
|
+
this.applySecurityMeasures();
|
|
131
|
+
|
|
132
|
+
// Setup DRM if configured
|
|
133
|
+
if (this.secureConfig.drm) {
|
|
134
|
+
this.configureDRM();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Setup watermark if configured
|
|
138
|
+
if (this.secureConfig.watermark) {
|
|
139
|
+
this.setupWatermark();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Setup analytics if enabled
|
|
143
|
+
if (this.secureConfig.analytics?.enabled) {
|
|
144
|
+
this.setupAnalytics();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Setup custom controls if needed
|
|
148
|
+
if (this.secureConfig.features) {
|
|
149
|
+
this.setupCustomControls();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Start session heartbeat
|
|
153
|
+
this.startHeartbeat();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async initialize(container: HTMLElement | string, config?: SecurePlayerConfig): Promise<void> {
|
|
157
|
+
this.secureConfig = config || {} as SecurePlayerConfig;
|
|
158
|
+
|
|
159
|
+
// Validate domain if domain lock is enabled
|
|
160
|
+
if (this.secureConfig.security?.domainLock) {
|
|
161
|
+
this.validateDomain();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate token
|
|
165
|
+
if (this.secureConfig.security?.token) {
|
|
166
|
+
await this.validateToken();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await super.initialize(container, this.secureConfig);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private applySecurityMeasures(): void {
|
|
173
|
+
if (!this.secureConfig.security) return;
|
|
174
|
+
|
|
175
|
+
// Prevent right-click context menu
|
|
176
|
+
if (this.secureConfig.security.preventInspect) {
|
|
177
|
+
this.preventInspection();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Prevent screen capture (limited effectiveness)
|
|
181
|
+
if (this.secureConfig.security.preventScreenCapture) {
|
|
182
|
+
this.preventScreenCapture();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Disable text selection
|
|
186
|
+
this.disableTextSelection();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private preventInspection(): void {
|
|
190
|
+
// Prevent right-click
|
|
191
|
+
document.addEventListener('contextmenu', (e) => {
|
|
192
|
+
if (this.container?.contains(e.target as Node)) {
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Prevent F12 and other dev tools shortcuts
|
|
198
|
+
document.addEventListener('keydown', (e) => {
|
|
199
|
+
// F12, Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+Shift+C
|
|
200
|
+
if (e.keyCode === 123 ||
|
|
201
|
+
(e.ctrlKey && e.shiftKey && (e.keyCode === 73 || e.keyCode === 74 || e.keyCode === 67))) {
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Detect dev tools (basic detection)
|
|
207
|
+
let devtools = { open: false, orientation: null };
|
|
208
|
+
const threshold = 160;
|
|
209
|
+
|
|
210
|
+
setInterval(() => {
|
|
211
|
+
if (window.outerHeight - window.innerHeight > threshold ||
|
|
212
|
+
window.outerWidth - window.innerWidth > threshold) {
|
|
213
|
+
if (!devtools.open) {
|
|
214
|
+
devtools.open = true;
|
|
215
|
+
this.handleDevToolsOpen();
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
devtools.open = false;
|
|
219
|
+
}
|
|
220
|
+
}, 500);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private handleDevToolsOpen(): void {
|
|
224
|
+
console.warn('Developer tools detected');
|
|
225
|
+
this.trackEvent({
|
|
226
|
+
eventType: 'security_warning',
|
|
227
|
+
timestamp: Date.now(),
|
|
228
|
+
sessionId: this.sessionId,
|
|
229
|
+
data: {
|
|
230
|
+
type: 'devtools_opened',
|
|
231
|
+
url: window.location.href
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private preventScreenCapture(): void {
|
|
237
|
+
// CSS-based screen capture prevention (limited support)
|
|
238
|
+
if (this.container) {
|
|
239
|
+
this.container.style.cssText += `
|
|
240
|
+
-webkit-user-select: none;
|
|
241
|
+
-moz-user-select: none;
|
|
242
|
+
-ms-user-select: none;
|
|
243
|
+
user-select: none;
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add overlay div that becomes black when screenshot is attempted (experimental)
|
|
248
|
+
const overlay = document.createElement('div');
|
|
249
|
+
overlay.style.cssText = `
|
|
250
|
+
position: absolute;
|
|
251
|
+
top: 0;
|
|
252
|
+
left: 0;
|
|
253
|
+
width: 100%;
|
|
254
|
+
height: 100%;
|
|
255
|
+
z-index: 9998;
|
|
256
|
+
pointer-events: none;
|
|
257
|
+
mix-blend-mode: screen;
|
|
258
|
+
background: transparent;
|
|
259
|
+
`;
|
|
260
|
+
this.container?.appendChild(overlay);
|
|
261
|
+
|
|
262
|
+
// Monitor for screen recording indicators (limited effectiveness)
|
|
263
|
+
this.detectScreenRecording();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private detectScreenRecording(): void {
|
|
267
|
+
// Check for common screen recording extensions (very limited)
|
|
268
|
+
const suspiciousExtensions = [
|
|
269
|
+
'screen-capture',
|
|
270
|
+
'screencastify',
|
|
271
|
+
'loom',
|
|
272
|
+
'awesome-screenshot'
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// Monitor DOM mutations for recording indicators
|
|
276
|
+
this.screenRecordingProtection = new MutationObserver((mutations) => {
|
|
277
|
+
mutations.forEach((mutation) => {
|
|
278
|
+
mutation.addedNodes.forEach((node) => {
|
|
279
|
+
if (node.nodeName && suspiciousExtensions.some(ext =>
|
|
280
|
+
node.nodeName.toLowerCase().includes(ext))) {
|
|
281
|
+
this.handleScreenRecordingDetected();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
this.screenRecordingProtection.observe(document.body, {
|
|
288
|
+
childList: true,
|
|
289
|
+
subtree: true
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private handleScreenRecordingDetected(): void {
|
|
294
|
+
console.warn('Potential screen recording detected');
|
|
295
|
+
this.trackEvent({
|
|
296
|
+
eventType: 'security_warning',
|
|
297
|
+
timestamp: Date.now(),
|
|
298
|
+
sessionId: this.sessionId,
|
|
299
|
+
data: {
|
|
300
|
+
type: 'screen_recording_suspected',
|
|
301
|
+
url: window.location.href
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private disableTextSelection(): void {
|
|
307
|
+
if (this.container) {
|
|
308
|
+
this.container.style.userSelect = 'none';
|
|
309
|
+
this.container.style.webkitUserSelect = 'none';
|
|
310
|
+
|
|
311
|
+
// Prevent text selection via JavaScript
|
|
312
|
+
this.container.addEventListener('selectstart', (e) => {
|
|
313
|
+
e.preventDefault();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private validateDomain(): void {
|
|
319
|
+
const currentDomain = window.location.hostname;
|
|
320
|
+
const allowedDomains = this.secureConfig.security?.domainLock || [];
|
|
321
|
+
|
|
322
|
+
if (!allowedDomains.includes(currentDomain)) {
|
|
323
|
+
throw new Error(`Domain ${currentDomain} is not authorized to play this video`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private async validateToken(): Promise<void> {
|
|
328
|
+
const token = this.secureConfig.security?.token;
|
|
329
|
+
const otp = this.secureConfig.security?.otp;
|
|
330
|
+
|
|
331
|
+
if (!token) {
|
|
332
|
+
throw new Error('Security token is required');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// In production, validate token with backend
|
|
336
|
+
try {
|
|
337
|
+
const response = await fetch(`${this.secureConfig.analytics?.endpoint || '/api'}/validate-token`, {
|
|
338
|
+
method: 'POST',
|
|
339
|
+
headers: {
|
|
340
|
+
'Content-Type': 'application/json',
|
|
341
|
+
'Authorization': `Bearer ${token}`
|
|
342
|
+
},
|
|
343
|
+
body: JSON.stringify({
|
|
344
|
+
otp,
|
|
345
|
+
sessionId: this.sessionId,
|
|
346
|
+
domain: window.location.hostname,
|
|
347
|
+
userAgent: navigator.userAgent
|
|
348
|
+
})
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
if (!response.ok) {
|
|
352
|
+
throw new Error('Token validation failed');
|
|
353
|
+
}
|
|
354
|
+
} catch (error) {
|
|
355
|
+
console.error('Token validation error:', error);
|
|
356
|
+
// In demo mode, continue without validation
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private configureDRM(): void {
|
|
361
|
+
if (!this.video) return;
|
|
362
|
+
|
|
363
|
+
const video = this.video as any;
|
|
364
|
+
|
|
365
|
+
// Setup EME (Encrypted Media Extensions)
|
|
366
|
+
if (video.requestMediaKeySystemAccess) {
|
|
367
|
+
this.setupEME();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Configure Shaka Player for DRM if needed
|
|
371
|
+
if (this.secureConfig.drm?.widevine || this.secureConfig.drm?.fairplay) {
|
|
372
|
+
this.setupShakaPlayer();
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private async setupEME(): Promise<void> {
|
|
377
|
+
const config = this.secureConfig.drm;
|
|
378
|
+
if (!config) return;
|
|
379
|
+
|
|
380
|
+
const keySystemConfigs: Record<string, any> = {};
|
|
381
|
+
|
|
382
|
+
// Widevine configuration
|
|
383
|
+
if (config.widevine) {
|
|
384
|
+
keySystemConfigs['com.widevine.alpha'] = [{
|
|
385
|
+
initDataTypes: ['cenc'],
|
|
386
|
+
videoCapabilities: [{
|
|
387
|
+
contentType: 'video/mp4;codecs="avc1.42E01E"'
|
|
388
|
+
}],
|
|
389
|
+
audioCapabilities: [{
|
|
390
|
+
contentType: 'audio/mp4;codecs="mp4a.40.2"'
|
|
391
|
+
}]
|
|
392
|
+
}];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// PlayReady configuration
|
|
396
|
+
if (config.playready) {
|
|
397
|
+
keySystemConfigs['com.microsoft.playready'] = [{
|
|
398
|
+
initDataTypes: ['cenc'],
|
|
399
|
+
videoCapabilities: [{
|
|
400
|
+
contentType: 'video/mp4;codecs="avc1.42E01E"'
|
|
401
|
+
}],
|
|
402
|
+
audioCapabilities: [{
|
|
403
|
+
contentType: 'audio/mp4;codecs="mp4a.40.2"'
|
|
404
|
+
}]
|
|
405
|
+
}];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// FairPlay configuration
|
|
409
|
+
if (config.fairplay) {
|
|
410
|
+
keySystemConfigs['com.apple.fps.1_0'] = [{
|
|
411
|
+
initDataTypes: ['cenc'],
|
|
412
|
+
videoCapabilities: [{
|
|
413
|
+
contentType: 'video/mp4;codecs="avc1.42E01E"'
|
|
414
|
+
}],
|
|
415
|
+
audioCapabilities: [{
|
|
416
|
+
contentType: 'audio/mp4;codecs="mp4a.40.2"'
|
|
417
|
+
}]
|
|
418
|
+
}];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Request access to key systems
|
|
422
|
+
for (const [keySystem, configs] of Object.entries(keySystemConfigs)) {
|
|
423
|
+
try {
|
|
424
|
+
const access = await navigator.requestMediaKeySystemAccess(keySystem, configs);
|
|
425
|
+
const mediaKeys = await access.createMediaKeys();
|
|
426
|
+
await this.video!.setMediaKeys(mediaKeys);
|
|
427
|
+
|
|
428
|
+
// Set up license request handling
|
|
429
|
+
this.setupLicenseRequest(mediaKeys, keySystem);
|
|
430
|
+
|
|
431
|
+
console.log(`DRM system ${keySystem} initialized`);
|
|
432
|
+
break;
|
|
433
|
+
} catch (error) {
|
|
434
|
+
console.error(`Failed to setup ${keySystem}:`, error);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private setupLicenseRequest(mediaKeys: MediaKeys, keySystem: string): void {
|
|
440
|
+
if (!this.video) return;
|
|
441
|
+
|
|
442
|
+
this.video.addEventListener('encrypted', async (event: any) => {
|
|
443
|
+
const session = mediaKeys.createSession();
|
|
444
|
+
|
|
445
|
+
session.addEventListener('message', async (event: any) => {
|
|
446
|
+
const message = event.message;
|
|
447
|
+
const licenseUrl = this.getLicenseUrl(keySystem);
|
|
448
|
+
|
|
449
|
+
if (licenseUrl) {
|
|
450
|
+
try {
|
|
451
|
+
const response = await this.requestLicense(licenseUrl, message, keySystem);
|
|
452
|
+
await session.update(response);
|
|
453
|
+
} catch (error) {
|
|
454
|
+
this.handleError({
|
|
455
|
+
code: 'DRM_LICENSE_ERROR',
|
|
456
|
+
message: `Failed to acquire license: ${error}`,
|
|
457
|
+
type: 'drm',
|
|
458
|
+
fatal: true,
|
|
459
|
+
details: error
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await session.generateRequest(event.initDataType, event.initData);
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private getLicenseUrl(keySystem: string): string | null {
|
|
470
|
+
switch (keySystem) {
|
|
471
|
+
case 'com.widevine.alpha':
|
|
472
|
+
return this.secureConfig.drm?.widevine?.licenseUrl || null;
|
|
473
|
+
case 'com.microsoft.playready':
|
|
474
|
+
return this.secureConfig.drm?.playready?.licenseUrl || null;
|
|
475
|
+
case 'com.apple.fps.1_0':
|
|
476
|
+
return this.secureConfig.drm?.fairplay?.licenseUrl || null;
|
|
477
|
+
default:
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private async requestLicense(url: string, message: ArrayBuffer, keySystem: string): Promise<ArrayBuffer> {
|
|
483
|
+
const headers = this.getLicenseHeaders(keySystem);
|
|
484
|
+
|
|
485
|
+
const response = await fetch(url, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
headers: {
|
|
488
|
+
...headers,
|
|
489
|
+
'Content-Type': 'application/octet-stream'
|
|
490
|
+
},
|
|
491
|
+
body: message
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
if (!response.ok) {
|
|
495
|
+
throw new Error(`License request failed: ${response.status}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return await response.arrayBuffer();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private getLicenseHeaders(keySystem: string): Record<string, string> {
|
|
502
|
+
const token = this.secureConfig.security?.token || '';
|
|
503
|
+
let headers: Record<string, string> = {
|
|
504
|
+
'Authorization': `Bearer ${token}`
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
switch (keySystem) {
|
|
508
|
+
case 'com.widevine.alpha':
|
|
509
|
+
headers = { ...headers, ...this.secureConfig.drm?.widevine?.headers };
|
|
510
|
+
break;
|
|
511
|
+
case 'com.microsoft.playready':
|
|
512
|
+
headers = { ...headers, ...this.secureConfig.drm?.playready?.headers };
|
|
513
|
+
break;
|
|
514
|
+
case 'com.apple.fps.1_0':
|
|
515
|
+
headers = { ...headers, ...this.secureConfig.drm?.fairplay?.headers };
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return headers;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private async setupShakaPlayer(): Promise<void> {
|
|
523
|
+
// Load Shaka Player if not already loaded
|
|
524
|
+
if (!(window as any).shaka) {
|
|
525
|
+
await this.loadScript('https://cdn.jsdelivr.net/npm/shaka-player@latest/dist/shaka-player.compiled.js');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const shaka = (window as any).shaka;
|
|
529
|
+
|
|
530
|
+
if (!shaka.Player.isBrowserSupported()) {
|
|
531
|
+
console.error('Browser does not support Shaka Player');
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const player = new shaka.Player(this.video);
|
|
536
|
+
|
|
537
|
+
// Configure DRM
|
|
538
|
+
const drmConfig: any = {};
|
|
539
|
+
|
|
540
|
+
if (this.secureConfig.drm?.widevine) {
|
|
541
|
+
drmConfig['com.widevine.alpha'] = {
|
|
542
|
+
serverUrl: this.secureConfig.drm.widevine.licenseUrl,
|
|
543
|
+
httpRequestHeaders: this.secureConfig.drm.widevine.headers || {}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (this.secureConfig.drm?.playready) {
|
|
548
|
+
drmConfig['com.microsoft.playready'] = {
|
|
549
|
+
serverUrl: this.secureConfig.drm.playready.licenseUrl,
|
|
550
|
+
httpRequestHeaders: this.secureConfig.drm.playready.headers || {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
player.configure({
|
|
555
|
+
drm: {
|
|
556
|
+
servers: drmConfig
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// Store Shaka player instance
|
|
561
|
+
(this as any).shakaPlayer = player;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
protected setupWatermark(): void {
|
|
565
|
+
if (!this.container || !this.video) return;
|
|
566
|
+
|
|
567
|
+
// Create watermark canvas
|
|
568
|
+
const canvas = document.createElement('canvas');
|
|
569
|
+
const context = canvas.getContext('2d');
|
|
570
|
+
|
|
571
|
+
if (!context) return;
|
|
572
|
+
|
|
573
|
+
canvas.style.cssText = `
|
|
574
|
+
position: absolute;
|
|
575
|
+
top: 0;
|
|
576
|
+
left: 0;
|
|
577
|
+
width: 100%;
|
|
578
|
+
height: 100%;
|
|
579
|
+
pointer-events: none;
|
|
580
|
+
z-index: 9999;
|
|
581
|
+
`;
|
|
582
|
+
|
|
583
|
+
this.watermarkLayer = {
|
|
584
|
+
canvas,
|
|
585
|
+
context
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
this.container.style.position = 'relative';
|
|
589
|
+
this.container.appendChild(canvas);
|
|
590
|
+
|
|
591
|
+
// Start watermark rendering
|
|
592
|
+
this.renderWatermark();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private renderWatermark(): void {
|
|
596
|
+
if (!this.watermarkLayer) return;
|
|
597
|
+
|
|
598
|
+
const { canvas, context } = this.watermarkLayer;
|
|
599
|
+
const config = this.secureConfig.watermark;
|
|
600
|
+
|
|
601
|
+
if (!config) return;
|
|
602
|
+
|
|
603
|
+
// Resize canvas to match video
|
|
604
|
+
canvas.width = this.container?.offsetWidth || 0;
|
|
605
|
+
canvas.height = this.container?.offsetHeight || 0;
|
|
606
|
+
|
|
607
|
+
// Clear canvas
|
|
608
|
+
context.clearRect(0, 0, canvas.width, canvas.height);
|
|
609
|
+
|
|
610
|
+
// Prepare watermark text
|
|
611
|
+
const watermarkText = this.buildWatermarkText();
|
|
612
|
+
|
|
613
|
+
// Set text properties
|
|
614
|
+
context.font = `${config.fontSize || 16}px Arial, sans-serif`;
|
|
615
|
+
context.fillStyle = config.fontColor || 'rgba(255, 255, 255, 0.5)';
|
|
616
|
+
context.globalAlpha = config.opacity || 0.5;
|
|
617
|
+
|
|
618
|
+
// Calculate position
|
|
619
|
+
const position = this.calculateWatermarkPosition(context, watermarkText);
|
|
620
|
+
|
|
621
|
+
// Apply blinking effect if enabled
|
|
622
|
+
if (config.blinking) {
|
|
623
|
+
const show = Math.floor(Date.now() / 1000) % 2 === 0;
|
|
624
|
+
if (!show) {
|
|
625
|
+
requestAnimationFrame(() => this.renderWatermark());
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Draw watermark text
|
|
631
|
+
const lines = watermarkText.split('\n');
|
|
632
|
+
lines.forEach((line, index) => {
|
|
633
|
+
context.fillText(line, position.x, position.y + (index * 20));
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Schedule next render
|
|
637
|
+
if (config.moving) {
|
|
638
|
+
setTimeout(() => this.renderWatermark(), config.interval || 3000);
|
|
639
|
+
} else {
|
|
640
|
+
requestAnimationFrame(() => this.renderWatermark());
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private buildWatermarkText(): string {
|
|
645
|
+
const config = this.secureConfig.watermark;
|
|
646
|
+
if (!config) return '';
|
|
647
|
+
|
|
648
|
+
const parts: string[] = [];
|
|
649
|
+
|
|
650
|
+
if (config.text) parts.push(config.text);
|
|
651
|
+
if (config.email) parts.push(config.email);
|
|
652
|
+
if (config.userId) parts.push(`ID: ${config.userId}`);
|
|
653
|
+
if (config.ip) parts.push(`IP: ${config.ip}`);
|
|
654
|
+
|
|
655
|
+
// Add timestamp
|
|
656
|
+
parts.push(new Date().toLocaleString());
|
|
657
|
+
|
|
658
|
+
return parts.join('\n');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private calculateWatermarkPosition(context: CanvasRenderingContext2D, text: string): { x: number, y: number } {
|
|
662
|
+
const config = this.secureConfig.watermark;
|
|
663
|
+
const canvas = this.watermarkLayer?.canvas;
|
|
664
|
+
|
|
665
|
+
if (!config || !canvas) return { x: 0, y: 0 };
|
|
666
|
+
|
|
667
|
+
const metrics = context.measureText(text.split('\n')[0]);
|
|
668
|
+
const textWidth = metrics.width;
|
|
669
|
+
const textHeight = (text.split('\n').length * 20);
|
|
670
|
+
const padding = 20;
|
|
671
|
+
|
|
672
|
+
let x = padding;
|
|
673
|
+
let y = padding + 16; // Account for font baseline
|
|
674
|
+
|
|
675
|
+
switch (config.position) {
|
|
676
|
+
case 'top-right':
|
|
677
|
+
x = canvas.width - textWidth - padding;
|
|
678
|
+
break;
|
|
679
|
+
case 'bottom-left':
|
|
680
|
+
y = canvas.height - textHeight - padding;
|
|
681
|
+
break;
|
|
682
|
+
case 'bottom-right':
|
|
683
|
+
x = canvas.width - textWidth - padding;
|
|
684
|
+
y = canvas.height - textHeight - padding;
|
|
685
|
+
break;
|
|
686
|
+
case 'center':
|
|
687
|
+
x = (canvas.width - textWidth) / 2;
|
|
688
|
+
y = (canvas.height - textHeight) / 2;
|
|
689
|
+
break;
|
|
690
|
+
case 'random':
|
|
691
|
+
x = Math.random() * (canvas.width - textWidth - padding * 2) + padding;
|
|
692
|
+
y = Math.random() * (canvas.height - textHeight - padding * 2) + padding;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return { x, y };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
private setupAnalytics(): void {
|
|
700
|
+
if (!this.secureConfig.analytics?.enabled) return;
|
|
701
|
+
|
|
702
|
+
// Track initial load
|
|
703
|
+
this.trackEvent({
|
|
704
|
+
eventType: 'player_loaded',
|
|
705
|
+
timestamp: Date.now(),
|
|
706
|
+
sessionId: this.sessionId,
|
|
707
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
708
|
+
userId: this.secureConfig.watermark?.userId,
|
|
709
|
+
data: {
|
|
710
|
+
url: window.location.href,
|
|
711
|
+
userAgent: navigator.userAgent,
|
|
712
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
713
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
// Setup periodic analytics reporting
|
|
718
|
+
this.analyticsTimer = window.setInterval(() => {
|
|
719
|
+
this.reportAnalytics();
|
|
720
|
+
}, this.secureConfig.analytics.interval || 30000);
|
|
721
|
+
|
|
722
|
+
// Track video events
|
|
723
|
+
this.setupAnalyticsTracking();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private setupAnalyticsTracking(): void {
|
|
727
|
+
// Track play event
|
|
728
|
+
this.on('onPlay', () => {
|
|
729
|
+
this.watchStartTime = Date.now();
|
|
730
|
+
this.trackEvent({
|
|
731
|
+
eventType: 'play',
|
|
732
|
+
timestamp: Date.now(),
|
|
733
|
+
sessionId: this.sessionId,
|
|
734
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
735
|
+
userId: this.secureConfig.watermark?.userId,
|
|
736
|
+
data: {
|
|
737
|
+
currentTime: this.getCurrentTime(),
|
|
738
|
+
duration: this.getDuration()
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
// Track pause event
|
|
744
|
+
this.on('onPause', () => {
|
|
745
|
+
if (this.watchStartTime > 0) {
|
|
746
|
+
this.totalWatchTime += Date.now() - this.watchStartTime;
|
|
747
|
+
this.watchStartTime = 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
this.trackEvent({
|
|
751
|
+
eventType: 'pause',
|
|
752
|
+
timestamp: Date.now(),
|
|
753
|
+
sessionId: this.sessionId,
|
|
754
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
755
|
+
userId: this.secureConfig.watermark?.userId,
|
|
756
|
+
data: {
|
|
757
|
+
currentTime: this.getCurrentTime(),
|
|
758
|
+
totalWatchTime: this.totalWatchTime
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// Track seek events
|
|
764
|
+
this.on('onSeeking', () => {
|
|
765
|
+
this.lastSeekPosition = this.getCurrentTime();
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
this.on('onSeeked', () => {
|
|
769
|
+
this.trackEvent({
|
|
770
|
+
eventType: 'seek',
|
|
771
|
+
timestamp: Date.now(),
|
|
772
|
+
sessionId: this.sessionId,
|
|
773
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
774
|
+
userId: this.secureConfig.watermark?.userId,
|
|
775
|
+
data: {
|
|
776
|
+
from: this.lastSeekPosition,
|
|
777
|
+
to: this.getCurrentTime()
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Track buffering
|
|
783
|
+
this.on('onBuffering', (isBuffering: boolean) => {
|
|
784
|
+
if (isBuffering) {
|
|
785
|
+
this.bufferingStartTime = Date.now();
|
|
786
|
+
} else if (this.bufferingStartTime > 0) {
|
|
787
|
+
this.totalBufferingTime += Date.now() - this.bufferingStartTime;
|
|
788
|
+
this.bufferingStartTime = 0;
|
|
789
|
+
|
|
790
|
+
this.trackEvent({
|
|
791
|
+
eventType: 'buffering',
|
|
792
|
+
timestamp: Date.now(),
|
|
793
|
+
sessionId: this.sessionId,
|
|
794
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
795
|
+
userId: this.secureConfig.watermark?.userId,
|
|
796
|
+
data: {
|
|
797
|
+
duration: this.totalBufferingTime,
|
|
798
|
+
currentTime: this.getCurrentTime()
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
// Track quality changes
|
|
805
|
+
this.on('onQualityChanged', (quality: Quality) => {
|
|
806
|
+
this.trackEvent({
|
|
807
|
+
eventType: 'quality_change',
|
|
808
|
+
timestamp: Date.now(),
|
|
809
|
+
sessionId: this.sessionId,
|
|
810
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
811
|
+
userId: this.secureConfig.watermark?.userId,
|
|
812
|
+
data: {
|
|
813
|
+
quality: quality.label,
|
|
814
|
+
bitrate: quality.bitrate,
|
|
815
|
+
resolution: `${quality.width}x${quality.height}`
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// Track errors
|
|
821
|
+
this.on('onError', (error: PlayerError) => {
|
|
822
|
+
this.trackEvent({
|
|
823
|
+
eventType: 'error',
|
|
824
|
+
timestamp: Date.now(),
|
|
825
|
+
sessionId: this.sessionId,
|
|
826
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
827
|
+
userId: this.secureConfig.watermark?.userId,
|
|
828
|
+
data: {
|
|
829
|
+
errorCode: error.code,
|
|
830
|
+
errorMessage: error.message,
|
|
831
|
+
errorType: error.type,
|
|
832
|
+
fatal: error.fatal
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Track video ended
|
|
838
|
+
this.on('onEnded', () => {
|
|
839
|
+
if (this.watchStartTime > 0) {
|
|
840
|
+
this.totalWatchTime += Date.now() - this.watchStartTime;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
this.trackEvent({
|
|
844
|
+
eventType: 'ended',
|
|
845
|
+
timestamp: Date.now(),
|
|
846
|
+
sessionId: this.sessionId,
|
|
847
|
+
videoId: ((this.source as any)?.metadata?.id ?? this.source?.metadata?.title),
|
|
848
|
+
userId: this.secureConfig.watermark?.userId,
|
|
849
|
+
data: {
|
|
850
|
+
totalWatchTime: this.totalWatchTime,
|
|
851
|
+
completionRate: (this.getCurrentTime() / this.getDuration()) * 100,
|
|
852
|
+
totalBufferingTime: this.totalBufferingTime
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
private trackEvent(event: AnalyticsEvent): void {
|
|
859
|
+
this.analyticsData.push(event);
|
|
860
|
+
|
|
861
|
+
// Send immediately for critical events
|
|
862
|
+
const criticalEvents = ['error', 'security_warning', 'ended'];
|
|
863
|
+
if (criticalEvents.includes(event.eventType)) {
|
|
864
|
+
this.reportAnalytics();
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
private async reportAnalytics(): Promise<void> {
|
|
869
|
+
if (this.analyticsData.length === 0) return;
|
|
870
|
+
|
|
871
|
+
const endpoint = this.secureConfig.analytics?.endpoint;
|
|
872
|
+
if (!endpoint) return;
|
|
873
|
+
|
|
874
|
+
const events = [...this.analyticsData];
|
|
875
|
+
this.analyticsData = [];
|
|
876
|
+
|
|
877
|
+
try {
|
|
878
|
+
await fetch(`${endpoint}/analytics`, {
|
|
879
|
+
method: 'POST',
|
|
880
|
+
headers: {
|
|
881
|
+
'Content-Type': 'application/json',
|
|
882
|
+
'Authorization': `Bearer ${this.secureConfig.security?.token || ''}`
|
|
883
|
+
},
|
|
884
|
+
body: JSON.stringify({
|
|
885
|
+
sessionId: this.sessionId,
|
|
886
|
+
events,
|
|
887
|
+
metadata: {
|
|
888
|
+
...this.secureConfig.analytics?.customData,
|
|
889
|
+
timestamp: Date.now()
|
|
890
|
+
}
|
|
891
|
+
})
|
|
892
|
+
});
|
|
893
|
+
} catch (error) {
|
|
894
|
+
console.error('Failed to report analytics:', error);
|
|
895
|
+
// Re-add events for retry
|
|
896
|
+
this.analyticsData.unshift(...events);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private setupCustomControls(): void {
|
|
901
|
+
if (!this.secureConfig.features) return;
|
|
902
|
+
|
|
903
|
+
// Create custom controls container
|
|
904
|
+
const controls = document.createElement('div');
|
|
905
|
+
controls.className = 'secure-player-controls';
|
|
906
|
+
controls.style.cssText = `
|
|
907
|
+
position: absolute;
|
|
908
|
+
bottom: 0;
|
|
909
|
+
left: 0;
|
|
910
|
+
right: 0;
|
|
911
|
+
background: linear-gradient(transparent, rgba(0,0,0,0.8));
|
|
912
|
+
padding: 20px;
|
|
913
|
+
display: flex;
|
|
914
|
+
align-items: center;
|
|
915
|
+
gap: 15px;
|
|
916
|
+
z-index: 10000;
|
|
917
|
+
`;
|
|
918
|
+
|
|
919
|
+
// Add quality selector
|
|
920
|
+
if (this.secureConfig.features.qualitySelector) {
|
|
921
|
+
this.createQualitySelector(controls);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Add speed control
|
|
925
|
+
if (this.secureConfig.features.speedControl) {
|
|
926
|
+
this.createSpeedControl(controls);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// Add keyboard shortcuts
|
|
930
|
+
if (this.secureConfig.features.keyboardShortcuts) {
|
|
931
|
+
this.setupKeyboardShortcuts();
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
this.customControls = controls;
|
|
935
|
+
this.container?.appendChild(controls);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private createQualitySelector(container: HTMLElement): void {
|
|
939
|
+
const button = document.createElement('button');
|
|
940
|
+
button.innerHTML = 'Quality';
|
|
941
|
+
button.style.cssText = `
|
|
942
|
+
background: rgba(255,255,255,0.1);
|
|
943
|
+
border: 1px solid rgba(255,255,255,0.3);
|
|
944
|
+
color: white;
|
|
945
|
+
padding: 5px 10px;
|
|
946
|
+
border-radius: 4px;
|
|
947
|
+
cursor: pointer;
|
|
948
|
+
`;
|
|
949
|
+
|
|
950
|
+
const menu = document.createElement('div');
|
|
951
|
+
menu.style.cssText = `
|
|
952
|
+
position: absolute;
|
|
953
|
+
bottom: 100%;
|
|
954
|
+
background: rgba(0,0,0,0.9);
|
|
955
|
+
border: 1px solid rgba(255,255,255,0.3);
|
|
956
|
+
border-radius: 4px;
|
|
957
|
+
padding: 5px 0;
|
|
958
|
+
display: none;
|
|
959
|
+
min-width: 100px;
|
|
960
|
+
`;
|
|
961
|
+
|
|
962
|
+
// Populate quality options
|
|
963
|
+
this.getQualities().forEach((quality, index) => {
|
|
964
|
+
const option = document.createElement('div');
|
|
965
|
+
option.textContent = quality.label;
|
|
966
|
+
option.style.cssText = `
|
|
967
|
+
padding: 5px 15px;
|
|
968
|
+
color: white;
|
|
969
|
+
cursor: pointer;
|
|
970
|
+
`;
|
|
971
|
+
option.addEventListener('click', () => {
|
|
972
|
+
this.setQuality(index);
|
|
973
|
+
menu.style.display = 'none';
|
|
974
|
+
});
|
|
975
|
+
menu.appendChild(option);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
// Add auto option
|
|
979
|
+
const autoOption = document.createElement('div');
|
|
980
|
+
autoOption.textContent = 'Auto';
|
|
981
|
+
autoOption.style.cssText = `
|
|
982
|
+
padding: 5px 15px;
|
|
983
|
+
color: white;
|
|
984
|
+
cursor: pointer;
|
|
985
|
+
border-top: 1px solid rgba(255,255,255,0.3);
|
|
986
|
+
`;
|
|
987
|
+
autoOption.addEventListener('click', () => {
|
|
988
|
+
this.setAutoQuality(true);
|
|
989
|
+
menu.style.display = 'none';
|
|
990
|
+
});
|
|
991
|
+
menu.appendChild(autoOption);
|
|
992
|
+
|
|
993
|
+
button.addEventListener('click', () => {
|
|
994
|
+
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const wrapper = document.createElement('div');
|
|
998
|
+
wrapper.style.position = 'relative';
|
|
999
|
+
wrapper.appendChild(button);
|
|
1000
|
+
wrapper.appendChild(menu);
|
|
1001
|
+
container.appendChild(wrapper);
|
|
1002
|
+
|
|
1003
|
+
this.qualityMenu = menu;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private createSpeedControl(container: HTMLElement): void {
|
|
1007
|
+
const select = document.createElement('select');
|
|
1008
|
+
select.style.cssText = `
|
|
1009
|
+
background: rgba(255,255,255,0.1);
|
|
1010
|
+
border: 1px solid rgba(255,255,255,0.3);
|
|
1011
|
+
color: white;
|
|
1012
|
+
padding: 5px;
|
|
1013
|
+
border-radius: 4px;
|
|
1014
|
+
cursor: pointer;
|
|
1015
|
+
`;
|
|
1016
|
+
|
|
1017
|
+
const speeds = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
|
|
1018
|
+
speeds.forEach(speed => {
|
|
1019
|
+
const option = document.createElement('option');
|
|
1020
|
+
option.value = speed.toString();
|
|
1021
|
+
option.textContent = `${speed}x`;
|
|
1022
|
+
if (speed === 1) option.selected = true;
|
|
1023
|
+
select.appendChild(option);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
select.addEventListener('change', () => {
|
|
1027
|
+
this.setPlaybackRate(parseFloat(select.value));
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
container.appendChild(select);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
protected setupKeyboardShortcuts(): void {
|
|
1034
|
+
document.addEventListener('keydown', (e) => {
|
|
1035
|
+
if (!this.container?.contains(document.activeElement)) return;
|
|
1036
|
+
|
|
1037
|
+
switch (e.key) {
|
|
1038
|
+
case ' ':
|
|
1039
|
+
case 'k':
|
|
1040
|
+
e.preventDefault();
|
|
1041
|
+
this.isPlaying() ? this.pause() : this.play();
|
|
1042
|
+
break;
|
|
1043
|
+
case 'f':
|
|
1044
|
+
e.preventDefault();
|
|
1045
|
+
this.toggleFullscreen();
|
|
1046
|
+
break;
|
|
1047
|
+
case 'm':
|
|
1048
|
+
e.preventDefault();
|
|
1049
|
+
this.toggleMute();
|
|
1050
|
+
break;
|
|
1051
|
+
case 'ArrowLeft':
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
this.seek(this.getCurrentTime() - 10);
|
|
1054
|
+
break;
|
|
1055
|
+
case 'ArrowRight':
|
|
1056
|
+
e.preventDefault();
|
|
1057
|
+
this.seek(this.getCurrentTime() + 10);
|
|
1058
|
+
break;
|
|
1059
|
+
case 'ArrowUp':
|
|
1060
|
+
e.preventDefault();
|
|
1061
|
+
this.setVolume(this.state.volume + 0.1);
|
|
1062
|
+
break;
|
|
1063
|
+
case 'ArrowDown':
|
|
1064
|
+
e.preventDefault();
|
|
1065
|
+
this.setVolume(this.state.volume - 0.1);
|
|
1066
|
+
break;
|
|
1067
|
+
case '0':
|
|
1068
|
+
case '1':
|
|
1069
|
+
case '2':
|
|
1070
|
+
case '3':
|
|
1071
|
+
case '4':
|
|
1072
|
+
case '5':
|
|
1073
|
+
case '6':
|
|
1074
|
+
case '7':
|
|
1075
|
+
case '8':
|
|
1076
|
+
case '9':
|
|
1077
|
+
e.preventDefault();
|
|
1078
|
+
const percent = parseInt(e.key) * 10;
|
|
1079
|
+
this.seek((this.getDuration() * percent) / 100);
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
private startHeartbeat(): void {
|
|
1086
|
+
// Send heartbeat every 30 seconds to maintain session
|
|
1087
|
+
this.heartbeatTimer = window.setInterval(() => {
|
|
1088
|
+
this.sendHeartbeat();
|
|
1089
|
+
}, 30000);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
private async sendHeartbeat(): Promise<void> {
|
|
1093
|
+
const endpoint = this.secureConfig.analytics?.endpoint;
|
|
1094
|
+
if (!endpoint) return;
|
|
1095
|
+
|
|
1096
|
+
try {
|
|
1097
|
+
await fetch(`${endpoint}/heartbeat`, {
|
|
1098
|
+
method: 'POST',
|
|
1099
|
+
headers: {
|
|
1100
|
+
'Content-Type': 'application/json',
|
|
1101
|
+
'Authorization': `Bearer ${this.secureConfig.security?.token || ''}`
|
|
1102
|
+
},
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
sessionId: this.sessionId,
|
|
1105
|
+
timestamp: Date.now(),
|
|
1106
|
+
currentTime: this.getCurrentTime(),
|
|
1107
|
+
playing: this.isPlaying()
|
|
1108
|
+
})
|
|
1109
|
+
});
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
console.error('Heartbeat failed:', error);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private generateSessionId(): string {
|
|
1116
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
1117
|
+
const r = Math.random() * 16 | 0;
|
|
1118
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
1119
|
+
return v.toString(16);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async destroy(): Promise<void> {
|
|
1124
|
+
// Clean up watermark
|
|
1125
|
+
if (this.watermarkLayer) {
|
|
1126
|
+
if (this.watermarkLayer.animationFrame) {
|
|
1127
|
+
cancelAnimationFrame(this.watermarkLayer.animationFrame);
|
|
1128
|
+
}
|
|
1129
|
+
this.watermarkLayer.canvas.remove();
|
|
1130
|
+
this.watermarkLayer = undefined;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// Clean up analytics
|
|
1134
|
+
if (this.analyticsTimer) {
|
|
1135
|
+
clearInterval(this.analyticsTimer);
|
|
1136
|
+
this.reportAnalytics(); // Send final analytics
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Clean up heartbeat
|
|
1140
|
+
if (this.heartbeatTimer) {
|
|
1141
|
+
clearInterval(this.heartbeatTimer);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Clean up screen recording protection
|
|
1145
|
+
if (this.screenRecordingProtection) {
|
|
1146
|
+
this.screenRecordingProtection.disconnect();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Clean up custom controls
|
|
1150
|
+
if (this.customControls) {
|
|
1151
|
+
this.customControls.remove();
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Clean up Shaka player if used
|
|
1155
|
+
if ((this as any).shakaPlayer) {
|
|
1156
|
+
await (this as any).shakaPlayer.destroy();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
await super.destroy();
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Export for use
|
|
1164
|
+
export default SecureVideoPlayer;
|