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,231 @@
1
+ ' ********************************************************
2
+ ' ** Unified Video Framework - Roku Implementation
3
+ ' ** VideoPlayer.brs - Main video player component
4
+ ' ********************************************************
5
+
6
+ ' Initialize the video player component
7
+ Function VideoPlayer_Init() as Object
8
+ this = {
9
+ ' Component properties
10
+ videoContent: CreateObject("roSGNode", "ContentNode")
11
+ video: invalid
12
+ state: "idle"
13
+ config: {}
14
+
15
+ ' Public methods
16
+ Load: VideoPlayer_Load
17
+ Play: VideoPlayer_Play
18
+ Pause: VideoPlayer_Pause
19
+ Stop: VideoPlayer_Stop
20
+ Seek: VideoPlayer_Seek
21
+ SetVolume: VideoPlayer_SetVolume
22
+ GetCurrentTime: VideoPlayer_GetCurrentTime
23
+ GetDuration: VideoPlayer_GetDuration
24
+ GetState: VideoPlayer_GetState
25
+ Destroy: VideoPlayer_Destroy
26
+
27
+ ' Event handling
28
+ SetEventCallback: VideoPlayer_SetEventCallback
29
+ eventCallback: invalid
30
+ }
31
+
32
+ ' Create the video node
33
+ this.video = CreateObject("roSGNode", "Video")
34
+ this.video.observeField("state", "OnVideoStateChange")
35
+ this.video.observeField("position", "OnPositionChange")
36
+ this.video.observeField("duration", "OnDurationChange")
37
+
38
+ return this
39
+ End Function
40
+
41
+ ' Load a video source
42
+ Function VideoPlayer_Load(source as Object) as Void
43
+ m.videoContent.url = source.url
44
+ m.videoContent.title = source.title
45
+
46
+ ' Set streaming format
47
+ if source.type = "application/x-mpegURL" then
48
+ m.videoContent.streamFormat = "hls"
49
+ else if source.type = "application/dash+xml" then
50
+ m.videoContent.streamFormat = "dash"
51
+ else
52
+ m.videoContent.streamFormat = "mp4"
53
+ end if
54
+
55
+ ' Handle DRM if present
56
+ if source.drm <> invalid then
57
+ VideoPlayer_SetupDRM(source.drm)
58
+ end if
59
+
60
+ ' Set subtitles if present
61
+ if source.subtitles <> invalid then
62
+ VideoPlayer_SetupSubtitles(source.subtitles)
63
+ end if
64
+
65
+ m.video.content = m.videoContent
66
+ m.state = "loaded"
67
+
68
+ ' Trigger ready event
69
+ if m.eventCallback <> invalid then
70
+ m.eventCallback("ready", {})
71
+ end if
72
+ End Function
73
+
74
+ ' Start playback
75
+ Function VideoPlayer_Play() as Void
76
+ if m.state = "loaded" or m.state = "paused" then
77
+ m.video.control = "play"
78
+ m.state = "playing"
79
+
80
+ if m.eventCallback <> invalid then
81
+ m.eventCallback("play", {})
82
+ end if
83
+ end if
84
+ End Function
85
+
86
+ ' Pause playback
87
+ Function VideoPlayer_Pause() as Void
88
+ if m.state = "playing" then
89
+ m.video.control = "pause"
90
+ m.state = "paused"
91
+
92
+ if m.eventCallback <> invalid then
93
+ m.eventCallback("pause", {})
94
+ end if
95
+ end if
96
+ End Function
97
+
98
+ ' Stop playback
99
+ Function VideoPlayer_Stop() as Void
100
+ m.video.control = "stop"
101
+ m.state = "stopped"
102
+
103
+ if m.eventCallback <> invalid then
104
+ m.eventCallback("stop", {})
105
+ end if
106
+ End Function
107
+
108
+ ' Seek to position (in seconds)
109
+ Function VideoPlayer_Seek(position as Float) as Void
110
+ m.video.seek = position
111
+
112
+ if m.eventCallback <> invalid then
113
+ m.eventCallback("seeking", {position: position})
114
+ end if
115
+ End Function
116
+
117
+ ' Set volume (0.0 to 1.0)
118
+ Function VideoPlayer_SetVolume(volume as Float) as Void
119
+ ' Roku doesn't have direct volume control from apps
120
+ ' This would typically be handled by the system
121
+ print "Volume control is handled by system"
122
+ End Function
123
+
124
+ ' Get current playback position in seconds
125
+ Function VideoPlayer_GetCurrentTime() as Float
126
+ return m.video.position
127
+ End Function
128
+
129
+ ' Get video duration in seconds
130
+ Function VideoPlayer_GetDuration() as Float
131
+ return m.video.duration
132
+ End Function
133
+
134
+ ' Get current player state
135
+ Function VideoPlayer_GetState() as String
136
+ return m.state
137
+ End Function
138
+
139
+ ' Clean up resources
140
+ Function VideoPlayer_Destroy() as Void
141
+ m.video.control = "stop"
142
+ m.video = invalid
143
+ m.videoContent = invalid
144
+ m.state = "idle"
145
+ End Function
146
+
147
+ ' Set event callback function
148
+ Function VideoPlayer_SetEventCallback(callback as Function) as Void
149
+ m.eventCallback = callback
150
+ End Function
151
+
152
+ ' Setup DRM configuration
153
+ Function VideoPlayer_SetupDRM(drm as Object) as Void
154
+ if drm.type = "playready" then
155
+ m.videoContent.drmParams = {
156
+ licenseServerURL: drm.licenseUrl
157
+ serializationURL: drm.certificateUrl
158
+ }
159
+ else if drm.type = "widevine" then
160
+ m.videoContent.drmParams = {
161
+ keySystem: "Widevine"
162
+ licenseServerURL: drm.licenseUrl
163
+ }
164
+ end if
165
+ End Function
166
+
167
+ ' Setup subtitles
168
+ Function VideoPlayer_SetupSubtitles(subtitles as Object) as Void
169
+ subtitleTracks = []
170
+
171
+ for each subtitle in subtitles
172
+ track = {
173
+ url: subtitle.url
174
+ language: subtitle.language
175
+ name: subtitle.label
176
+ }
177
+ subtitleTracks.push(track)
178
+ end for
179
+
180
+ m.videoContent.subtitleTracks = subtitleTracks
181
+ End Function
182
+
183
+ ' Handle video state changes
184
+ Sub OnVideoStateChange()
185
+ state = m.video.state
186
+
187
+ if state = "playing" then
188
+ m.state = "playing"
189
+ if m.eventCallback <> invalid then
190
+ m.eventCallback("playing", {})
191
+ end if
192
+ else if state = "paused" then
193
+ m.state = "paused"
194
+ else if state = "buffering" then
195
+ if m.eventCallback <> invalid then
196
+ m.eventCallback("buffering", {})
197
+ end if
198
+ else if state = "error" then
199
+ m.state = "error"
200
+ if m.eventCallback <> invalid then
201
+ m.eventCallback("error", {
202
+ code: m.video.errorCode
203
+ message: m.video.errorMsg
204
+ })
205
+ end if
206
+ else if state = "finished" then
207
+ m.state = "ended"
208
+ if m.eventCallback <> invalid then
209
+ m.eventCallback("ended", {})
210
+ end if
211
+ end if
212
+ End Sub
213
+
214
+ ' Handle position changes
215
+ Sub OnPositionChange()
216
+ if m.eventCallback <> invalid then
217
+ m.eventCallback("timeupdate", {
218
+ currentTime: m.video.position
219
+ duration: m.video.duration
220
+ })
221
+ end if
222
+ End Sub
223
+
224
+ ' Handle duration changes
225
+ Sub OnDurationChange()
226
+ if m.eventCallback <> invalid then
227
+ m.eventCallback("durationchange", {
228
+ duration: m.video.duration
229
+ })
230
+ end if
231
+ End Sub
@@ -0,0 +1,28 @@
1
+ ' Main entry point for Roku Unified Video Player
2
+ sub Main(args as Dynamic)
3
+ ' Initialize the screen
4
+ screen = CreateObject("roSGScreen")
5
+ m.port = CreateObject("roMessagePort")
6
+ screen.setMessagePort(m.port)
7
+
8
+ ' Create the main scene
9
+ scene = screen.CreateScene("MainScene")
10
+ screen.show()
11
+
12
+ ' Pass launch arguments to scene
13
+ if args <> invalid and args.contentId <> invalid then
14
+ scene.launchArgs = args
15
+ end if
16
+
17
+ ' Main event loop
18
+ while true
19
+ msg = wait(0, m.port)
20
+ msgType = type(msg)
21
+
22
+ if msgType = "roSGScreenEvent" then
23
+ if msg.isScreenClosed() then
24
+ return
25
+ end if
26
+ end if
27
+ end while
28
+ end sub
@@ -0,0 +1,292 @@
1
+ # Getting Started
2
+
3
+ ## Install
4
+
5
+ ### npm
6
+ ```bash
7
+ npm i @unified-video/web
8
+ # Optional (recommended for production control of adaptive streaming):
9
+ npm i hls.js dashjs
10
+ ```
11
+
12
+ ### yarn
13
+ ```bash
14
+ yarn add @unified-video/web
15
+ # Optional:
16
+ yarn add hls.js dashjs
17
+ ```
18
+
19
+ ### pnpm
20
+ ```bash
21
+ pnpm add @unified-video/web
22
+ # Optional:
23
+ pnpm add hls.js dashjs
24
+ ```
25
+
26
+ > Notes
27
+ > - hls.js and dashjs are optional peer dependencies. If you don’t install them, the player can load them from public CDNs at runtime. For production builds and offline environments, install them as shown above.
28
+ > - TypeScript types are bundled—no extra @types package needed.
29
+
30
+ ## Quick start (bundler)
31
+ ```ts
32
+ import { WebPlayer } from '@unified-video/web';
33
+
34
+ const player = new WebPlayer();
35
+
36
+ async function main() {
37
+ // Attach the player to a container (element or selector)
38
+ await player.initialize('#player', {
39
+ autoPlay: false,
40
+ muted: false,
41
+ enableAdaptiveBitrate: true, // allows auto quality with HLS/DASH
42
+ debug: false,
43
+ });
44
+
45
+ // Load a source (mp4, hls, dash, webm)
46
+ await player.load({
47
+ url: 'https://example.com/stream.m3u8',
48
+ type: 'hls', // 'mp4' | 'hls' | 'dash' | 'webm' | 'auto'
49
+ subtitles: [
50
+ {
51
+ url: 'https://example.com/subs/en.vtt',
52
+ language: 'en',
53
+ label: 'English',
54
+ kind: 'subtitles',
55
+ default: true,
56
+ },
57
+ ],
58
+ metadata: {
59
+ title: 'My Stream',
60
+ description: 'An example stream with metadata-driven UI',
61
+ thumbnailUrl: 'https://example.com/thumb.jpg',
62
+ posterUrl: 'https://example.com/poster.jpg'
63
+ },
64
+ });
65
+
66
+ await player.play();
67
+ }
68
+
69
+ main().catch(console.error);
70
+ ```
71
+
72
+ ### Container markup
73
+ ```html
74
+ <div id="player" style="width: 100%; max-width: 960px; margin: 0 auto;"></div>
75
+ ```
76
+
77
+ ### Chromecast (optional)
78
+ ```html
79
+ <!-- Add this if you want Cast sender support in the browser -->
80
+ <script src="https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1" async></script>
81
+ ```
82
+
83
+ ## Enhanced UI and Dynamic Theming
84
+ The WebPlayer renders a premium UI (gradient border, top action buttons, animated center play, watermark, modern control bar) and supports live theming via CSS variables.
85
+
86
+ Set a theme at runtime (after initialize and before/after load):
87
+ ```ts
88
+ // Single accent (accent2 is derived)
89
+ player.setTheme('#00bcd4');
90
+
91
+ // Full theme object
92
+ player.setTheme({
93
+ accent: '#ff0000',
94
+ accent2: '#ff4d4f',
95
+ iconColor: '#ffffff',
96
+ textPrimary: '#ffffff',
97
+ textSecondary: 'rgba(255,255,255,0.75)',
98
+ });
99
+ ```
100
+
101
+ ### Theme variables
102
+ - `--uvf-accent-1`, `--uvf-accent-2` (accent gradient)
103
+ - `--uvf-accent-1-20` (translucent accent for glow/badges)
104
+ - `--uvf-icon-color`
105
+ - `--uvf-text-primary`, `--uvf-text-secondary`
106
+ - Settings menu scrollbar tuning:
107
+ - `--uvf-scrollbar-width`
108
+ - `--uvf-scrollbar-thumb-start`, `--uvf-scrollbar-thumb-end`
109
+ - `--uvf-scrollbar-thumb-hover-start`, `--uvf-scrollbar-thumb-hover-end`
110
+
111
+ You can also adjust scrollbar behavior programmatically:
112
+ ```ts
113
+ player.setSettingsScrollbarStyle('compact'); // 'default' | 'compact' | 'overlay'
114
+ player.setSettingsScrollbarConfig({ widthPx: 6, intensity: 1 });
115
+ ```
116
+
117
+ ## Metadata-driven UI (WebPlayer)
118
+ - The built-in title bar is shown only if at least one of `title`, `description`, or `thumbnailUrl` is present in `source.metadata`.
119
+ - When those fields are omitted, the title bar stays hidden; no default text is displayed.
120
+ - For the underlying video poster, you can provide `metadata.posterUrl`.
121
+ - The built-in Share action uses the Web Share API when available; it includes `title`/`text` only if provided, otherwise it shares just the current page URL.
122
+
123
+ ## Keyboard shortcuts (Web)
124
+ - Space or K: Play/Pause
125
+ - Arrow Left/Right: Seek -/+ 10s
126
+ - Arrow Up/Down: Volume up/down
127
+ - M: Mute/Unmute
128
+ - F: Toggle Fullscreen
129
+ - P: Picture-in-Picture
130
+ - 0..9: Seek to 0%, 10%, …, 90%
131
+
132
+ ## Local demo (monorepo)
133
+ - Build the web package:
134
+ - `npm run build:web`
135
+ - Start the demo server from the repo root:
136
+ - `npm run serve:demo`
137
+ - Open the enhanced demo:
138
+ - http://localhost:3000/apps/demo/enhanced-player.html
139
+
140
+ ## Tests (monorepo)
141
+ - Run the web package tests:
142
+ - `npm run test -w packages/web`
143
+
144
+ ## Events and controls
145
+ ```ts
146
+ // Listen to player events
147
+ player.on('onPlay', () => console.log('playing'));
148
+ player.on('onPause', () => console.log('paused'));
149
+ player.on('onQualityChanged', (q) => console.log('quality ->', q?.label));
150
+ player.on('onError', (err) => console.error('player error', err));
151
+
152
+ // Control playback
153
+ await player.play();
154
+ player.pause();
155
+ player.seek(60); // 60 seconds
156
+ player.setVolume(0.5); // 50%
157
+ player.toggleMute();
158
+
159
+ // Query state
160
+ console.log(player.getState());
161
+ ```
162
+
163
+ ## CDN / no-bundler usage (ESM via CDN)
164
+ ```html
165
+ <div id="player"></div>
166
+
167
+ <script type="module">
168
+ import { WebPlayer } from 'https://esm.sh/@unified-video/web';
169
+
170
+ const player = new WebPlayer();
171
+ await player.initialize('#player', { autoPlay: false });
172
+
173
+ await player.load({
174
+ url: 'https://example.com/video.mp4',
175
+ type: 'mp4',
176
+ });
177
+
178
+ await player.play();
179
+ </script>
180
+ ```
181
+
182
+ ## Requirements
183
+ - Modern browsers with standard DOM APIs.
184
+ - For HLS/DASH playback, install `hls.js`/`dashjs` or allow the player to fetch them from CDNs at runtime.
185
+
186
+ ## Tip
187
+ - To keep bundle size under control and avoid network fetches during playback startup, prefer installing `hls.js` and `dashjs` in your app rather than relying on runtime CDN loading.
188
+
189
+ ---
190
+
191
+ ## React usage
192
+
193
+ A built-in React component is provided: `WebPlayerView`. It renders the enhanced UI and supports live theming via the `playerTheme` prop.
194
+
195
+ Basic usage
196
+ ```tsx path=null start=null
197
+ import React from 'react';
198
+ import { WebPlayerView } from '@unified-video/web';
199
+
200
+ export default function PlayerSection() {
201
+ return (
202
+ <div style={{ maxWidth: 960, margin: '0 auto' }}>
203
+ <WebPlayerView
204
+ url="https://example.com/stream.m3u8"
205
+ type="hls"
206
+ autoPlay={false}
207
+ muted={false}
208
+ enableAdaptiveBitrate={true}
209
+ cast={true} // loads Cast sender SDK and shows Cast button
210
+ metadata={{
211
+ title: 'My Stream',
212
+ description: 'An example stream with metadata-driven UI',
213
+ thumbnailUrl: 'https://example.com/thumb.jpg',
214
+ posterUrl: 'https://example.com/poster.jpg',
215
+ }}
216
+ playerTheme={{
217
+ accent: '#ff0000',
218
+ accent2: '#ff4d4f',
219
+ iconColor: '#ffffff',
220
+ textPrimary: '#ffffff',
221
+ textSecondary: 'rgba(255,255,255,0.75)',
222
+ }}
223
+ style={{ width: '100%' }}
224
+ />
225
+ </div>
226
+ );
227
+ }
228
+ ```
229
+
230
+ Dynamic theme updates
231
+ ```tsx path=null start=null
232
+ import React, { useState } from 'react';
233
+ import { WebPlayerView } from '@unified-video/web';
234
+
235
+ export default function ThemedPlayer() {
236
+ const [theme, setTheme] = useState({
237
+ accent: '#ff0000',
238
+ accent2: '#ff4d4f',
239
+ iconColor: '#ffffff',
240
+ textPrimary: '#ffffff',
241
+ textSecondary: 'rgba(255,255,255,0.75)',
242
+ });
243
+
244
+ return (
245
+ <>
246
+ <div style={{ marginBottom: 12 }}>
247
+ <button onClick={() => setTheme({ accent: '#00bcd4', accent2: '#40c4ff', iconColor: '#fff', textPrimary: '#e6f7ff', textSecondary: 'rgba(230,247,255,0.7)' })}>
248
+ Switch to Cyan
249
+ </button>
250
+ <button onClick={() => setTheme('#7e57c2')} style={{ marginLeft: 8 }}>
251
+ Single Accent (Purple)
252
+ </button>
253
+ </div>
254
+
255
+ <WebPlayerView
256
+ url="https://example.com/stream.m3u8"
257
+ type="hls"
258
+ playerTheme={theme}
259
+ style={{ width: '100%', maxWidth: 960, margin: '0 auto' }}
260
+ />
261
+ </>
262
+ );
263
+ }
264
+ ```
265
+
266
+ Advanced tweaks (onReady)
267
+ ```tsx path=null start=null
268
+ import React from 'react';
269
+ import { WebPlayerView, WebPlayer } from '@unified-video/web';
270
+
271
+ export default function PlayerWithControls() {
272
+ const onReady = (player: WebPlayer) => {
273
+ // Compact settings menu scrollbar and tuned look
274
+ player.setSettingsScrollbarStyle('compact'); // 'default' | 'compact' | 'overlay'
275
+ player.setSettingsScrollbarConfig({ widthPx: 6, intensity: 1 });
276
+ };
277
+
278
+ return (
279
+ <WebPlayerView
280
+ url="https://example.com/stream.m3u8"
281
+ type="hls"
282
+ onReady={onReady}
283
+ playerTheme="#00bcd4"
284
+ />
285
+ );
286
+ }
287
+ ```
288
+
289
+ Notes
290
+ - `cast` adds the Cast sender SDK; a Cast button appears when the framework is ready and compatible devices are available.
291
+ - Use client-side rendering for SSR frameworks.
292
+ - The enhanced UI in React matches the web build and honors the same theme variables.
@@ -0,0 +1,28 @@
1
+ module.exports = {
2
+ testEnvironment: 'jsdom',
3
+ roots: ['<rootDir>/src'],
4
+ testMatch: [
5
+ '**/__tests__/**/*.test.ts',
6
+ '**/__tests__/**/*.test.tsx',
7
+ '**/?(*.)+(spec|test).[tj]s?(x)'
8
+ ],
9
+ transform: {
10
+ '^.+\\.(ts|tsx)$': [
11
+ 'babel-jest',
12
+ {
13
+ presets: [
14
+ ['@babel/preset-env', { targets: { node: 'current' } }],
15
+ '@babel/preset-typescript'
16
+ ]
17
+ }
18
+ ]
19
+ },
20
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'json'],
21
+ moduleNameMapper: {
22
+ '^@unified-video/core$': '<rootDir>/../core/src/index.ts',
23
+ '^@unified-video/core/(.*)$': '<rootDir>/../core/src/$1'
24
+ },
25
+ transformIgnorePatterns: ['/node_modules/'],
26
+ setupFilesAfterEnv: ['<rootDir>/jest.setup.ts']
27
+ };
28
+
@@ -0,0 +1,110 @@
1
+ // Jest setup for DOM APIs not implemented by JSDOM
2
+
3
+ // Mock HTMLMediaElement methods used by the player
4
+ Object.defineProperty(HTMLMediaElement.prototype, 'load', {
5
+ configurable: true,
6
+ value: jest.fn()
7
+ });
8
+ Object.defineProperty(HTMLMediaElement.prototype, 'play', {
9
+ configurable: true,
10
+ value: jest.fn().mockResolvedValue()
11
+ });
12
+ Object.defineProperty(HTMLMediaElement.prototype, 'pause', {
13
+ configurable: true,
14
+ value: jest.fn()
15
+ });
16
+
17
+ // Mock Canvas 2D context
18
+ Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
19
+ configurable: true,
20
+ value: () => {
21
+ return {
22
+ clearRect: jest.fn(),
23
+ fillText: jest.fn(),
24
+ createLinearGradient: () => ({ addColorStop: jest.fn() }),
25
+ font: '',
26
+ fillStyle: '',
27
+ textAlign: 'left'
28
+ } as any;
29
+ }
30
+ });
31
+
32
+ // Stub HLS.js for tests to avoid dynamic script loading
33
+ // Provides minimal API used by WebPlayer
34
+ // @ts-ignore
35
+ window.Hls = {
36
+ isSupported: () => true,
37
+ Events: {
38
+ MANIFEST_PARSED: 'manifestParsed',
39
+ LEVEL_SWITCHED: 'levelSwitched',
40
+ ERROR: 'error'
41
+ },
42
+ ErrorTypes: {
43
+ NETWORK_ERROR: 'networkError',
44
+ MEDIA_ERROR: 'mediaError'
45
+ }
46
+ };
47
+
48
+ // Provide constructor behavior
49
+ // @ts-ignore
50
+ window.Hls = class {
51
+ static isSupported() { return true; }
52
+ static Events = {
53
+ MANIFEST_PARSED: 'manifestParsed',
54
+ LEVEL_SWITCHED: 'levelSwitched',
55
+ ERROR: 'error'
56
+ };
57
+ static ErrorTypes = {
58
+ NETWORK_ERROR: 'networkError',
59
+ MEDIA_ERROR: 'mediaError'
60
+ };
61
+ private handlers: Record<string, Function[]> = {};
62
+ constructor(_: any) {}
63
+ loadSource(_: string) {}
64
+ attachMedia(_: HTMLVideoElement | null) {}
65
+ on(evt: string, handler: Function) {
66
+ this.handlers[evt] = this.handlers[evt] || [];
67
+ this.handlers[evt].push(handler);
68
+ }
69
+ startLoad() {}
70
+ recoverMediaError() {}
71
+ destroy() {}
72
+ get currentLevel() { return -1; }
73
+ set currentLevel(_val: number) {}
74
+ };
75
+
76
+ // Stub dash.js for tests to avoid dynamic script loading
77
+ // @ts-ignore
78
+ (function(){
79
+ const MediaPlayer = function() {
80
+ return {
81
+ create() {
82
+ const listeners: Record<string, Function[]> = {};
83
+ return {
84
+ initialize: (_video: HTMLVideoElement | null, _url: string, _autoPlay?: boolean) => {},
85
+ updateSettings: (_: any) => {},
86
+ on: (evt: string, handler: Function) => {
87
+ listeners[evt] = listeners[evt] || [];
88
+ listeners[evt].push(handler);
89
+ },
90
+ reset: () => {},
91
+ setQualityFor: (_type: string, _index: number) => {},
92
+ getBitrateInfoListFor: (_type: string) => [
93
+ { height: 1080, width: 1920, bitrate: 4000000 },
94
+ { height: 720, width: 1280, bitrate: 2500000 }
95
+ ]
96
+ } as any;
97
+ }
98
+ };
99
+ };
100
+ // Attach static events map to function object
101
+ // @ts-ignore
102
+ MediaPlayer.events = {
103
+ QUALITY_CHANGE_RENDERED: 'qualityChangeRendered',
104
+ STREAM_INITIALIZED: 'streamInitialized',
105
+ ERROR: 'error'
106
+ };
107
+ // @ts-ignore
108
+ window.dashjs = { MediaPlayer };
109
+ })();
110
+