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.
Files changed (129) hide show
  1. package/.github/workflows/ci.yml +253 -0
  2. package/ANDROID_TV_IMPLEMENTATION.md +313 -0
  3. package/COMPLETION_STATUS.md +165 -0
  4. package/CONTRIBUTING.md +376 -0
  5. package/FINAL_STATUS_REPORT.md +170 -0
  6. package/FRAMEWORK_REVIEW.md +247 -0
  7. package/IMPROVEMENTS_SUMMARY.md +168 -0
  8. package/LICENSE +21 -0
  9. package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
  10. package/PAYWALL_RENTAL_FLOW.md +499 -0
  11. package/PLATFORM_SETUP_GUIDE.md +1636 -0
  12. package/README.md +315 -0
  13. package/RUN_LOCALLY.md +151 -0
  14. package/apps/demo/cast-sender-min.html +173 -0
  15. package/apps/demo/custom-player.html +883 -0
  16. package/apps/demo/demo.html +990 -0
  17. package/apps/demo/enhanced-player.html +3556 -0
  18. package/apps/demo/index.html +159 -0
  19. package/apps/rental-api/.env.example +24 -0
  20. package/apps/rental-api/README.md +23 -0
  21. package/apps/rental-api/migrations/001_init.sql +35 -0
  22. package/apps/rental-api/migrations/002_videos.sql +10 -0
  23. package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
  24. package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
  25. package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
  26. package/apps/rental-api/package-lock.json +2045 -0
  27. package/apps/rental-api/package.json +33 -0
  28. package/apps/rental-api/scripts/run-migration.js +42 -0
  29. package/apps/rental-api/scripts/update-video-currency.js +21 -0
  30. package/apps/rental-api/scripts/update-video-price.js +19 -0
  31. package/apps/rental-api/src/config.ts +14 -0
  32. package/apps/rental-api/src/db.ts +10 -0
  33. package/apps/rental-api/src/routes/cashfree.ts +167 -0
  34. package/apps/rental-api/src/routes/pesapal.ts +92 -0
  35. package/apps/rental-api/src/routes/rentals.ts +242 -0
  36. package/apps/rental-api/src/routes/webhooks.ts +73 -0
  37. package/apps/rental-api/src/server.ts +41 -0
  38. package/apps/rental-api/src/services/entitlements.ts +45 -0
  39. package/apps/rental-api/src/services/payments.ts +22 -0
  40. package/apps/rental-api/tsconfig.json +17 -0
  41. package/check-urls.ps1 +74 -0
  42. package/comparison-report.md +181 -0
  43. package/docs/PAYWALL.md +95 -0
  44. package/docs/PLAYER_UI_VISIBILITY.md +431 -0
  45. package/docs/README.md +7 -0
  46. package/docs/SYSTEM_ARCHITECTURE.md +612 -0
  47. package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
  48. package/examples/android/JavaSampleApp/MainActivity.java +641 -0
  49. package/examples/android/JavaSampleApp/activity_main.xml +226 -0
  50. package/examples/android/SampleApp/MainActivity.kt +430 -0
  51. package/examples/ios/SampleApp/ViewController.swift +337 -0
  52. package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
  53. package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
  54. package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
  55. package/jest.config.js +33 -0
  56. package/jitpack.yml +5 -0
  57. package/lerna.json +35 -0
  58. package/package.json +69 -0
  59. package/packages/PLATFORM_STATUS.md +163 -0
  60. package/packages/android/build.gradle +135 -0
  61. package/packages/android/src/main/AndroidManifest.xml +36 -0
  62. package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
  63. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
  64. package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
  65. package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
  66. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
  67. package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
  68. package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
  69. package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
  70. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
  71. package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
  72. package/packages/core/package.json +34 -0
  73. package/packages/core/src/BasePlayer.ts +250 -0
  74. package/packages/core/src/VideoPlayer.ts +237 -0
  75. package/packages/core/src/VideoPlayerFactory.ts +145 -0
  76. package/packages/core/src/index.ts +20 -0
  77. package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
  78. package/packages/core/src/interfaces.ts +240 -0
  79. package/packages/core/src/utils/EventEmitter.ts +66 -0
  80. package/packages/core/src/utils/PlatformDetector.ts +300 -0
  81. package/packages/core/tsconfig.json +20 -0
  82. package/packages/enact/package.json +51 -0
  83. package/packages/enact/src/VideoPlayer.js +365 -0
  84. package/packages/enact/src/adapters/TizenAdapter.js +354 -0
  85. package/packages/enact/src/index.js +82 -0
  86. package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
  87. package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
  88. package/packages/ios/GETTING_STARTED.md +100 -0
  89. package/packages/ios/Package.swift +35 -0
  90. package/packages/ios/README.md +84 -0
  91. package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
  92. package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
  93. package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
  94. package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
  95. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
  96. package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
  97. package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
  98. package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
  99. package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
  100. package/packages/ios/build_framework.sh +55 -0
  101. package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
  102. package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
  103. package/packages/react-native/package.json +51 -0
  104. package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
  105. package/packages/react-native/src/VideoPlayer.tsx +224 -0
  106. package/packages/react-native/src/index.ts +28 -0
  107. package/packages/react-native/src/utils/EventEmitter.ts +66 -0
  108. package/packages/react-native/tsconfig.json +31 -0
  109. package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
  110. package/packages/roku/package.json +44 -0
  111. package/packages/roku/source/VideoPlayer.brs +231 -0
  112. package/packages/roku/source/main.brs +28 -0
  113. package/packages/web/GETTING_STARTED.md +292 -0
  114. package/packages/web/jest.config.js +28 -0
  115. package/packages/web/jest.setup.ts +110 -0
  116. package/packages/web/package.json +50 -0
  117. package/packages/web/src/SecureVideoPlayer.ts +1164 -0
  118. package/packages/web/src/WebPlayer.ts +3110 -0
  119. package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
  120. package/packages/web/src/index.ts +14 -0
  121. package/packages/web/src/paywall/PaywallController.ts +215 -0
  122. package/packages/web/src/react/WebPlayerView.tsx +177 -0
  123. package/packages/web/tsconfig.json +23 -0
  124. package/packages/web/webpack.config.js +45 -0
  125. package/server.js +131 -0
  126. package/server.py +84 -0
  127. package/test-urls.ps1 +97 -0
  128. package/test-video-urls.ps1 +87 -0
  129. 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;