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,883 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Custom Video Player - Professional UI</title>
7
+
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
17
+ background: #f5f5f5;
18
+ min-height: 100vh;
19
+ display: flex;
20
+ flex-direction: column;
21
+ align-items: center;
22
+ justify-content: center;
23
+ }
24
+
25
+
26
+ .player-wrapper {
27
+ width: 100%;
28
+ max-width: 100%;
29
+ background: #000;
30
+ overflow: hidden;
31
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
32
+ position: relative;
33
+ }
34
+
35
+ .video-container {
36
+ position: relative;
37
+ width: 100%;
38
+ aspect-ratio: 16 / 9;
39
+ height: auto;
40
+ max-height: 100vh;
41
+ background: #000;
42
+ overflow: hidden;
43
+ }
44
+
45
+ #videoPlayer {
46
+ position: absolute;
47
+ top: 0;
48
+ left: 0;
49
+ width: 100%;
50
+ height: 100%;
51
+ background: #000;
52
+ }
53
+
54
+ /* Watermark Overlay */
55
+ .watermark-layer {
56
+ position: absolute;
57
+ top: 0;
58
+ left: 0;
59
+ width: 100%;
60
+ height: 100%;
61
+ pointer-events: none;
62
+ z-index: 5;
63
+ }
64
+
65
+ /* Cast Icon */
66
+ .cast-button {
67
+ position: absolute;
68
+ top: 20px;
69
+ right: 20px;
70
+ width: 36px;
71
+ height: 36px;
72
+ background: rgba(0, 0, 0, 0.6);
73
+ border: 1px solid rgba(255, 255, 255, 0.3);
74
+ border-radius: 4px;
75
+ cursor: pointer;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ z-index: 10;
80
+ transition: all 0.3s ease;
81
+ }
82
+
83
+ .cast-button:hover {
84
+ background: rgba(0, 0, 0, 0.8);
85
+ border-color: rgba(255, 255, 255, 0.5);
86
+ }
87
+
88
+ .cast-button svg {
89
+ width: 20px;
90
+ height: 20px;
91
+ fill: #fff;
92
+ }
93
+
94
+ /* Loading Spinner */
95
+ .loading-spinner {
96
+ position: absolute;
97
+ top: 50%;
98
+ left: 50%;
99
+ transform: translate(-50%, -50%);
100
+ width: 50px;
101
+ height: 50px;
102
+ border: 3px solid rgba(255, 255, 255, 0.2);
103
+ border-top-color: #fff;
104
+ border-radius: 50%;
105
+ animation: spin 0.8s linear infinite;
106
+ display: none;
107
+ z-index: 10;
108
+ }
109
+
110
+ .loading-spinner.active {
111
+ display: block;
112
+ }
113
+
114
+ @keyframes spin {
115
+ to { transform: translate(-50%, -50%) rotate(360deg); }
116
+ }
117
+
118
+ /* Custom Controls Bar */
119
+ .controls-bar {
120
+ position: absolute;
121
+ bottom: 0;
122
+ left: 0;
123
+ right: 0;
124
+ height: 50px;
125
+ background: linear-gradient(to top, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.7));
126
+ display: flex;
127
+ align-items: center;
128
+ padding: 0 15px;
129
+ transition: opacity 0.3s ease;
130
+ z-index: 10;
131
+ }
132
+
133
+ .controls-bar.hidden {
134
+ opacity: 0;
135
+ pointer-events: none;
136
+ }
137
+
138
+ /* Control Buttons */
139
+ .control-btn {
140
+ background: none;
141
+ border: none;
142
+ color: #fff;
143
+ cursor: pointer;
144
+ padding: 0;
145
+ margin: 0 8px;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ transition: opacity 0.2s ease;
150
+ }
151
+
152
+ .control-btn:hover {
153
+ opacity: 0.8;
154
+ }
155
+
156
+ .control-btn svg {
157
+ width: 20px;
158
+ height: 20px;
159
+ fill: #fff;
160
+ }
161
+
162
+ .control-btn.play-pause svg {
163
+ width: 24px;
164
+ height: 24px;
165
+ }
166
+
167
+ /* Progress Bar */
168
+ .progress-container {
169
+ flex: 1;
170
+ margin: 0 15px;
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 10px;
174
+ }
175
+
176
+ .time-display {
177
+ color: #fff;
178
+ font-size: 13px;
179
+ font-weight: 400;
180
+ white-space: nowrap;
181
+ min-width: 90px;
182
+ }
183
+
184
+ .progress-bar {
185
+ flex: 1;
186
+ height: 4px;
187
+ background: rgba(255, 255, 255, 0.2);
188
+ border-radius: 2px;
189
+ position: relative;
190
+ cursor: pointer;
191
+ overflow: hidden;
192
+ }
193
+
194
+ .progress-bar:hover {
195
+ height: 6px;
196
+ }
197
+
198
+ .progress-buffered {
199
+ position: absolute;
200
+ top: 0;
201
+ left: 0;
202
+ height: 100%;
203
+ background: rgba(255, 255, 255, 0.3);
204
+ pointer-events: none;
205
+ }
206
+
207
+ .progress-filled {
208
+ position: absolute;
209
+ top: 0;
210
+ left: 0;
211
+ height: 100%;
212
+ background: #fff;
213
+ pointer-events: none;
214
+ }
215
+
216
+ .progress-handle {
217
+ position: absolute;
218
+ top: 50%;
219
+ transform: translate(-50%, -50%);
220
+ width: 12px;
221
+ height: 12px;
222
+ background: #fff;
223
+ border-radius: 50%;
224
+ opacity: 0;
225
+ transition: opacity 0.2s ease;
226
+ pointer-events: none;
227
+ }
228
+
229
+ .progress-bar:hover .progress-handle {
230
+ opacity: 1;
231
+ }
232
+
233
+ /* Volume Control */
234
+ .volume-control {
235
+ display: flex;
236
+ align-items: center;
237
+ margin-right: 10px;
238
+ }
239
+
240
+ .volume-slider {
241
+ width: 0;
242
+ height: 4px;
243
+ background: rgba(255, 255, 255, 0.2);
244
+ border-radius: 2px;
245
+ margin-left: 10px;
246
+ cursor: pointer;
247
+ position: relative;
248
+ opacity: 0;
249
+ transition: width 0.2s ease, opacity 0.2s ease;
250
+ }
251
+
252
+ .volume-control:hover .volume-slider {
253
+ width: 60px;
254
+ opacity: 1;
255
+ }
256
+
257
+ .volume-fill {
258
+ height: 100%;
259
+ background: #fff;
260
+ border-radius: 2px;
261
+ pointer-events: none;
262
+ }
263
+
264
+ /* Speed Control */
265
+ .speed-control {
266
+ position: relative;
267
+ }
268
+
269
+ .speed-display {
270
+ color: #fff;
271
+ font-size: 13px;
272
+ font-weight: 500;
273
+ cursor: pointer;
274
+ padding: 4px 8px;
275
+ border: 1px solid rgba(255, 255, 255, 0.3);
276
+ border-radius: 4px;
277
+ transition: all 0.2s ease;
278
+ }
279
+
280
+ .speed-display:hover {
281
+ border-color: rgba(255, 255, 255, 0.5);
282
+ background: rgba(255, 255, 255, 0.1);
283
+ }
284
+
285
+ .speed-menu {
286
+ position: absolute;
287
+ bottom: 40px;
288
+ right: 0;
289
+ background: rgba(0, 0, 0, 0.95);
290
+ border: 1px solid rgba(255, 255, 255, 0.2);
291
+ border-radius: 4px;
292
+ padding: 5px 0;
293
+ display: none;
294
+ min-width: 60px;
295
+ }
296
+
297
+ .speed-menu.active {
298
+ display: block;
299
+ }
300
+
301
+ .speed-option {
302
+ color: #fff;
303
+ font-size: 13px;
304
+ padding: 5px 15px;
305
+ cursor: pointer;
306
+ transition: background 0.2s ease;
307
+ }
308
+
309
+ .speed-option:hover {
310
+ background: rgba(255, 255, 255, 0.1);
311
+ }
312
+
313
+ .speed-option.active {
314
+ background: rgba(255, 255, 255, 0.2);
315
+ }
316
+
317
+ /* Quality Control */
318
+ .quality-control {
319
+ position: relative;
320
+ margin: 0 8px;
321
+ }
322
+
323
+ .quality-badge {
324
+ color: #fff;
325
+ font-size: 11px;
326
+ font-weight: 600;
327
+ background: rgba(255, 255, 255, 0.2);
328
+ padding: 3px 8px;
329
+ border-radius: 4px;
330
+ cursor: pointer;
331
+ transition: all 0.2s ease;
332
+ }
333
+
334
+ .quality-badge:hover {
335
+ background: rgba(255, 255, 255, 0.3);
336
+ }
337
+
338
+ /* Fullscreen button */
339
+ .fullscreen-btn svg {
340
+ width: 18px;
341
+ height: 18px;
342
+ }
343
+
344
+ /* Center Play Button */
345
+ .center-play-btn {
346
+ position: absolute;
347
+ top: 50%;
348
+ left: 50%;
349
+ transform: translate(-50%, -50%);
350
+ width: 70px;
351
+ height: 70px;
352
+ background: rgba(0, 0, 0, 0.7);
353
+ border: 2px solid rgba(255, 255, 255, 0.8);
354
+ border-radius: 50%;
355
+ display: flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ cursor: pointer;
359
+ transition: all 0.3s ease;
360
+ z-index: 8;
361
+ }
362
+
363
+ .center-play-btn:hover {
364
+ transform: translate(-50%, -50%) scale(1.1);
365
+ background: rgba(0, 0, 0, 0.8);
366
+ }
367
+
368
+ .center-play-btn.hidden {
369
+ display: none;
370
+ }
371
+
372
+ .center-play-btn svg {
373
+ width: 30px;
374
+ height: 30px;
375
+ fill: #fff;
376
+ margin-left: 3px;
377
+ }
378
+
379
+ /* Hover to show controls */
380
+ .player-wrapper:hover .controls-bar {
381
+ opacity: 1;
382
+ pointer-events: all;
383
+ }
384
+
385
+ .player-wrapper.no-cursor {
386
+ cursor: none;
387
+ }
388
+
389
+ .player-wrapper.no-cursor .controls-bar {
390
+ opacity: 0;
391
+ pointer-events: none;
392
+ }
393
+
394
+ /* Mobile Responsiveness */
395
+ @media (max-width: 768px) {
396
+ .speed-control,
397
+ .quality-control {
398
+ display: none;
399
+ }
400
+
401
+ .control-btn {
402
+ margin: 0 5px;
403
+ }
404
+
405
+ .progress-container {
406
+ margin: 0 10px;
407
+ }
408
+ }
409
+ </style>
410
+ </head>
411
+ <body>
412
+ <div class="player-wrapper" id="playerWrapper">
413
+ <div class="video-container">
414
+ <video id="videoPlayer"></video>
415
+
416
+ <!-- Watermark Canvas -->
417
+ <canvas class="watermark-layer" id="watermarkCanvas"></canvas>
418
+
419
+ <!-- Cast Button -->
420
+ <div class="cast-button" title="Cast">
421
+ <svg viewBox="0 0 24 24">
422
+ <path d="M1 18v3h3c0-1.66-1.34-3-3-3zm0-4v2c2.76 0 5 2.24 5 5h2c0-3.87-3.13-7-7-7zm18-7H5v1.63c3.96 1.28 7.09 4.41 8.37 8.37H19V7zM1 10v2c4.97 0 9 4.03 9 9h2c0-6.08-4.93-11-11-11zm20-7H3c-1.1 0-2 .9-2 2v3h2V5h18v14h-7v2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
423
+ </svg>
424
+ </div>
425
+
426
+ <!-- Loading Spinner -->
427
+ <div class="loading-spinner" id="loadingSpinner"></div>
428
+
429
+ <!-- Center Play Button -->
430
+ <div class="center-play-btn" id="centerPlayBtn">
431
+ <svg viewBox="0 0 24 24">
432
+ <path d="M8 5v14l11-7z"/>
433
+ </svg>
434
+ </div>
435
+
436
+ <!-- Controls Bar -->
437
+ <div class="controls-bar" id="controlsBar">
438
+ <!-- Back Button -->
439
+ <button class="control-btn" id="backBtn" title="Back 10s">
440
+ <svg viewBox="0 0 24 24">
441
+ <path d="M11.99 5V1l-5 5 5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6h-2c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
442
+ </svg>
443
+ </button>
444
+
445
+ <!-- Play/Pause Button -->
446
+ <button class="control-btn play-pause" id="playPauseBtn" title="Play">
447
+ <svg viewBox="0 0 24 24" id="playIcon">
448
+ <path d="M8 5v14l11-7z"/>
449
+ </svg>
450
+ <svg viewBox="0 0 24 24" id="pauseIcon" style="display: none;">
451
+ <path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
452
+ </svg>
453
+ </button>
454
+
455
+ <!-- Forward Button -->
456
+ <button class="control-btn" id="forwardBtn" title="Forward 10s">
457
+ <svg viewBox="0 0 24 24">
458
+ <path d="M12.01 19c-3.31 0-6-2.69-6-6s2.69-6 6-6V5l5 5-5 5V9c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4h2c0 3.31-2.69 6-6 6z"/>
459
+ </svg>
460
+ </button>
461
+
462
+ <!-- Volume Control -->
463
+ <div class="volume-control">
464
+ <button class="control-btn" id="volumeBtn" title="Volume">
465
+ <svg viewBox="0 0 24 24" id="volumeIcon">
466
+ <path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
467
+ </svg>
468
+ <svg viewBox="0 0 24 24" id="volumeMuteIcon" style="display: none;">
469
+ <path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
470
+ </svg>
471
+ </button>
472
+ <div class="volume-slider" id="volumeSlider">
473
+ <div class="volume-fill" id="volumeFill" style="width: 100%;"></div>
474
+ </div>
475
+ </div>
476
+
477
+ <!-- Progress Bar -->
478
+ <div class="progress-container">
479
+ <span class="time-display" id="timeDisplay">00:00 / 00:00</span>
480
+ <div class="progress-bar" id="progressBar">
481
+ <div class="progress-buffered" id="progressBuffered"></div>
482
+ <div class="progress-filled" id="progressFilled"></div>
483
+ <div class="progress-handle" id="progressHandle"></div>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- Speed Control -->
488
+ <div class="speed-control">
489
+ <div class="speed-display" id="speedDisplay">1x</div>
490
+ <div class="speed-menu" id="speedMenu">
491
+ <div class="speed-option" data-speed="0.5">0.5x</div>
492
+ <div class="speed-option" data-speed="0.75">0.75x</div>
493
+ <div class="speed-option active" data-speed="1">1x</div>
494
+ <div class="speed-option" data-speed="1.25">1.25x</div>
495
+ <div class="speed-option" data-speed="1.5">1.5x</div>
496
+ <div class="speed-option" data-speed="2">2x</div>
497
+ </div>
498
+ </div>
499
+
500
+ <!-- Quality Control -->
501
+ <div class="quality-control">
502
+ <div class="quality-badge" id="qualityBadge">HD</div>
503
+ </div>
504
+
505
+ <!-- Fullscreen Button -->
506
+ <button class="control-btn fullscreen-btn" id="fullscreenBtn" title="Fullscreen">
507
+ <svg viewBox="0 0 24 24" id="fullscreenIcon">
508
+ <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
509
+ </svg>
510
+ <svg viewBox="0 0 24 24" id="fullscreenExitIcon" style="display: none;">
511
+ <path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>
512
+ </svg>
513
+ </button>
514
+ </div>
515
+ </div>
516
+ </div>
517
+
518
+ <script>
519
+ class CustomVideoPlayer {
520
+ constructor() {
521
+ this.video = document.getElementById('videoPlayer');
522
+ this.playerWrapper = document.getElementById('playerWrapper');
523
+ this.controlsBar = document.getElementById('controlsBar');
524
+ this.playPauseBtn = document.getElementById('playPauseBtn');
525
+ this.centerPlayBtn = document.getElementById('centerPlayBtn');
526
+ this.progressBar = document.getElementById('progressBar');
527
+ this.progressFilled = document.getElementById('progressFilled');
528
+ this.progressBuffered = document.getElementById('progressBuffered');
529
+ this.progressHandle = document.getElementById('progressHandle');
530
+ this.timeDisplay = document.getElementById('timeDisplay');
531
+ this.volumeBtn = document.getElementById('volumeBtn');
532
+ this.volumeSlider = document.getElementById('volumeSlider');
533
+ this.volumeFill = document.getElementById('volumeFill');
534
+ this.speedDisplay = document.getElementById('speedDisplay');
535
+ this.speedMenu = document.getElementById('speedMenu');
536
+ this.qualityBadge = document.getElementById('qualityBadge');
537
+ this.fullscreenBtn = document.getElementById('fullscreenBtn');
538
+ this.loadingSpinner = document.getElementById('loadingSpinner');
539
+ this.watermarkCanvas = document.getElementById('watermarkCanvas');
540
+
541
+ this.hideControlsTimeout = null;
542
+ this.isPlaying = false;
543
+ this.isDragging = false;
544
+
545
+ this.init();
546
+ }
547
+
548
+ init() {
549
+ this.setupEventListeners();
550
+ this.setupKeyboardShortcuts();
551
+ this.setupWatermark();
552
+
553
+ // Load sample video
554
+ this.loadVideo('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4');
555
+ }
556
+
557
+ setupEventListeners() {
558
+ // Disable right-click context menu on video and player
559
+ this.video.addEventListener('contextmenu', (e) => {
560
+ e.preventDefault();
561
+ return false;
562
+ });
563
+
564
+ this.playerWrapper.addEventListener('contextmenu', (e) => {
565
+ e.preventDefault();
566
+ return false;
567
+ });
568
+
569
+ // Play/Pause
570
+ this.playPauseBtn.addEventListener('click', () => this.togglePlayPause());
571
+ this.centerPlayBtn.addEventListener('click', () => this.togglePlayPause());
572
+ this.video.addEventListener('click', () => this.togglePlayPause());
573
+
574
+ // Video events
575
+ this.video.addEventListener('play', () => this.onPlay());
576
+ this.video.addEventListener('pause', () => this.onPause());
577
+ this.video.addEventListener('timeupdate', () => this.updateProgress());
578
+ this.video.addEventListener('progress', () => this.updateBuffered());
579
+ this.video.addEventListener('loadedmetadata', () => this.onLoadedMetadata());
580
+ this.video.addEventListener('waiting', () => this.showLoading());
581
+ this.video.addEventListener('canplay', () => this.hideLoading());
582
+ this.video.addEventListener('ended', () => this.onEnded());
583
+
584
+ // Progress bar
585
+ this.progressBar.addEventListener('click', (e) => this.seek(e));
586
+ this.progressBar.addEventListener('mousedown', () => this.isDragging = true);
587
+ document.addEventListener('mouseup', () => this.isDragging = false);
588
+ document.addEventListener('mousemove', (e) => {
589
+ if (this.isDragging && e.target === this.progressBar) {
590
+ this.seek(e);
591
+ }
592
+ });
593
+
594
+ // Back/Forward
595
+ document.getElementById('backBtn').addEventListener('click', () => this.skip(-10));
596
+ document.getElementById('forwardBtn').addEventListener('click', () => this.skip(10));
597
+
598
+ // Volume
599
+ this.volumeBtn.addEventListener('click', () => this.toggleMute());
600
+ this.volumeSlider.addEventListener('click', (e) => this.setVolume(e));
601
+
602
+ // Speed
603
+ this.speedDisplay.addEventListener('click', () => {
604
+ this.speedMenu.classList.toggle('active');
605
+ });
606
+
607
+ document.querySelectorAll('.speed-option').forEach(option => {
608
+ option.addEventListener('click', (e) => {
609
+ const speed = parseFloat(e.target.dataset.speed);
610
+ this.setSpeed(speed);
611
+ });
612
+ });
613
+
614
+ // Fullscreen
615
+ this.fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
616
+
617
+ // Hide controls on idle
618
+ this.playerWrapper.addEventListener('mousemove', () => this.showControls());
619
+ this.playerWrapper.addEventListener('mouseleave', () => this.scheduleHideControls());
620
+
621
+ // Hide menus when clicking outside
622
+ document.addEventListener('click', (e) => {
623
+ if (!e.target.closest('.speed-control')) {
624
+ this.speedMenu.classList.remove('active');
625
+ }
626
+ });
627
+ }
628
+
629
+ setupKeyboardShortcuts() {
630
+ document.addEventListener('keydown', (e) => {
631
+ if (e.target.tagName === 'INPUT') return;
632
+
633
+ switch(e.key) {
634
+ case ' ':
635
+ case 'k':
636
+ e.preventDefault();
637
+ this.togglePlayPause();
638
+ break;
639
+ case 'ArrowLeft':
640
+ e.preventDefault();
641
+ this.skip(-10);
642
+ break;
643
+ case 'ArrowRight':
644
+ e.preventDefault();
645
+ this.skip(10);
646
+ break;
647
+ case 'ArrowUp':
648
+ e.preventDefault();
649
+ this.changeVolume(0.1);
650
+ break;
651
+ case 'ArrowDown':
652
+ e.preventDefault();
653
+ this.changeVolume(-0.1);
654
+ break;
655
+ case 'm':
656
+ e.preventDefault();
657
+ this.toggleMute();
658
+ break;
659
+ case 'f':
660
+ e.preventDefault();
661
+ this.toggleFullscreen();
662
+ break;
663
+ case '0':
664
+ case '1':
665
+ case '2':
666
+ case '3':
667
+ case '4':
668
+ case '5':
669
+ case '6':
670
+ case '7':
671
+ case '8':
672
+ case '9':
673
+ e.preventDefault();
674
+ const percent = parseInt(e.key) * 10;
675
+ this.video.currentTime = (this.video.duration * percent) / 100;
676
+ break;
677
+ }
678
+ });
679
+ }
680
+
681
+ setupWatermark() {
682
+ const ctx = this.watermarkCanvas.getContext('2d');
683
+
684
+ const renderWatermark = () => {
685
+ const container = this.watermarkCanvas.parentElement;
686
+ this.watermarkCanvas.width = container.offsetWidth;
687
+ this.watermarkCanvas.height = container.offsetHeight;
688
+
689
+ ctx.clearRect(0, 0, this.watermarkCanvas.width, this.watermarkCanvas.height);
690
+
691
+ // Add watermark text
692
+ ctx.font = '14px Arial';
693
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
694
+ ctx.textAlign = 'left';
695
+
696
+ const text = `User ID: ${Math.random().toString(36).substring(7).toUpperCase()}`;
697
+ const x = 20 + Math.random() * (this.watermarkCanvas.width - 200);
698
+ const y = 40 + Math.random() * (this.watermarkCanvas.height - 80);
699
+
700
+ ctx.fillText(text, x, y);
701
+ ctx.fillText(new Date().toLocaleString(), x, y + 20);
702
+ };
703
+
704
+ // Render watermark periodically
705
+ setInterval(renderWatermark, 5000);
706
+ renderWatermark();
707
+ }
708
+
709
+ loadVideo(url) {
710
+ this.video.src = url;
711
+ this.video.load();
712
+ }
713
+
714
+ togglePlayPause() {
715
+ if (this.video.paused) {
716
+ this.video.play();
717
+ } else {
718
+ this.video.pause();
719
+ }
720
+ }
721
+
722
+ onPlay() {
723
+ this.isPlaying = true;
724
+ document.getElementById('playIcon').style.display = 'none';
725
+ document.getElementById('pauseIcon').style.display = 'block';
726
+ this.centerPlayBtn.classList.add('hidden');
727
+ this.scheduleHideControls();
728
+ }
729
+
730
+ onPause() {
731
+ this.isPlaying = false;
732
+ document.getElementById('playIcon').style.display = 'block';
733
+ document.getElementById('pauseIcon').style.display = 'none';
734
+ this.centerPlayBtn.classList.remove('hidden');
735
+ this.showControls();
736
+ }
737
+
738
+ onEnded() {
739
+ this.onPause();
740
+ this.video.currentTime = 0;
741
+ }
742
+
743
+ onLoadedMetadata() {
744
+ this.updateTimeDisplay();
745
+ }
746
+
747
+ updateProgress() {
748
+ const percent = (this.video.currentTime / this.video.duration) * 100;
749
+ this.progressFilled.style.width = percent + '%';
750
+ this.progressHandle.style.left = percent + '%';
751
+ this.updateTimeDisplay();
752
+ }
753
+
754
+ updateBuffered() {
755
+ if (this.video.buffered.length > 0) {
756
+ const buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
757
+ this.progressBuffered.style.width = buffered + '%';
758
+ }
759
+ }
760
+
761
+ updateTimeDisplay() {
762
+ const current = this.formatTime(this.video.currentTime);
763
+ const duration = this.formatTime(this.video.duration);
764
+ this.timeDisplay.textContent = `${current} / ${duration}`;
765
+ }
766
+
767
+ formatTime(seconds) {
768
+ if (!seconds || isNaN(seconds)) return '00:00';
769
+
770
+ const hours = Math.floor(seconds / 3600);
771
+ const minutes = Math.floor((seconds % 3600) / 60);
772
+ const secs = Math.floor(seconds % 60);
773
+
774
+ if (hours > 0) {
775
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
776
+ } else {
777
+ return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
778
+ }
779
+ }
780
+
781
+ seek(e) {
782
+ const rect = this.progressBar.getBoundingClientRect();
783
+ const percent = (e.clientX - rect.left) / rect.width;
784
+ this.video.currentTime = percent * this.video.duration;
785
+ }
786
+
787
+ skip(seconds) {
788
+ this.video.currentTime = Math.max(0, Math.min(this.video.currentTime + seconds, this.video.duration));
789
+ }
790
+
791
+ toggleMute() {
792
+ this.video.muted = !this.video.muted;
793
+ this.updateVolumeIcon();
794
+ }
795
+
796
+ setVolume(e) {
797
+ const rect = this.volumeSlider.getBoundingClientRect();
798
+ const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
799
+ this.video.volume = percent;
800
+ this.volumeFill.style.width = (percent * 100) + '%';
801
+ this.updateVolumeIcon();
802
+ }
803
+
804
+ changeVolume(delta) {
805
+ this.video.volume = Math.max(0, Math.min(1, this.video.volume + delta));
806
+ this.volumeFill.style.width = (this.video.volume * 100) + '%';
807
+ this.updateVolumeIcon();
808
+ }
809
+
810
+ updateVolumeIcon() {
811
+ const volumeIcon = document.getElementById('volumeIcon');
812
+ const muteIcon = document.getElementById('volumeMuteIcon');
813
+
814
+ if (this.video.muted || this.video.volume === 0) {
815
+ volumeIcon.style.display = 'none';
816
+ muteIcon.style.display = 'block';
817
+ } else {
818
+ volumeIcon.style.display = 'block';
819
+ muteIcon.style.display = 'none';
820
+ }
821
+ }
822
+
823
+ setSpeed(speed) {
824
+ this.video.playbackRate = speed;
825
+ this.speedDisplay.textContent = speed + 'x';
826
+
827
+ // Update active state
828
+ document.querySelectorAll('.speed-option').forEach(option => {
829
+ option.classList.remove('active');
830
+ if (parseFloat(option.dataset.speed) === speed) {
831
+ option.classList.add('active');
832
+ }
833
+ });
834
+
835
+ this.speedMenu.classList.remove('active');
836
+ }
837
+
838
+ toggleFullscreen() {
839
+ if (!document.fullscreenElement) {
840
+ this.playerWrapper.requestFullscreen();
841
+ document.getElementById('fullscreenIcon').style.display = 'none';
842
+ document.getElementById('fullscreenExitIcon').style.display = 'block';
843
+ } else {
844
+ document.exitFullscreen();
845
+ document.getElementById('fullscreenIcon').style.display = 'block';
846
+ document.getElementById('fullscreenExitIcon').style.display = 'none';
847
+ }
848
+ }
849
+
850
+ showControls() {
851
+ clearTimeout(this.hideControlsTimeout);
852
+ this.playerWrapper.classList.remove('no-cursor');
853
+ this.controlsBar.classList.remove('hidden');
854
+ }
855
+
856
+ scheduleHideControls() {
857
+ if (!this.isPlaying) return;
858
+
859
+ clearTimeout(this.hideControlsTimeout);
860
+ this.hideControlsTimeout = setTimeout(() => {
861
+ if (this.isPlaying) {
862
+ this.playerWrapper.classList.add('no-cursor');
863
+ this.controlsBar.classList.add('hidden');
864
+ }
865
+ }, 3000);
866
+ }
867
+
868
+ showLoading() {
869
+ this.loadingSpinner.classList.add('active');
870
+ }
871
+
872
+ hideLoading() {
873
+ this.loadingSpinner.classList.remove('active');
874
+ }
875
+ }
876
+
877
+ // Initialize player when DOM is ready
878
+ document.addEventListener('DOMContentLoaded', () => {
879
+ new CustomVideoPlayer();
880
+ });
881
+ </script>
882
+ </body>
883
+ </html>