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,431 @@
|
|
|
1
|
+
# Player UI Visibility API (playerId-driven)
|
|
2
|
+
|
|
3
|
+
This document defines an admin-friendly API for dynamically controlling per-player UI using a playerId. The WebPlayer fetches a JSON config using `playerId` and applies it. Everything defaults to visible and the default layout if not configured.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
## Controls you can toggle
|
|
7
|
+
|
|
8
|
+
All keys are booleans (true = visible, false = hidden). Omit a key to fall back to true.
|
|
9
|
+
|
|
10
|
+
- cast: Cast button + Stop Casting
|
|
11
|
+
- settings: Settings button + Settings menu
|
|
12
|
+
- share: Share button
|
|
13
|
+
- fullscreen: Fullscreen button
|
|
14
|
+
- pip: Picture-in-Picture button
|
|
15
|
+
- playlist: Playlist button
|
|
16
|
+
- skipBack: Back 10s button
|
|
17
|
+
- skipForward: Forward 10s button
|
|
18
|
+
- volume: Volume button + Volume panel
|
|
19
|
+
- time: Time display (00:00 / 00:00)
|
|
20
|
+
- qualityBadge: Quality badge (HD/AUTO)
|
|
21
|
+
- seekbar: Progress bar section (bar, handle, tooltip)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## DOM ID mapping (WebPlayer)
|
|
25
|
+
|
|
26
|
+
Used internally to show/hide elements.
|
|
27
|
+
|
|
28
|
+
- cast -> uvf-cast-btn, uvf-stop-cast-btn
|
|
29
|
+
- settings -> uvf-settings-btn, uvf-settings-menu
|
|
30
|
+
- share -> uvf-share-btn
|
|
31
|
+
- fullscreen -> uvf-fullscreen-btn
|
|
32
|
+
- pip -> uvf-pip-btn
|
|
33
|
+
- playlist -> uvf-playlist-btn
|
|
34
|
+
- skipBack -> uvf-skip-back
|
|
35
|
+
- skipForward -> uvf-skip-forward
|
|
36
|
+
- volume -> uvf-volume-btn, uvf-volume-panel
|
|
37
|
+
- time -> uvf-time-display
|
|
38
|
+
- qualityBadge -> uvf-quality-badge
|
|
39
|
+
- seekbar -> uvf-progress-section, uvf-progress-bar
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## API design
|
|
43
|
+
|
|
44
|
+
Single source of truth per `playerId`. The player only uses these endpoints—no other fallbacks.
|
|
45
|
+
|
|
46
|
+
- GET /player-ui/{playerId}
|
|
47
|
+
- Returns JSON visibility+layout for that playerId
|
|
48
|
+
- 200 with JSON if configured; 404 if not configured
|
|
49
|
+
- PUT /player-ui/{playerId}
|
|
50
|
+
- Admin upsert of the visibility+layout JSON
|
|
51
|
+
- 200 with the saved JSON
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
## Response format (GET)
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"playerId": "landing_hero",
|
|
59
|
+
"v": 1,
|
|
60
|
+
"visibility": {
|
|
61
|
+
"cast": false,
|
|
62
|
+
"settings": true,
|
|
63
|
+
"share": false,
|
|
64
|
+
"fullscreen": true,
|
|
65
|
+
"pip": false,
|
|
66
|
+
"playlist": false,
|
|
67
|
+
"skipBack": true,
|
|
68
|
+
"skipForward": true,
|
|
69
|
+
"volume": true,
|
|
70
|
+
"time": true,
|
|
71
|
+
"qualityBadge": true,
|
|
72
|
+
"seekbar": true
|
|
73
|
+
},
|
|
74
|
+
"layout": {
|
|
75
|
+
"template": "classic",
|
|
76
|
+
"regions": {
|
|
77
|
+
"topLeft": ["playlist"],
|
|
78
|
+
"topRight": ["cast", "share"],
|
|
79
|
+
"bottomLeft": ["playPause", "skipBack", "skipForward"],
|
|
80
|
+
"bottomRight": ["settings", "pip", "fullscreen"],
|
|
81
|
+
"centerOverlay": ["centerPlay"]
|
|
82
|
+
},
|
|
83
|
+
"order": {
|
|
84
|
+
"playPause": 10,
|
|
85
|
+
"skipBack": 20,
|
|
86
|
+
"skipForward": 30,
|
|
87
|
+
"settings": 10,
|
|
88
|
+
"pip": 20,
|
|
89
|
+
"fullscreen": 30
|
|
90
|
+
},
|
|
91
|
+
"breakpoints": {
|
|
92
|
+
"desktop": {
|
|
93
|
+
"regions": {
|
|
94
|
+
"topRight": ["cast", "share"],
|
|
95
|
+
"bottomRight": ["settings", "pip", "fullscreen"]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"tablet": {
|
|
99
|
+
"regions": {
|
|
100
|
+
"topRight": ["cast"],
|
|
101
|
+
"bottomRight": ["settings", "fullscreen"]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"mobile": {
|
|
105
|
+
"regions": {
|
|
106
|
+
"topRight": [],
|
|
107
|
+
"bottomRight": ["settings"],
|
|
108
|
+
"topLeft": []
|
|
109
|
+
},
|
|
110
|
+
"overrides": {
|
|
111
|
+
"share": { "visible": false },
|
|
112
|
+
"pip": { "visible": false }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"overrides": {
|
|
117
|
+
"qualityBadge": { "region": "bottomRight", "order": 5 },
|
|
118
|
+
"time": { "region": "bottomLeft", "order": 40 },
|
|
119
|
+
"seekbar": { "region": "bottomFull", "order": 1 }
|
|
120
|
+
},
|
|
121
|
+
"style": {
|
|
122
|
+
"gap": 10,
|
|
123
|
+
"padding": 16,
|
|
124
|
+
"align": "space-between"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"meta": {
|
|
128
|
+
"updatedAt": "2025-09-02T09:00:00.000Z",
|
|
129
|
+
"updatedBy": "admin@example.com"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Notes
|
|
135
|
+
- `visibility` and `layout` may omit keys; omitted visibility keys are treated as `true`; omitted layout falls back to the default template.
|
|
136
|
+
- `v` is a schema/version number (start with `1`).
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
## Request format (PUT)
|
|
140
|
+
|
|
141
|
+
Admin panels send the parts they modify; backend stores them.
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"visibility": {
|
|
146
|
+
"cast": false,
|
|
147
|
+
"share": false,
|
|
148
|
+
"pip": false,
|
|
149
|
+
"seekbar": true
|
|
150
|
+
},
|
|
151
|
+
"layout": {
|
|
152
|
+
"template": "classic",
|
|
153
|
+
"overrides": {
|
|
154
|
+
"fullscreen": { "region": "topRight", "order": 1 },
|
|
155
|
+
"settings": { "region": "bottomRight", "order": 10 }
|
|
156
|
+
},
|
|
157
|
+
"breakpoints": {
|
|
158
|
+
"mobile": {
|
|
159
|
+
"regions": {
|
|
160
|
+
"topRight": [],
|
|
161
|
+
"bottomRight": ["settings"]
|
|
162
|
+
},
|
|
163
|
+
"overrides": {
|
|
164
|
+
"fullscreen": { "visible": false }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A successful upsert returns the complete stored object.
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
## JSON Schema (validation)
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"type": "object",
|
|
180
|
+
"properties": {
|
|
181
|
+
"playerId": { "type": "string" },
|
|
182
|
+
"v": { "type": "integer" },
|
|
183
|
+
"visibility": {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"properties": {
|
|
186
|
+
"cast": { "type": "boolean" },
|
|
187
|
+
"settings": { "type": "boolean" },
|
|
188
|
+
"share": { "type": "boolean" },
|
|
189
|
+
"fullscreen": { "type": "boolean" },
|
|
190
|
+
"pip": { "type": "boolean" },
|
|
191
|
+
"playlist": { "type": "boolean" },
|
|
192
|
+
"skipBack": { "type": "boolean" },
|
|
193
|
+
"skipForward": { "type": "boolean" },
|
|
194
|
+
"volume": { "type": "boolean" },
|
|
195
|
+
"time": { "type": "boolean" },
|
|
196
|
+
"qualityBadge": { "type": "boolean" },
|
|
197
|
+
"seekbar": { "type": "boolean" }
|
|
198
|
+
},
|
|
199
|
+
"additionalProperties": false
|
|
200
|
+
},
|
|
201
|
+
"layout": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"properties": {
|
|
204
|
+
"template": { "type": "string" },
|
|
205
|
+
"regions": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"description": "Map of regionName -> array of control keys",
|
|
208
|
+
"additionalProperties": {
|
|
209
|
+
"type": "array",
|
|
210
|
+
"items": { "type": "string" }
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
"order": {
|
|
214
|
+
"type": "object",
|
|
215
|
+
"description": "Global z-order within a region (lower comes first)",
|
|
216
|
+
"additionalProperties": { "type": "integer" }
|
|
217
|
+
},
|
|
218
|
+
"overrides": {
|
|
219
|
+
"type": "object",
|
|
220
|
+
"description": "Per-control overrides (region/order/visibility/position)",
|
|
221
|
+
"additionalProperties": {
|
|
222
|
+
"type": "object",
|
|
223
|
+
"properties": {
|
|
224
|
+
"region": { "type": "string" },
|
|
225
|
+
"order": { "type": "integer" },
|
|
226
|
+
"visible": { "type": "boolean" },
|
|
227
|
+
"position": {
|
|
228
|
+
"type": "object",
|
|
229
|
+
"properties": {
|
|
230
|
+
"mode": { "type": "string", "enum": ["region", "absolute"] },
|
|
231
|
+
"top": { "type": "number" },
|
|
232
|
+
"right": { "type": "number" },
|
|
233
|
+
"bottom": { "type": "number" },
|
|
234
|
+
"left": { "type": "number" }
|
|
235
|
+
},
|
|
236
|
+
"additionalProperties": false
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"additionalProperties": true
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"breakpoints": {
|
|
243
|
+
"type": "object",
|
|
244
|
+
"description": "Responsive overrides per breakpoint",
|
|
245
|
+
"properties": {
|
|
246
|
+
"desktop": { "$ref": "#/definitions/bp" },
|
|
247
|
+
"tablet": { "$ref": "#/definitions/bp" },
|
|
248
|
+
"mobile": { "$ref": "#/definitions/bp" }
|
|
249
|
+
},
|
|
250
|
+
"additionalProperties": { "$ref": "#/definitions/bp" }
|
|
251
|
+
},
|
|
252
|
+
"style": {
|
|
253
|
+
"type": "object",
|
|
254
|
+
"properties": {
|
|
255
|
+
"gap": { "type": "number" },
|
|
256
|
+
"padding": { "type": "number" },
|
|
257
|
+
"align": { "type": "string", "enum": ["start", "center", "end", "space-between"] }
|
|
258
|
+
},
|
|
259
|
+
"additionalProperties": true
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
"additionalProperties": false,
|
|
263
|
+
"definitions": {
|
|
264
|
+
"bp": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"properties": {
|
|
267
|
+
"regions": {
|
|
268
|
+
"type": "object",
|
|
269
|
+
"additionalProperties": {
|
|
270
|
+
"type": "array",
|
|
271
|
+
"items": { "type": "string" }
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"overrides": {
|
|
275
|
+
"type": "object",
|
|
276
|
+
"additionalProperties": {
|
|
277
|
+
"type": "object",
|
|
278
|
+
"properties": {
|
|
279
|
+
"region": { "type": "string" },
|
|
280
|
+
"order": { "type": "integer" },
|
|
281
|
+
"visible": { "type": "boolean" },
|
|
282
|
+
"position": {
|
|
283
|
+
"type": "object",
|
|
284
|
+
"properties": {
|
|
285
|
+
"mode": { "type": "string", "enum": ["region", "absolute"] },
|
|
286
|
+
"top": { "type": "number" },
|
|
287
|
+
"right": { "type": "number" },
|
|
288
|
+
"bottom": { "type": "number" },
|
|
289
|
+
"left": { "type": "number" }
|
|
290
|
+
},
|
|
291
|
+
"additionalProperties": false
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
"additionalProperties": true
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"additionalProperties": false
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
"meta": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"properties": {
|
|
305
|
+
"updatedAt": { "type": "string", "format": "date-time" },
|
|
306
|
+
"updatedBy": { "type": "string" }
|
|
307
|
+
},
|
|
308
|
+
"additionalProperties": true
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
"additionalProperties": false
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
## Database model
|
|
317
|
+
|
|
318
|
+
One record per `playerId`. Store `visibility` and `layout` as JSON/JSONB so you can evolve easily.
|
|
319
|
+
|
|
320
|
+
```sql
|
|
321
|
+
CREATE TABLE IF NOT EXISTS player_ui_config (
|
|
322
|
+
player_id TEXT PRIMARY KEY,
|
|
323
|
+
v INTEGER NOT NULL DEFAULT 1,
|
|
324
|
+
visibility JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
325
|
+
layout JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
326
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
327
|
+
updated_by TEXT
|
|
328
|
+
);
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
## Backend outline (Express-style pseudocode)
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
// GET /player-ui/:playerId
|
|
336
|
+
app.get('/player-ui/:playerId', async (req, res) => {
|
|
337
|
+
const playerId = req.params.playerId.trim();
|
|
338
|
+
const row = await db.selectOne('player_ui_config', { player_id: playerId });
|
|
339
|
+
if (!row) return res.status(404).json({ error: 'not_found', playerId });
|
|
340
|
+
|
|
341
|
+
const defaultsVis = {
|
|
342
|
+
cast: true, settings: true, share: true, fullscreen: true, pip: true,
|
|
343
|
+
playlist: true, skipBack: true, skipForward: true, volume: true,
|
|
344
|
+
time: true, qualityBadge: true, seekbar: true
|
|
345
|
+
};
|
|
346
|
+
const vis = { ...defaultsVis, ...(row.visibility || {}) };
|
|
347
|
+
const layout = row.layout || {};
|
|
348
|
+
|
|
349
|
+
res.json({
|
|
350
|
+
playerId,
|
|
351
|
+
v: row.v || 1,
|
|
352
|
+
visibility: vis,
|
|
353
|
+
layout,
|
|
354
|
+
meta: { updatedAt: row.updated_at, updatedBy: row.updated_by || null }
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// PUT /player-ui/:playerId
|
|
359
|
+
app.put('/player-ui/:playerId', requireAdminAuth, async (req, res) => {
|
|
360
|
+
const playerId = req.params.playerId.trim();
|
|
361
|
+
const incomingVis = req.body?.visibility || {};
|
|
362
|
+
const incomingLayout = req.body?.layout || {};
|
|
363
|
+
|
|
364
|
+
const allowedVis = [
|
|
365
|
+
'cast','settings','share','fullscreen','pip','playlist',
|
|
366
|
+
'skipBack','skipForward','volume','time','qualityBadge','seekbar'
|
|
367
|
+
];
|
|
368
|
+
const cleanedVis = {};
|
|
369
|
+
for (const k of allowedVis) if (typeof incomingVis[k] === 'boolean') cleanedVis[k] = incomingVis[k];
|
|
370
|
+
|
|
371
|
+
await db.upsert('player_ui_config', {
|
|
372
|
+
player_id: playerId,
|
|
373
|
+
v: 1,
|
|
374
|
+
visibility: cleanedVis,
|
|
375
|
+
layout: incomingLayout,
|
|
376
|
+
updated_at: new Date(),
|
|
377
|
+
updated_by: req.user?.email || 'system'
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
res.json({ status: true, playerId, v: 1, visibility: cleanedVis, layout: incomingLayout, meta: { updatedAt: new Date().toISOString(), updatedBy: req.user?.email || 'system' } });
|
|
381
|
+
});
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
## Admin panel UX (suggestion)
|
|
386
|
+
|
|
387
|
+
- Page: Player UI Profiles
|
|
388
|
+
- Table: playerId, Updated, Updated By, Actions (Edit, Duplicate)
|
|
389
|
+
- Tabs in edit form: Visibility, Layout, Preview
|
|
390
|
+
- Visibility tab: toggles for all flags
|
|
391
|
+
- Layout tab:
|
|
392
|
+
- Template select (default/classic/compact/mobile-first)
|
|
393
|
+
- Region editor (drag buttons into regions, set order)
|
|
394
|
+
- Breakpoint editor (desktop/tablet/mobile overrides)
|
|
395
|
+
- Per-control overrides (region/order/visibility, optional absolute position)
|
|
396
|
+
- Preview tab: embedded test player using this `playerId`
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
## Player integration (WebPlayer)
|
|
400
|
+
|
|
401
|
+
Pass a `playerId` and a `visibilityEndpoint`. The player calls GET /player-ui/{playerId}, applies `visibility`, then applies `layout` by moving controls into regions and ordering them. If fetch fails or 404, it uses all-visible + default layout.
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
await player.initialize('#player', {
|
|
405
|
+
ui: {
|
|
406
|
+
playerId: 'landing_hero',
|
|
407
|
+
visibilityEndpoint: 'https://your-api.example.com/player-ui/{playerId}'
|
|
408
|
+
}
|
|
409
|
+
} as any);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Region model in the player (suggestion)
|
|
413
|
+
- Predefine region containers in DOM (created by the player) such as:
|
|
414
|
+
- topLeft, topRight, bottomLeft, bottomRight, bottomFull, centerOverlay
|
|
415
|
+
- For each region name in `layout.regions`, the player appends known control nodes by ID.
|
|
416
|
+
- Apply `order` and per-control `overrides` to fine-tune placement.
|
|
417
|
+
- `breakpoints` are applied by listening to resize and reapplying the region mapping.
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
## Defaults (no config)
|
|
421
|
+
|
|
422
|
+
If no record exists for a `playerId` or the request fails, the player should:
|
|
423
|
+
- Show all controls (visibility = all true)
|
|
424
|
+
- Use the internal default layout (template = default)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
## Future-proofing
|
|
428
|
+
|
|
429
|
+
- Keep `v` for schema evolution (e.g., adding new regions or properties)
|
|
430
|
+
- Unknown fields should be ignored by the player
|
|
431
|
+
|