unified-video-framework 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +253 -0
- package/ANDROID_TV_IMPLEMENTATION.md +313 -0
- package/COMPLETION_STATUS.md +165 -0
- package/CONTRIBUTING.md +376 -0
- package/FINAL_STATUS_REPORT.md +170 -0
- package/FRAMEWORK_REVIEW.md +247 -0
- package/IMPROVEMENTS_SUMMARY.md +168 -0
- package/LICENSE +21 -0
- package/NATIVE_APP_INTEGRATION_GUIDE.md +903 -0
- package/PAYWALL_RENTAL_FLOW.md +499 -0
- package/PLATFORM_SETUP_GUIDE.md +1636 -0
- package/README.md +315 -0
- package/RUN_LOCALLY.md +151 -0
- package/apps/demo/cast-sender-min.html +173 -0
- package/apps/demo/custom-player.html +883 -0
- package/apps/demo/demo.html +990 -0
- package/apps/demo/enhanced-player.html +3556 -0
- package/apps/demo/index.html +159 -0
- package/apps/rental-api/.env.example +24 -0
- package/apps/rental-api/README.md +23 -0
- package/apps/rental-api/migrations/001_init.sql +35 -0
- package/apps/rental-api/migrations/002_videos.sql +10 -0
- package/apps/rental-api/migrations/003_add_gateway_subref.sql +4 -0
- package/apps/rental-api/migrations/004_update_gateways.sql +4 -0
- package/apps/rental-api/migrations/005_seed_demo_video.sql +5 -0
- package/apps/rental-api/package-lock.json +2045 -0
- package/apps/rental-api/package.json +33 -0
- package/apps/rental-api/scripts/run-migration.js +42 -0
- package/apps/rental-api/scripts/update-video-currency.js +21 -0
- package/apps/rental-api/scripts/update-video-price.js +19 -0
- package/apps/rental-api/src/config.ts +14 -0
- package/apps/rental-api/src/db.ts +10 -0
- package/apps/rental-api/src/routes/cashfree.ts +167 -0
- package/apps/rental-api/src/routes/pesapal.ts +92 -0
- package/apps/rental-api/src/routes/rentals.ts +242 -0
- package/apps/rental-api/src/routes/webhooks.ts +73 -0
- package/apps/rental-api/src/server.ts +41 -0
- package/apps/rental-api/src/services/entitlements.ts +45 -0
- package/apps/rental-api/src/services/payments.ts +22 -0
- package/apps/rental-api/tsconfig.json +17 -0
- package/check-urls.ps1 +74 -0
- package/comparison-report.md +181 -0
- package/docs/PAYWALL.md +95 -0
- package/docs/PLAYER_UI_VISIBILITY.md +431 -0
- package/docs/README.md +7 -0
- package/docs/SYSTEM_ARCHITECTURE.md +612 -0
- package/docs/VDOCIPHER_CLONE_REQUIREMENTS.md +403 -0
- package/examples/android/JavaSampleApp/MainActivity.java +641 -0
- package/examples/android/JavaSampleApp/activity_main.xml +226 -0
- package/examples/android/SampleApp/MainActivity.kt +430 -0
- package/examples/ios/SampleApp/ViewController.swift +337 -0
- package/examples/ios/SwiftUISampleApp/ContentView.swift +304 -0
- package/iOS_IMPLEMENTATION_OPTIONS.md +470 -0
- package/ios/UnifiedVideoPlayer/UnifiedVideoPlayer.podspec +33 -0
- package/jest.config.js +33 -0
- package/jitpack.yml +5 -0
- package/lerna.json +35 -0
- package/package.json +69 -0
- package/packages/PLATFORM_STATUS.md +163 -0
- package/packages/android/build.gradle +135 -0
- package/packages/android/src/main/AndroidManifest.xml +36 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/PlayerConfiguration.java +221 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.java +1037 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/UnifiedVideoPlayer.kt +707 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/analytics/AnalyticsProvider.java +9 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastManager.java +141 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/cast/CastOptionsProvider.java +29 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/overlay/WatermarkOverlayView.java +88 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/pip/PipActionReceiver.java +33 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlaybackService.java +110 -0
- package/packages/android/src/main/java/com/unifiedvideo/player/services/PlayerHolder.java +19 -0
- package/packages/core/package.json +34 -0
- package/packages/core/src/BasePlayer.ts +250 -0
- package/packages/core/src/VideoPlayer.ts +237 -0
- package/packages/core/src/VideoPlayerFactory.ts +145 -0
- package/packages/core/src/index.ts +20 -0
- package/packages/core/src/interfaces/IVideoPlayer.ts +184 -0
- package/packages/core/src/interfaces.ts +240 -0
- package/packages/core/src/utils/EventEmitter.ts +66 -0
- package/packages/core/src/utils/PlatformDetector.ts +300 -0
- package/packages/core/tsconfig.json +20 -0
- package/packages/enact/package.json +51 -0
- package/packages/enact/src/VideoPlayer.js +365 -0
- package/packages/enact/src/adapters/TizenAdapter.js +354 -0
- package/packages/enact/src/index.js +82 -0
- package/packages/ios/BUILD_INSTRUCTIONS.md +108 -0
- package/packages/ios/FIX_EMBED_ISSUE.md +142 -0
- package/packages/ios/GETTING_STARTED.md +100 -0
- package/packages/ios/Package.swift +35 -0
- package/packages/ios/README.md +84 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Analytics/AnalyticsEmitter.swift +26 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/DRM/FairPlayDRMManager.swift +102 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Info.plist +24 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Remote/RemoteCommandCenter.swift +109 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayer.swift +811 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/UnifiedVideoPlayerView.swift +640 -0
- package/packages/ios/Sources/UnifiedVideoPlayer/Utilities/Color+Hex.swift +36 -0
- package/packages/ios/UnifiedVideoPlayer.podspec +27 -0
- package/packages/ios/UnifiedVideoPlayer.xcodeproj/project.pbxproj +385 -0
- package/packages/ios/build_framework.sh +55 -0
- package/packages/react-native/android/src/main/java/com/unifiedvideo/UnifiedVideoPlayerModule.kt +482 -0
- package/packages/react-native/ios/UnifiedVideoPlayer.swift +436 -0
- package/packages/react-native/package.json +51 -0
- package/packages/react-native/src/ReactNativePlayer.tsx +423 -0
- package/packages/react-native/src/VideoPlayer.tsx +224 -0
- package/packages/react-native/src/index.ts +28 -0
- package/packages/react-native/src/utils/EventEmitter.ts +66 -0
- package/packages/react-native/tsconfig.json +31 -0
- package/packages/roku/components/UnifiedVideoPlayer.brs +400 -0
- package/packages/roku/package.json +44 -0
- package/packages/roku/source/VideoPlayer.brs +231 -0
- package/packages/roku/source/main.brs +28 -0
- package/packages/web/GETTING_STARTED.md +292 -0
- package/packages/web/jest.config.js +28 -0
- package/packages/web/jest.setup.ts +110 -0
- package/packages/web/package.json +50 -0
- package/packages/web/src/SecureVideoPlayer.ts +1164 -0
- package/packages/web/src/WebPlayer.ts +3110 -0
- package/packages/web/src/__tests__/WebPlayer.test.ts +314 -0
- package/packages/web/src/index.ts +14 -0
- package/packages/web/src/paywall/PaywallController.ts +215 -0
- package/packages/web/src/react/WebPlayerView.tsx +177 -0
- package/packages/web/tsconfig.json +23 -0
- package/packages/web/webpack.config.js +45 -0
- package/server.js +131 -0
- package/server.py +84 -0
- package/test-urls.ps1 +97 -0
- package/test-video-urls.ps1 +87 -0
- package/tsconfig.json +39 -0
|
@@ -0,0 +1,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
|
+
|