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,990 @@
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>Unified Video Framework - Demo</title>
7
+
8
+ <!-- Optional: HLS.js for HLS streaming support (will be loaded dynamically if needed) -->
9
+ <!-- Optional: dash.js for DASH streaming support (will be loaded dynamically if needed) -->
10
+
11
+ <style>
12
+ * {
13
+ margin: 0;
14
+ padding: 0;
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
20
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21
+ min-height: 100vh;
22
+ display: flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ padding: 20px;
26
+ }
27
+
28
+ .container {
29
+ background: white;
30
+ border-radius: 20px;
31
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
32
+ overflow: hidden;
33
+ max-width: 1200px;
34
+ width: 100%;
35
+ }
36
+
37
+ .header {
38
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
39
+ color: white;
40
+ padding: 30px;
41
+ text-align: center;
42
+ }
43
+
44
+ .header h1 {
45
+ font-size: 28px;
46
+ margin-bottom: 10px;
47
+ }
48
+
49
+ .header p {
50
+ opacity: 0.9;
51
+ font-size: 14px;
52
+ }
53
+
54
+ .format-support {
55
+ display: flex;
56
+ gap: 10px;
57
+ justify-content: center;
58
+ margin-top: 10px;
59
+ }
60
+
61
+ .format-badge {
62
+ background: rgba(255,255,255,0.2);
63
+ padding: 4px 12px;
64
+ border-radius: 20px;
65
+ font-size: 12px;
66
+ }
67
+
68
+ .player-section {
69
+ background: #000;
70
+ position: relative;
71
+ padding-top: 56.25%; /* 16:9 aspect ratio */
72
+ }
73
+
74
+ #videoPlayer {
75
+ position: absolute;
76
+ top: 0;
77
+ left: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+
82
+ .controls-section {
83
+ padding: 30px;
84
+ background: #f8f9fa;
85
+ }
86
+
87
+ .control-group {
88
+ margin-bottom: 25px;
89
+ }
90
+
91
+ .control-group h3 {
92
+ font-size: 16px;
93
+ color: #333;
94
+ margin-bottom: 15px;
95
+ font-weight: 600;
96
+ }
97
+
98
+ .button-group {
99
+ display: flex;
100
+ gap: 10px;
101
+ flex-wrap: wrap;
102
+ }
103
+
104
+ button {
105
+ padding: 10px 20px;
106
+ border: none;
107
+ border-radius: 8px;
108
+ font-size: 14px;
109
+ cursor: pointer;
110
+ transition: all 0.3s ease;
111
+ font-weight: 500;
112
+ }
113
+
114
+ .btn-primary {
115
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
116
+ color: white;
117
+ }
118
+
119
+ .btn-primary:hover {
120
+ transform: translateY(-2px);
121
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
122
+ }
123
+
124
+ .btn-secondary {
125
+ background: #e9ecef;
126
+ color: #495057;
127
+ }
128
+
129
+ .btn-secondary:hover {
130
+ background: #dee2e6;
131
+ }
132
+
133
+ .btn-success {
134
+ background: #28a745;
135
+ color: white;
136
+ }
137
+
138
+ .btn-success:hover {
139
+ background: #218838;
140
+ }
141
+
142
+ .input-group {
143
+ display: flex;
144
+ gap: 10px;
145
+ margin-bottom: 15px;
146
+ }
147
+
148
+ input[type="text"] {
149
+ flex: 1;
150
+ padding: 10px 15px;
151
+ border: 2px solid #e9ecef;
152
+ border-radius: 8px;
153
+ font-size: 14px;
154
+ transition: border-color 0.3s ease;
155
+ }
156
+
157
+ input[type="text"]:focus {
158
+ outline: none;
159
+ border-color: #667eea;
160
+ }
161
+
162
+ .status-bar {
163
+ background: white;
164
+ padding: 20px 30px;
165
+ border-top: 1px solid #e9ecef;
166
+ display: flex;
167
+ justify-content: space-between;
168
+ align-items: center;
169
+ flex-wrap: wrap;
170
+ gap: 15px;
171
+ }
172
+
173
+ .status-item {
174
+ display: flex;
175
+ align-items: center;
176
+ gap: 8px;
177
+ }
178
+
179
+ .status-label {
180
+ font-size: 12px;
181
+ color: #6c757d;
182
+ font-weight: 600;
183
+ text-transform: uppercase;
184
+ }
185
+
186
+ .status-value {
187
+ font-size: 14px;
188
+ color: #333;
189
+ font-weight: 500;
190
+ }
191
+
192
+ .sample-videos {
193
+ display: grid;
194
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
195
+ gap: 10px;
196
+ }
197
+
198
+ .sample-btn {
199
+ text-align: left;
200
+ padding: 12px;
201
+ background: white;
202
+ border: 2px solid #e9ecef;
203
+ color: #333;
204
+ position: relative;
205
+ }
206
+
207
+ .sample-btn:hover {
208
+ border-color: #667eea;
209
+ background: #f8f9ff;
210
+ }
211
+
212
+ .sample-btn strong {
213
+ display: block;
214
+ margin-bottom: 4px;
215
+ }
216
+
217
+ .sample-btn small {
218
+ color: #6c757d;
219
+ font-size: 11px;
220
+ }
221
+
222
+ .format-tag {
223
+ position: absolute;
224
+ top: 8px;
225
+ right: 8px;
226
+ background: #667eea;
227
+ color: white;
228
+ padding: 2px 8px;
229
+ border-radius: 4px;
230
+ font-size: 10px;
231
+ font-weight: bold;
232
+ }
233
+
234
+ .volume-control {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 10px;
238
+ }
239
+
240
+ input[type="range"] {
241
+ width: 150px;
242
+ }
243
+
244
+ .quality-selector {
245
+ margin-top: 10px;
246
+ }
247
+
248
+ .quality-buttons {
249
+ display: flex;
250
+ gap: 5px;
251
+ flex-wrap: wrap;
252
+ }
253
+
254
+ .quality-btn {
255
+ padding: 5px 10px;
256
+ font-size: 12px;
257
+ background: white;
258
+ border: 1px solid #dee2e6;
259
+ }
260
+
261
+ .quality-btn.active {
262
+ background: #667eea;
263
+ color: white;
264
+ border-color: #667eea;
265
+ }
266
+
267
+ .message {
268
+ padding: 12px;
269
+ border-radius: 8px;
270
+ margin-bottom: 15px;
271
+ display: none;
272
+ }
273
+
274
+ .message.error {
275
+ background: #f8d7da;
276
+ color: #721c24;
277
+ }
278
+
279
+ .message.success {
280
+ background: #d4edda;
281
+ color: #155724;
282
+ }
283
+
284
+ .message.info {
285
+ background: #d1ecf1;
286
+ color: #0c5460;
287
+ }
288
+
289
+ .toggle-switch {
290
+ display: flex;
291
+ align-items: center;
292
+ gap: 10px;
293
+ margin-bottom: 15px;
294
+ }
295
+
296
+ .switch {
297
+ position: relative;
298
+ display: inline-block;
299
+ width: 50px;
300
+ height: 24px;
301
+ }
302
+
303
+ .switch input {
304
+ opacity: 0;
305
+ width: 0;
306
+ height: 0;
307
+ }
308
+
309
+ .slider {
310
+ position: absolute;
311
+ cursor: pointer;
312
+ top: 0;
313
+ left: 0;
314
+ right: 0;
315
+ bottom: 0;
316
+ background-color: #ccc;
317
+ transition: .4s;
318
+ border-radius: 24px;
319
+ }
320
+
321
+ .slider:before {
322
+ position: absolute;
323
+ content: "";
324
+ height: 16px;
325
+ width: 16px;
326
+ left: 4px;
327
+ bottom: 4px;
328
+ background-color: white;
329
+ transition: .4s;
330
+ border-radius: 50%;
331
+ }
332
+
333
+ input:checked + .slider {
334
+ background-color: #667eea;
335
+ }
336
+
337
+ input:checked + .slider:before {
338
+ transform: translateX(26px);
339
+ }
340
+
341
+ @media (max-width: 768px) {
342
+ .button-group {
343
+ flex-direction: column;
344
+ }
345
+
346
+ button {
347
+ width: 100%;
348
+ }
349
+
350
+ .sample-videos {
351
+ grid-template-columns: 1fr;
352
+ }
353
+ }
354
+ </style>
355
+ </head>
356
+ <body>
357
+ <div class="container">
358
+ <div class="header">
359
+ <h1>🎬 Unified Video Framework</h1>
360
+ <p>Complete Video Player Demo</p>
361
+ <div class="format-support">
362
+ <span class="format-badge">✓ MP4</span>
363
+ <span class="format-badge" id="hlsBadge">✓ HLS</span>
364
+ <span class="format-badge" id="dashBadge">✓ DASH</span>
365
+ <span class="format-badge">✓ WebM</span>
366
+ </div>
367
+ </div>
368
+
369
+ <div class="player-section">
370
+ <video id="videoPlayer" controls playsinline></video>
371
+ </div>
372
+
373
+ <div class="controls-section">
374
+ <div id="messageBox" class="message"></div>
375
+
376
+ <div class="control-group">
377
+ <h3>Settings</h3>
378
+ <div class="toggle-switch">
379
+ <label for="enhancedMode">Enhanced Streaming Mode (HLS/DASH Support)</label>
380
+ <label class="switch">
381
+ <input type="checkbox" id="enhancedMode" checked>
382
+ <span class="slider"></span>
383
+ </label>
384
+ </div>
385
+ <small style="color: #6c757d;">Enable for HLS/DASH streaming support. Disable for lightweight native-only playback.</small>
386
+ </div>
387
+
388
+ <div class="control-group">
389
+ <h3>Load Video</h3>
390
+ <div class="input-group">
391
+ <input type="text" id="videoUrl" placeholder="Enter video URL (MP4, M3U8, MPD, WebM)">
392
+ <button class="btn-primary" onclick="loadVideo()">Load Video</button>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="control-group">
397
+ <h3>Sample Videos</h3>
398
+ <div class="sample-videos">
399
+ <button class="sample-btn" onclick="loadSample('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 'mp4')">
400
+ <span class="format-tag">MP4</span>
401
+ <strong>Big Buck Bunny</strong>
402
+ <small>Progressive Download • 9:56</small>
403
+ </button>
404
+ <button class="sample-btn" onclick="loadSample('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/Sintel.mp4', 'mp4')">
405
+ <span class="format-tag">MP4</span>
406
+ <strong>Sintel</strong>
407
+ <small>Progressive Download • 14:48</small>
408
+ </button>
409
+ <button class="sample-btn" onclick="loadSample('https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8', 'hls')">
410
+ <span class="format-tag">HLS</span>
411
+ <strong>Tears of Steel</strong>
412
+ <small>HLS Adaptive Streaming</small>
413
+ </button>
414
+ <button class="sample-btn" onclick="loadSample('https://demo.unified-streaming.com/k8s/features/stable/video/tears-of-steel/tears-of-steel.ism/.m3u8', 'hls')">
415
+ <span class="format-tag">HLS</span>
416
+ <strong>Tears of Steel (Alt)</strong>
417
+ <small>HLS Multi-bitrate</small>
418
+ </button>
419
+ <button class="sample-btn" onclick="loadSample('https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', 'dash')">
420
+ <span class="format-tag">DASH</span>
421
+ <strong>Big Buck Bunny DASH</strong>
422
+ <small>MPEG-DASH Streaming</small>
423
+ </button>
424
+ <button class="sample-btn" onclick="loadSample('https://dash.akamaized.net/envivio/EnvivioDash3/manifest.mpd', 'dash')">
425
+ <span class="format-tag">DASH</span>
426
+ <strong>Envivio Demo</strong>
427
+ <small>DASH Multi-period</small>
428
+ </button>
429
+ </div>
430
+ </div>
431
+
432
+ <div class="control-group">
433
+ <h3>Playback Controls</h3>
434
+ <div class="button-group">
435
+ <button class="btn-success" onclick="player.play()">▶️ Play</button>
436
+ <button class="btn-secondary" onclick="player.pause()">⏸️ Pause</button>
437
+ <button class="btn-secondary" onclick="player.stop()">⏹️ Stop</button>
438
+ <button class="btn-secondary" onclick="player.seek(Math.max(0, player.getCurrentTime() - 10))">⏪ -10s</button>
439
+ <button class="btn-secondary" onclick="player.seek(player.getCurrentTime() + 10)">⏩ +10s</button>
440
+ </div>
441
+ </div>
442
+
443
+ <div class="control-group" id="qualityControl" style="display: none;">
444
+ <h3>Quality Selection</h3>
445
+ <div class="quality-buttons" id="qualityButtons"></div>
446
+ </div>
447
+
448
+ <div class="control-group">
449
+ <h3>Volume & Display</h3>
450
+ <div class="button-group">
451
+ <div class="volume-control">
452
+ <button class="btn-secondary" onclick="player.toggleMute()">🔇 Toggle Mute</button>
453
+ <input type="range" id="volumeSlider" min="0" max="100" value="100" onchange="player.setVolume(this.value/100)">
454
+ <span id="volumeValue">100%</span>
455
+ </div>
456
+ <button class="btn-secondary" onclick="player.enterPictureInPicture()">📺 PiP</button>
457
+ <button class="btn-secondary" onclick="player.enterFullscreen()">🔳 Fullscreen</button>
458
+ </div>
459
+ </div>
460
+
461
+ <div class="control-group">
462
+ <h3>Playback Speed</h3>
463
+ <div class="button-group">
464
+ <button class="btn-secondary" onclick="player.setPlaybackRate(0.5)">0.5x</button>
465
+ <button class="btn-secondary" onclick="player.setPlaybackRate(0.75)">0.75x</button>
466
+ <button class="btn-primary" onclick="player.setPlaybackRate(1)">1x</button>
467
+ <button class="btn-secondary" onclick="player.setPlaybackRate(1.25)">1.25x</button>
468
+ <button class="btn-secondary" onclick="player.setPlaybackRate(1.5)">1.5x</button>
469
+ <button class="btn-secondary" onclick="player.setPlaybackRate(2)">2x</button>
470
+ </div>
471
+ </div>
472
+ </div>
473
+
474
+ <div class="status-bar">
475
+ <div class="status-item">
476
+ <span class="status-label">State:</span>
477
+ <span class="status-value" id="playerState">Idle</span>
478
+ </div>
479
+ <div class="status-item">
480
+ <span class="status-label">Format:</span>
481
+ <span class="status-value" id="playerFormat">-</span>
482
+ </div>
483
+ <div class="status-item">
484
+ <span class="status-label">Time:</span>
485
+ <span class="status-value" id="playerTime">0:00 / 0:00</span>
486
+ </div>
487
+ <div class="status-item">
488
+ <span class="status-label">Buffered:</span>
489
+ <span class="status-value" id="playerBuffer">0%</span>
490
+ </div>
491
+ <div class="status-item">
492
+ <span class="status-label">Resolution:</span>
493
+ <span class="status-value" id="playerResolution">-</span>
494
+ </div>
495
+ <div class="status-item" id="bitrateItem" style="display: none;">
496
+ <span class="status-label">Bitrate:</span>
497
+ <span class="status-value" id="playerBitrate">-</span>
498
+ </div>
499
+ </div>
500
+ </div>
501
+
502
+ <script>
503
+ // Unified Video Player - Works with or without streaming libraries
504
+ class UnifiedVideoPlayer {
505
+ constructor(videoElement) {
506
+ this.video = videoElement;
507
+ this.state = 'idle';
508
+ this.hls = null;
509
+ this.dash = null;
510
+ this.currentFormat = null;
511
+ this.qualities = [];
512
+ this.enhancedMode = true;
513
+ this.librariesLoaded = false;
514
+ this.setupEventListeners();
515
+ }
516
+
517
+ setupEventListeners() {
518
+ this.video.addEventListener('play', () => this.updateState('playing'));
519
+ this.video.addEventListener('pause', () => this.updateState('paused'));
520
+ this.video.addEventListener('ended', () => this.updateState('ended'));
521
+ this.video.addEventListener('loadstart', () => this.updateState('loading'));
522
+ this.video.addEventListener('canplay', () => this.updateState('ready'));
523
+ this.video.addEventListener('error', (e) => this.handleError(e));
524
+ this.video.addEventListener('waiting', () => this.updateState('buffering'));
525
+
526
+ // Update UI
527
+ this.video.addEventListener('timeupdate', () => this.updateTime());
528
+ this.video.addEventListener('progress', () => this.updateBuffer());
529
+ this.video.addEventListener('loadedmetadata', () => this.updateResolution());
530
+ this.video.addEventListener('volumechange', () => this.updateVolume());
531
+ }
532
+
533
+ async loadStreamingLibraries() {
534
+ if (this.librariesLoaded) return;
535
+
536
+ this.showMessage('Loading streaming libraries...', 'info');
537
+
538
+ try {
539
+ // Load HLS.js
540
+ if (!window.Hls) {
541
+ await this.loadScript('https://cdn.jsdelivr.net/npm/hls.js@latest');
542
+ }
543
+
544
+ // Load dash.js
545
+ if (!window.dashjs) {
546
+ await this.loadScript('https://cdn.dashjs.org/latest/dash.all.min.js');
547
+ }
548
+
549
+ this.librariesLoaded = true;
550
+ this.showMessage('Streaming libraries loaded successfully', 'success');
551
+ } catch (error) {
552
+ this.showMessage('Failed to load streaming libraries: ' + error.message, 'error');
553
+ throw error;
554
+ }
555
+ }
556
+
557
+ loadScript(src) {
558
+ return new Promise((resolve, reject) => {
559
+ const script = document.createElement('script');
560
+ script.src = src;
561
+ script.onload = resolve;
562
+ script.onerror = () => reject(new Error(`Failed to load ${src}`));
563
+ document.head.appendChild(script);
564
+ });
565
+ }
566
+
567
+ async load(url, format = 'auto') {
568
+ // Clean up previous instances
569
+ this.cleanup();
570
+
571
+ // Auto-detect format if not specified
572
+ if (format === 'auto') {
573
+ if (url.includes('.m3u8')) {
574
+ format = 'hls';
575
+ } else if (url.includes('.mpd')) {
576
+ format = 'dash';
577
+ } else {
578
+ format = 'native';
579
+ }
580
+ }
581
+
582
+ this.currentFormat = format;
583
+ document.getElementById('playerFormat').textContent = format.toUpperCase();
584
+
585
+ try {
586
+ // Check if enhanced mode is enabled and we need streaming libraries
587
+ if (this.enhancedMode && (format === 'hls' || format === 'dash')) {
588
+ await this.loadStreamingLibraries();
589
+
590
+ if (format === 'hls') {
591
+ await this.loadHLS(url);
592
+ } else if (format === 'dash') {
593
+ await this.loadDASH(url);
594
+ }
595
+
596
+ document.getElementById('bitrateItem').style.display = 'flex';
597
+ } else {
598
+ // Use native playback
599
+ await this.loadNative(url);
600
+ document.getElementById('bitrateItem').style.display = 'none';
601
+ }
602
+
603
+ this.showMessage(`Successfully loaded ${format.toUpperCase()} video`, 'success');
604
+ } catch (error) {
605
+ this.showMessage(`Failed to load video: ${error.message}`, 'error');
606
+ console.error('Load error:', error);
607
+ }
608
+ }
609
+
610
+ async loadHLS(url) {
611
+ if (!window.Hls) {
612
+ throw new Error('HLS.js library not loaded');
613
+ }
614
+
615
+ if (Hls.isSupported()) {
616
+ this.hls = new Hls({
617
+ debug: false,
618
+ enableWorker: true,
619
+ lowLatencyMode: true,
620
+ backBufferLength: 90
621
+ });
622
+
623
+ this.hls.loadSource(url);
624
+ this.hls.attachMedia(this.video);
625
+
626
+ this.hls.on(Hls.Events.MANIFEST_PARSED, (event, data) => {
627
+ console.log('HLS manifest loaded, found ' + data.levels.length + ' quality levels');
628
+
629
+ // Store quality levels
630
+ this.qualities = data.levels.map((level, index) => ({
631
+ index: index,
632
+ height: level.height,
633
+ bitrate: level.bitrate,
634
+ label: `${level.height}p${level.frameRate ? '@' + Math.round(level.frameRate) + 'fps' : ''}`
635
+ }));
636
+
637
+ this.updateQualityButtons();
638
+
639
+ // Start playback
640
+ this.video.play().catch(e => console.log('Autoplay prevented'));
641
+ });
642
+
643
+ this.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
644
+ const level = this.hls.levels[data.level];
645
+ if (level) {
646
+ document.getElementById('playerBitrate').textContent =
647
+ `${Math.round(level.bitrate / 1000)} kbps`;
648
+ }
649
+ });
650
+
651
+ this.hls.on(Hls.Events.ERROR, (event, data) => {
652
+ if (data.fatal) {
653
+ switch (data.type) {
654
+ case Hls.ErrorTypes.NETWORK_ERROR:
655
+ console.error('Fatal network error encountered, trying to recover');
656
+ this.hls.startLoad();
657
+ break;
658
+ case Hls.ErrorTypes.MEDIA_ERROR:
659
+ console.error('Fatal media error encountered, trying to recover');
660
+ this.hls.recoverMediaError();
661
+ break;
662
+ default:
663
+ console.error('Fatal error, cannot recover');
664
+ this.hls.destroy();
665
+ this.showMessage('HLS Fatal Error: ' + data.details, 'error');
666
+ break;
667
+ }
668
+ }
669
+ });
670
+ } else if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
671
+ // Native HLS support (Safari)
672
+ this.video.src = url;
673
+ this.video.addEventListener('loadedmetadata', () => {
674
+ this.video.play().catch(e => console.log('Autoplay prevented'));
675
+ });
676
+ } else {
677
+ throw new Error('HLS is not supported in this browser');
678
+ }
679
+ }
680
+
681
+ async loadDASH(url) {
682
+ if (!window.dashjs) {
683
+ throw new Error('dash.js library not loaded');
684
+ }
685
+
686
+ this.dash = dashjs.MediaPlayer().create();
687
+ this.dash.initialize(this.video, url, false);
688
+
689
+ // Configure DASH settings
690
+ this.dash.updateSettings({
691
+ streaming: {
692
+ abr: {
693
+ autoSwitchBitrate: {
694
+ video: true,
695
+ audio: true
696
+ }
697
+ },
698
+ buffer: {
699
+ fastSwitchEnabled: true
700
+ },
701
+ gaps: {
702
+ jumpGaps: true
703
+ }
704
+ }
705
+ });
706
+
707
+ this.dash.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => {
708
+ console.log('DASH stream initialized');
709
+
710
+ // Get quality levels
711
+ setTimeout(() => {
712
+ try {
713
+ const bitrateList = this.dash.getBitrateInfoListFor && this.dash.getBitrateInfoListFor('video');
714
+
715
+ if (bitrateList && bitrateList.length > 0) {
716
+ this.qualities = bitrateList.map((info, index) => ({
717
+ index: index,
718
+ height: info.height || 0,
719
+ bitrate: info.bitrate || info.bandwidth || 0,
720
+ label: `${info.height || 'Quality ' + (index + 1)}p`
721
+ }));
722
+ this.updateQualityButtons();
723
+ }
724
+ } catch (error) {
725
+ console.log('Quality selection not available for this DASH stream');
726
+ }
727
+ }, 1000);
728
+
729
+ this.video.play().catch(e => console.log('Autoplay prevented'));
730
+ });
731
+
732
+ this.dash.on(dashjs.MediaPlayer.events.QUALITY_CHANGE_RENDERED, (e) => {
733
+ if (e.mediaType === 'video' && e.newQuality !== undefined) {
734
+ try {
735
+ const currentQuality = this.qualities[e.newQuality];
736
+ if (currentQuality) {
737
+ document.getElementById('playerBitrate').textContent =
738
+ `${Math.round(currentQuality.bitrate / 1000)} kbps`;
739
+ }
740
+ } catch (error) {
741
+ console.warn('Could not update bitrate display:', error);
742
+ }
743
+ }
744
+ });
745
+
746
+ this.dash.on(dashjs.MediaPlayer.events.ERROR, (e) => {
747
+ console.error('DASH error:', e);
748
+ this.showMessage('DASH Error: ' + e.error.message, 'error');
749
+ });
750
+ }
751
+
752
+ async loadNative(url) {
753
+ this.video.src = url;
754
+ this.video.load();
755
+ this.video.play().catch(e => console.log('Autoplay prevented'));
756
+ }
757
+
758
+ updateQualityButtons() {
759
+ const container = document.getElementById('qualityButtons');
760
+ const qualityControl = document.getElementById('qualityControl');
761
+
762
+ if (this.qualities.length > 0) {
763
+ qualityControl.style.display = 'block';
764
+ container.innerHTML = '';
765
+
766
+ // Add Auto button
767
+ const autoBtn = document.createElement('button');
768
+ autoBtn.className = 'quality-btn btn-secondary active';
769
+ autoBtn.textContent = 'Auto';
770
+ autoBtn.onclick = () => this.setQuality(-1);
771
+ container.appendChild(autoBtn);
772
+
773
+ // Add quality buttons
774
+ this.qualities.forEach((quality, index) => {
775
+ const btn = document.createElement('button');
776
+ btn.className = 'quality-btn btn-secondary';
777
+ btn.textContent = quality.label;
778
+ btn.onclick = () => this.setQuality(index);
779
+ container.appendChild(btn);
780
+ });
781
+ } else {
782
+ qualityControl.style.display = 'none';
783
+ }
784
+ }
785
+
786
+ setQuality(index) {
787
+ // Update button states
788
+ const buttons = document.querySelectorAll('.quality-btn');
789
+ buttons.forEach((btn, i) => {
790
+ btn.classList.toggle('active', i === index + 1);
791
+ });
792
+
793
+ if (this.hls) {
794
+ this.hls.currentLevel = index;
795
+ } else if (this.dash) {
796
+ if (index === -1) {
797
+ this.dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: true } } } });
798
+ } else {
799
+ this.dash.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } });
800
+ this.dash.setQualityFor('video', index);
801
+ }
802
+ }
803
+ }
804
+
805
+ cleanup() {
806
+ if (this.hls) {
807
+ this.hls.destroy();
808
+ this.hls = null;
809
+ }
810
+ if (this.dash) {
811
+ this.dash.reset();
812
+ this.dash = null;
813
+ }
814
+ this.qualities = [];
815
+ document.getElementById('qualityControl').style.display = 'none';
816
+ document.getElementById('bitrateItem').style.display = 'none';
817
+ }
818
+
819
+ play() {
820
+ this.video.play().catch(e => {
821
+ console.error('Play failed:', e);
822
+ this.showMessage('Playback failed: ' + e.message, 'error');
823
+ });
824
+ }
825
+
826
+ pause() {
827
+ this.video.pause();
828
+ }
829
+
830
+ stop() {
831
+ this.pause();
832
+ this.video.currentTime = 0;
833
+ }
834
+
835
+ seek(time) {
836
+ this.video.currentTime = Math.max(0, Math.min(time, this.video.duration || 0));
837
+ }
838
+
839
+ setVolume(level) {
840
+ this.video.volume = Math.max(0, Math.min(1, level));
841
+ }
842
+
843
+ toggleMute() {
844
+ this.video.muted = !this.video.muted;
845
+ }
846
+
847
+ setPlaybackRate(rate) {
848
+ this.video.playbackRate = rate;
849
+ }
850
+
851
+ getCurrentTime() {
852
+ return this.video.currentTime;
853
+ }
854
+
855
+ enterFullscreen() {
856
+ if (this.video.requestFullscreen) {
857
+ this.video.requestFullscreen();
858
+ } else if (this.video.webkitRequestFullscreen) {
859
+ this.video.webkitRequestFullscreen();
860
+ }
861
+ }
862
+
863
+ enterPictureInPicture() {
864
+ if (this.video.requestPictureInPicture) {
865
+ this.video.requestPictureInPicture().catch(e => {
866
+ console.error('PiP failed:', e);
867
+ this.showMessage('Picture-in-Picture not available', 'error');
868
+ });
869
+ }
870
+ }
871
+
872
+ handleError(e) {
873
+ this.updateState('error');
874
+ const error = this.video.error;
875
+ if (error) {
876
+ const message = `Video Error: ${error.message || 'Unknown error'}`;
877
+ console.error(message, error);
878
+ this.showMessage(message, 'error');
879
+ }
880
+ }
881
+
882
+ showMessage(text, type = 'info') {
883
+ const messageBox = document.getElementById('messageBox');
884
+ messageBox.textContent = text;
885
+ messageBox.className = `message ${type}`;
886
+ messageBox.style.display = 'block';
887
+
888
+ if (type !== 'error') {
889
+ setTimeout(() => {
890
+ messageBox.style.display = 'none';
891
+ }, 3000);
892
+ }
893
+ }
894
+
895
+ updateState(state) {
896
+ this.state = state;
897
+ document.getElementById('playerState').textContent = state.charAt(0).toUpperCase() + state.slice(1);
898
+ }
899
+
900
+ updateTime() {
901
+ const current = this.formatTime(this.video.currentTime);
902
+ const duration = this.formatTime(this.video.duration);
903
+ document.getElementById('playerTime').textContent = `${current} / ${duration}`;
904
+ }
905
+
906
+ updateBuffer() {
907
+ if (this.video.buffered.length > 0) {
908
+ const bufferedEnd = this.video.buffered.end(this.video.buffered.length - 1);
909
+ const duration = this.video.duration;
910
+ const percent = duration ? (bufferedEnd / duration * 100).toFixed(1) : 0;
911
+ document.getElementById('playerBuffer').textContent = `${percent}%`;
912
+ }
913
+ }
914
+
915
+ updateResolution() {
916
+ const width = this.video.videoWidth;
917
+ const height = this.video.videoHeight;
918
+ document.getElementById('playerResolution').textContent =
919
+ width && height ? `${width}x${height}` : '-';
920
+ }
921
+
922
+ updateVolume() {
923
+ const volume = Math.round(this.video.volume * 100);
924
+ document.getElementById('volumeValue').textContent = `${volume}%`;
925
+ document.getElementById('volumeSlider').value = volume;
926
+ }
927
+
928
+ formatTime(seconds) {
929
+ if (!isFinite(seconds)) return '0:00';
930
+ const mins = Math.floor(seconds / 60);
931
+ const secs = Math.floor(seconds % 60);
932
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
933
+ }
934
+
935
+ setEnhancedMode(enabled) {
936
+ this.enhancedMode = enabled;
937
+ const hlsBadge = document.getElementById('hlsBadge');
938
+ const dashBadge = document.getElementById('dashBadge');
939
+
940
+ if (enabled) {
941
+ hlsBadge.textContent = '✓ HLS';
942
+ dashBadge.textContent = '✓ DASH';
943
+ this.showMessage('Enhanced mode enabled - Full streaming support', 'success');
944
+ } else {
945
+ hlsBadge.textContent = '○ HLS';
946
+ dashBadge.textContent = '○ DASH';
947
+ this.showMessage('Enhanced mode disabled - Native playback only', 'info');
948
+ }
949
+ }
950
+ }
951
+
952
+ // Initialize player
953
+ const player = new UnifiedVideoPlayer(document.getElementById('videoPlayer'));
954
+
955
+ // Setup enhanced mode toggle
956
+ document.getElementById('enhancedMode').addEventListener('change', (e) => {
957
+ player.setEnhancedMode(e.target.checked);
958
+ });
959
+
960
+ // Helper functions
961
+ function loadVideo() {
962
+ const url = document.getElementById('videoUrl').value.trim();
963
+ if (url) {
964
+ player.load(url, 'auto');
965
+ } else {
966
+ alert('Please enter a video URL');
967
+ }
968
+ }
969
+
970
+ function loadSample(url, format) {
971
+ document.getElementById('videoUrl').value = url;
972
+ player.load(url, format);
973
+ }
974
+
975
+ // Auto-load first sample on page load
976
+ window.addEventListener('load', () => {
977
+ // Check if libraries are available
978
+ if (window.Hls) {
979
+ console.log('HLS.js pre-loaded, version:', Hls.version);
980
+ }
981
+ if (window.dashjs) {
982
+ console.log('dash.js pre-loaded, version:', dashjs.Version);
983
+ }
984
+
985
+ // Load a default video
986
+ loadSample('https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4', 'native');
987
+ });
988
+ </script>
989
+ </body>
990
+ </html>