@thestatic-tv/dcl-sdk 2.3.0 → 2.5.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/README.md +55 -427
- package/dist/index.d.mts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +89 -18
- package/dist/index.mjs +89 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,489 +5,117 @@ Connect your Decentraland scene to [thestatic.tv](https://thestatic.tv) - the de
|
|
|
5
5
|
[](https://www.npmjs.com/package/@thestatic-tv/dcl-sdk)
|
|
6
6
|
[](https://docs.decentraland.org/)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
- Track visitors to your Decentraland scene
|
|
10
|
-
- Display the full thestatic.tv channel lineup in their scenes
|
|
11
|
-
- Track video watching metrics
|
|
12
|
-
- Enable likes/follows from within DCL
|
|
13
|
-
- Get referral credit for driving traffic to channels
|
|
8
|
+
## Features
|
|
14
9
|
|
|
15
|
-
|
|
10
|
+
- **Visitor Analytics** - Track visitors and session metrics
|
|
11
|
+
- **Channel Guide** - Display live streams and VODs in your scene
|
|
12
|
+
- **Watch Metrics** - Track video viewing time
|
|
13
|
+
- **Interactions** - Enable likes/follows from within DCL
|
|
14
|
+
- **Built-in UI** - Pre-built Guide and Chat components
|
|
15
|
+
- **Admin Panel** - In-scene controls for Pro tier
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|------|-------|----------|
|
|
19
|
-
| **Free** | $5/mo | Session/visitor tracking only |
|
|
20
|
-
| **Standard** | $10/mo | Free + Guide UI, Chat UI, Heartbeat, Interactions |
|
|
21
|
-
| **Pro** | $15/mo | Standard + Admin Panel (Video tab, Mod tab, Custom scene tabs) |
|
|
22
|
-
|
|
23
|
-
> **All keys use `dcls_` prefix** - the server determines your subscription level.
|
|
24
|
-
>
|
|
25
|
-
> **Free Trial**: All new scene keys include a 7-day free trial!
|
|
26
|
-
>
|
|
27
|
-
> **Expiry Warnings**: You'll receive dashboard notifications at 7, 3, and 1 days before your subscription expires.
|
|
28
|
-
|
|
29
|
-
Get your key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) → DCL Scenes tab.
|
|
30
|
-
|
|
31
|
-
### Coordinate Locking
|
|
32
|
-
|
|
33
|
-
When you create a scene key, you'll be asked to confirm your parcel coordinates. **Once confirmed, coordinates are permanently locked** to prevent key reuse across different scenes. This ensures analytics accurately reflect a single scene.
|
|
34
|
-
|
|
35
|
-
### Key Rotation
|
|
36
|
-
|
|
37
|
-
If you need to rotate your API key (e.g., if compromised), you can do so from the dashboard without losing your subscription or analytics history. The old key is immediately invalidated.
|
|
38
|
-
|
|
39
|
-
## Example Scene
|
|
40
|
-
|
|
41
|
-
Clone our example scene to get started quickly:
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
git clone https://github.com/thestatic-tv/thestatic-dcl-example.git
|
|
45
|
-
cd thestatic-dcl-example
|
|
46
|
-
npm install
|
|
47
|
-
npm start
|
|
48
|
-
```
|
|
17
|
+
## Quick Start
|
|
49
18
|
|
|
50
|
-
|
|
19
|
+
1. Get your API key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
|
|
51
20
|
|
|
52
|
-
|
|
21
|
+
2. Install the SDK:
|
|
53
22
|
|
|
54
23
|
```bash
|
|
55
24
|
npm install @thestatic-tv/dcl-sdk
|
|
56
25
|
```
|
|
57
26
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
1. Get your API key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
|
|
61
|
-
|
|
62
|
-
2. Initialize the client in your scene's `main()` function:
|
|
27
|
+
3. Initialize in your scene:
|
|
63
28
|
|
|
64
29
|
```typescript
|
|
65
|
-
import {} from '@dcl/sdk/math'
|
|
66
|
-
import { engine } from '@dcl/sdk/ecs'
|
|
67
30
|
import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
|
|
68
31
|
|
|
69
32
|
let staticTV: StaticTVClient
|
|
70
33
|
|
|
71
34
|
export function main() {
|
|
72
35
|
staticTV = new StaticTVClient({
|
|
73
|
-
apiKey: '
|
|
36
|
+
apiKey: 'your_api_key_here'
|
|
74
37
|
})
|
|
75
|
-
// Session tracking starts automatically
|
|
38
|
+
// Session tracking starts automatically
|
|
76
39
|
}
|
|
77
40
|
```
|
|
78
41
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
// Get all channels
|
|
83
|
-
const channels = await staticTV.guide.getChannels()
|
|
42
|
+
## Example Scene
|
|
84
43
|
|
|
85
|
-
|
|
86
|
-
const liveChannels = await staticTV.guide.getLiveChannels()
|
|
44
|
+
Clone our starter scene to get up and running quickly:
|
|
87
45
|
|
|
88
|
-
|
|
89
|
-
|
|
46
|
+
```bash
|
|
47
|
+
git clone https://github.com/thestatic-tv/thestatic-dcl-starter.git
|
|
48
|
+
cd thestatic-dcl-starter
|
|
49
|
+
npm install
|
|
50
|
+
npm start
|
|
90
51
|
```
|
|
91
52
|
|
|
92
|
-
|
|
53
|
+
## Basic Usage
|
|
93
54
|
|
|
94
55
|
```typescript
|
|
95
|
-
//
|
|
96
|
-
staticTV.
|
|
56
|
+
// Get channels (Standard/Pro tier)
|
|
57
|
+
const channels = await staticTV.guide.getChannels()
|
|
58
|
+
const liveChannels = await staticTV.guide.getLiveChannels()
|
|
97
59
|
|
|
98
|
-
//
|
|
60
|
+
// Track video watching
|
|
61
|
+
staticTV.heartbeat.startWatching('channel-slug')
|
|
99
62
|
staticTV.heartbeat.stopWatching()
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
5. Enable interactions:
|
|
103
63
|
|
|
104
|
-
|
|
105
|
-
// Like a channel (requires wallet connection)
|
|
64
|
+
// User interactions (requires wallet)
|
|
106
65
|
await staticTV.interactions.like('channel-slug')
|
|
107
|
-
|
|
108
|
-
// Follow a channel
|
|
109
66
|
await staticTV.interactions.follow('channel-slug')
|
|
110
|
-
```
|
|
111
67
|
|
|
112
|
-
|
|
68
|
+
// Check your tier
|
|
69
|
+
console.log('Tier:', staticTV.tier) // 'free', 'standard', or 'pro'
|
|
113
70
|
|
|
114
|
-
|
|
71
|
+
// Cleanup
|
|
115
72
|
await staticTV.destroy()
|
|
116
73
|
```
|
|
117
74
|
|
|
118
|
-
##
|
|
119
|
-
|
|
120
|
-
If you don't have a channel but want to track visitors to your scene:
|
|
121
|
-
|
|
122
|
-
1. Get a scene key from [thestatic.tv/dashboard](https://thestatic.tv/dashboard) (DCL Scenes tab)
|
|
123
|
-
|
|
124
|
-
2. Initialize with your scene key in `main()`:
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import {} from '@dcl/sdk/math'
|
|
128
|
-
import { engine } from '@dcl/sdk/ecs'
|
|
129
|
-
import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
|
|
130
|
-
|
|
131
|
-
let staticTV: StaticTVClient
|
|
132
|
-
|
|
133
|
-
export function main() {
|
|
134
|
-
staticTV = new StaticTVClient({
|
|
135
|
-
apiKey: 'dcls_your_scene_key_here'
|
|
136
|
-
})
|
|
137
|
-
// Session tracking starts automatically!
|
|
138
|
-
}
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
3. Check the current tier:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
// Check tier (free, standard, or pro)
|
|
145
|
-
console.log('Current tier:', staticTV.tier)
|
|
146
|
-
|
|
147
|
-
if (staticTV.isFree) {
|
|
148
|
-
console.log('Running in free tier - session tracking only')
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// guide, heartbeat, and interactions are null in free tier
|
|
152
|
-
if (staticTV.guide) {
|
|
153
|
-
const channels = await staticTV.guide.getChannels()
|
|
154
|
-
}
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
## API Reference
|
|
158
|
-
|
|
159
|
-
### StaticTVClient
|
|
160
|
-
|
|
161
|
-
Main client class for interacting with thestatic.tv.
|
|
162
|
-
|
|
163
|
-
#### Constructor Options
|
|
164
|
-
|
|
165
|
-
| Option | Type | Default | Description |
|
|
166
|
-
|--------|------|---------|-------------|
|
|
167
|
-
| `apiKey` | `string` | required | Your API key (all keys use `dcls_` prefix) |
|
|
168
|
-
| `autoStartSession` | `boolean` | `true` | Automatically start session tracking |
|
|
169
|
-
| `sessionHeartbeatInterval` | `number` | `30000` | Session heartbeat interval (ms) |
|
|
170
|
-
| `watchHeartbeatInterval` | `number` | `60000` | Watch heartbeat interval (ms) |
|
|
171
|
-
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
172
|
-
|
|
173
|
-
#### Properties
|
|
174
|
-
|
|
175
|
-
| Property | Type | Description |
|
|
176
|
-
|----------|------|-------------|
|
|
177
|
-
| `keyType` | `'channel' \| 'scene'` | The detected key type |
|
|
178
|
-
| `tier` | `'free' \| 'standard' \| 'pro'` | Current subscription tier |
|
|
179
|
-
| `isFree` | `boolean` | `true` if free tier (session tracking only) |
|
|
180
|
-
| `hasStandardFeatures` | `boolean` | `true` if standard features enabled |
|
|
181
|
-
| `hasProFeatures` | `boolean` | `true` if pro features enabled |
|
|
182
|
-
| `guide` | `GuideModule \| null` | Guide module (null in free tier) |
|
|
183
|
-
| `session` | `SessionModule` | Session module (always available) |
|
|
184
|
-
| `heartbeat` | `HeartbeatModule \| null` | Heartbeat module (null in free tier) |
|
|
185
|
-
| `interactions` | `InteractionsModule \| null` | Interactions module (null in free tier) |
|
|
186
|
-
| `guideUI` | `GuideUIModule \| null` | Guide UI module (null in free tier) |
|
|
187
|
-
| `chatUI` | `ChatUIModule \| null` | Chat UI module (null in free tier) |
|
|
188
|
-
| `adminPanel` | `AdminPanelUIModule \| null` | Admin panel (null until enableProFeatures() called) |
|
|
189
|
-
| `isLite` | `boolean` | **Deprecated**: Use `isFree` instead |
|
|
190
|
-
|
|
191
|
-
#### Methods
|
|
192
|
-
|
|
193
|
-
| Method | Description |
|
|
194
|
-
|--------|-------------|
|
|
195
|
-
| `enableProFeatures(config)` | Enable Pro tier admin panel |
|
|
196
|
-
| `registerSceneTab(tab)` | Add custom scene tab (Pro tier) |
|
|
197
|
-
| `destroy()` | Cleanup before scene unload |
|
|
198
|
-
|
|
199
|
-
### Guide Module (Standard/Pro Tier)
|
|
200
|
-
|
|
201
|
-
```typescript
|
|
202
|
-
staticTV.guide.getChannels() // Get all channels (cached 30s)
|
|
203
|
-
staticTV.guide.getLiveChannels() // Get only live channels
|
|
204
|
-
staticTV.guide.getChannel(slug) // Get a specific channel
|
|
205
|
-
staticTV.guide.getVods() // Get VODs
|
|
206
|
-
staticTV.guide.clearCache() // Clear the channel cache
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
### Session Module
|
|
210
|
-
|
|
211
|
-
Session tracking starts automatically by default.
|
|
212
|
-
|
|
213
|
-
```typescript
|
|
214
|
-
staticTV.session.startSession() // Manually start session
|
|
215
|
-
staticTV.session.endSession() // End session
|
|
216
|
-
staticTV.session.getSessionId() // Get current session ID
|
|
217
|
-
staticTV.session.isSessionActive() // Check if session is active
|
|
218
|
-
|
|
219
|
-
// Get scene stats (visitors, sessions, etc.)
|
|
220
|
-
const stats = await staticTV.session.getStats()
|
|
221
|
-
if (stats) {
|
|
222
|
-
console.log('Total sessions today:', stats.totalSessions)
|
|
223
|
-
console.log('Unique visitors:', stats.uniqueVisitors)
|
|
224
|
-
console.log('Total minutes watched:', stats.totalMinutes)
|
|
225
|
-
console.log('You are visitor #', stats.visitorNumber)
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
### Heartbeat Module (Standard/Pro Tier)
|
|
230
|
-
|
|
231
|
-
Track video watching. Each heartbeat represents 1 minute watched.
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
staticTV.heartbeat.startWatching(channelSlug) // Start tracking
|
|
235
|
-
staticTV.heartbeat.stopWatching() // Stop tracking
|
|
236
|
-
staticTV.heartbeat.getCurrentChannel() // Get currently watched channel
|
|
237
|
-
staticTV.heartbeat.isCurrentlyWatching() // Check if watching
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Interactions Module (Standard/Pro Tier)
|
|
75
|
+
## Built-in UI Components
|
|
241
76
|
|
|
242
|
-
|
|
77
|
+
The SDK includes pre-built UI for channel browsing and chat:
|
|
243
78
|
|
|
244
79
|
```typescript
|
|
245
|
-
staticTV
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Built-in channel browser UI. You provide a callback to handle video selection.
|
|
252
|
-
|
|
253
|
-
```typescript
|
|
254
|
-
let staticTV: StaticTVClient
|
|
255
|
-
|
|
256
|
-
export function main() {
|
|
257
|
-
staticTV = new StaticTVClient({
|
|
258
|
-
apiKey: 'dcls_your_api_key',
|
|
259
|
-
guideUI: {
|
|
260
|
-
onVideoSelect: (video) => {
|
|
261
|
-
// Handle video playback in your scene
|
|
262
|
-
console.log('Playing:', video.name, video.src)
|
|
263
|
-
}
|
|
80
|
+
staticTV = new StaticTVClient({
|
|
81
|
+
apiKey: 'your_api_key',
|
|
82
|
+
guideUI: {
|
|
83
|
+
onVideoSelect: (video) => {
|
|
84
|
+
// Handle video playback
|
|
85
|
+
console.log('Playing:', video.name)
|
|
264
86
|
}
|
|
265
|
-
}
|
|
87
|
+
},
|
|
88
|
+
chatUI: {
|
|
89
|
+
position: 'right'
|
|
90
|
+
}
|
|
91
|
+
})
|
|
266
92
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
93
|
+
// Initialize UI components
|
|
94
|
+
staticTV.guideUI.init()
|
|
95
|
+
staticTV.chatUI.init()
|
|
270
96
|
|
|
271
|
-
// Show/hide
|
|
272
|
-
staticTV.guideUI.show()
|
|
273
|
-
staticTV.guideUI.hide()
|
|
97
|
+
// Show/hide
|
|
274
98
|
staticTV.guideUI.toggle()
|
|
275
|
-
|
|
276
|
-
// Check visibility
|
|
277
|
-
if (staticTV.guideUI.isVisible) { ... }
|
|
278
|
-
|
|
279
|
-
// Update "PLAYING" indicator
|
|
280
|
-
staticTV.guideUI.currentVideoId = 'video-id'
|
|
281
|
-
|
|
282
|
-
// Refresh data
|
|
283
|
-
await staticTV.guideUI.refresh()
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
### Chat UI Module (Standard/Pro Tier)
|
|
287
|
-
|
|
288
|
-
Real-time chat with Firebase authentication.
|
|
289
|
-
|
|
290
|
-
```typescript
|
|
291
|
-
let staticTV: StaticTVClient
|
|
292
|
-
|
|
293
|
-
export function main() {
|
|
294
|
-
staticTV = new StaticTVClient({
|
|
295
|
-
apiKey: 'dcls_your_api_key',
|
|
296
|
-
chatUI: {
|
|
297
|
-
position: 'right', // 'left', 'center', or 'right'
|
|
298
|
-
fontScale: 1.0
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
// Initialize the chat (call once after client is created)
|
|
303
|
-
staticTV.chatUI.init()
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Show/hide the chat
|
|
307
|
-
staticTV.chatUI.show()
|
|
308
|
-
staticTV.chatUI.hide()
|
|
309
99
|
staticTV.chatUI.toggle()
|
|
310
|
-
|
|
311
|
-
// Check visibility
|
|
312
|
-
if (staticTV.chatUI.isVisible) { ... }
|
|
313
|
-
|
|
314
|
-
// Get unread message count (for badge display)
|
|
315
|
-
const unread = staticTV.chatUI.unreadCount
|
|
316
|
-
|
|
317
|
-
// Switch channels
|
|
318
|
-
staticTV.chatUI.setChannel('channel-slug')
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Admin Panel Module (Pro Tier)
|
|
322
|
-
|
|
323
|
-
In-scene admin panel with Video and Mod tabs. Allows scene owners/admins to:
|
|
324
|
-
- Control live streaming (start/stop, rotate keys)
|
|
325
|
-
- Play videos from URL or predefined slots
|
|
326
|
-
- Manage scene admins and banned wallets
|
|
327
|
-
- Send broadcast messages to all players
|
|
328
|
-
|
|
329
|
-
```typescript
|
|
330
|
-
import { StaticTVClient } from '@thestatic-tv/dcl-sdk'
|
|
331
|
-
import ReactEcs, { ReactEcsRenderer } from '@dcl/sdk/react-ecs'
|
|
332
|
-
|
|
333
|
-
let staticTV: StaticTVClient
|
|
334
|
-
|
|
335
|
-
export function main() {
|
|
336
|
-
staticTV = new StaticTVClient({
|
|
337
|
-
apiKey: 'dcls_your_api_key'
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
// Enable admin panel (Pro tier)
|
|
341
|
-
staticTV.enableProFeatures({
|
|
342
|
-
// sceneId is optional - defaults to your API key ID
|
|
343
|
-
title: 'MY SCENE ADMIN', // Panel header title
|
|
344
|
-
onVideoPlay: (url) => {
|
|
345
|
-
// Handle video playback in your scene
|
|
346
|
-
videoPlayer.play(url)
|
|
347
|
-
},
|
|
348
|
-
onVideoStop: () => {
|
|
349
|
-
videoPlayer.stop()
|
|
350
|
-
},
|
|
351
|
-
onVideoSlotPlay: (slot) => {
|
|
352
|
-
// Play a predefined video slot (slot1-slot5)
|
|
353
|
-
fetchAndPlaySlot(slot)
|
|
354
|
-
},
|
|
355
|
-
onBroadcast: (text) => {
|
|
356
|
-
// Show notification to all players
|
|
357
|
-
showNotification(text)
|
|
358
|
-
},
|
|
359
|
-
onCommand: (type, payload) => {
|
|
360
|
-
// Handle custom commands (kickAll, kickBanned, etc.)
|
|
361
|
-
handleCommand(type, payload)
|
|
362
|
-
}
|
|
363
|
-
})
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// Render the admin panel
|
|
367
|
-
ReactEcsRenderer.setUiRenderer(() => {
|
|
368
|
-
return staticTV.adminPanel?.getComponent()
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
// Toggle panel visibility
|
|
372
|
-
staticTV.adminPanel.toggle()
|
|
373
|
-
|
|
374
|
-
// Check if user has admin access
|
|
375
|
-
if (staticTV.adminPanel.hasAccess) { ... }
|
|
376
100
|
```
|
|
377
101
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
| Option | Type | Default | Description |
|
|
381
|
-
|--------|------|---------|-------------|
|
|
382
|
-
| `sceneId` | `string` | API key ID | Scene ID for API calls (optional - defaults to your API key ID) |
|
|
383
|
-
| `title` | `string` | `'ADMIN PANEL'` | Header title |
|
|
384
|
-
| `headerColor` | `{r,g,b,a}` | red | Header background color |
|
|
385
|
-
| `showVideoTab` | `boolean` | `true` | Show Video tab |
|
|
386
|
-
| `showModTab` | `boolean` | `true` | Show Mod tab (owners only) |
|
|
387
|
-
| `onVideoPlay` | `(url) => void` | - | Called when video should play |
|
|
388
|
-
| `onVideoStop` | `() => void` | - | Called when video should stop |
|
|
389
|
-
| `onVideoSlotPlay` | `(slot) => void` | - | Called when slot is selected |
|
|
390
|
-
| `onBroadcast` | `(text) => void` | - | Called for broadcast messages |
|
|
391
|
-
| `onCommand` | `(type, payload) => void` | - | Called for custom commands |
|
|
392
|
-
| `footerLink` | `string` | scene page | Link shown in footer |
|
|
393
|
-
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
394
|
-
|
|
395
|
-
### Custom Scene Tabs (Pro Tier)
|
|
102
|
+
## Pricing & Tiers
|
|
396
103
|
|
|
397
|
-
|
|
104
|
+
See [thestatic.tv/info](https://thestatic.tv/info) for current pricing and tier features.
|
|
398
105
|
|
|
399
|
-
|
|
400
|
-
import ReactEcs, { UiEntity, Button, Label } from '@dcl/sdk/react-ecs'
|
|
401
|
-
|
|
402
|
-
// Enable pro features first
|
|
403
|
-
staticTV.enableProFeatures({ sceneId: 'my-scene', ... })
|
|
404
|
-
|
|
405
|
-
// Register custom tabs
|
|
406
|
-
staticTV.registerSceneTab({
|
|
407
|
-
label: 'LIGHTS',
|
|
408
|
-
id: 'lights',
|
|
409
|
-
render: () => (
|
|
410
|
-
<UiEntity uiTransform={{ flexDirection: 'column', padding: 8 }}>
|
|
411
|
-
<Label value="Light Controls" fontSize={14} />
|
|
412
|
-
<Button value="Disco Mode" onMouseDown={() => setDiscoLights()} />
|
|
413
|
-
<Button value="Ambient" onMouseDown={() => setAmbientLights()} />
|
|
414
|
-
<Button value="Off" onMouseDown={() => turnOffLights()} />
|
|
415
|
-
</UiEntity>
|
|
416
|
-
)
|
|
417
|
-
})
|
|
418
|
-
|
|
419
|
-
staticTV.registerSceneTab({
|
|
420
|
-
label: 'DOORS',
|
|
421
|
-
id: 'doors',
|
|
422
|
-
render: () => <MyDoorsControls />
|
|
423
|
-
})
|
|
424
|
-
```
|
|
106
|
+
All new keys include a **7-day free trial**.
|
|
425
107
|
|
|
426
|
-
|
|
108
|
+
## Documentation
|
|
427
109
|
|
|
428
|
-
|
|
110
|
+
Full API reference and Pro tier features (Admin Panel, Custom Tabs) available at:
|
|
429
111
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
import ReactEcs, { ReactEcsRenderer, UiEntity } from '@dcl/sdk/react-ecs'
|
|
434
|
-
|
|
435
|
-
// Outside main() - required by DCL
|
|
436
|
-
ReactEcsRenderer.setUiRenderer(() => {
|
|
437
|
-
if (!staticTV) return null
|
|
438
|
-
return ReactEcs.createElement(UiEntity, {
|
|
439
|
-
uiTransform: { width: '100%', height: '100%', positionType: 'absolute' },
|
|
440
|
-
children: [
|
|
441
|
-
staticTV.guideUI?.getComponent(),
|
|
442
|
-
staticTV.chatUI?.getComponent()
|
|
443
|
-
].filter(Boolean)
|
|
444
|
-
})
|
|
445
|
-
})
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Metrics Attribution
|
|
449
|
-
|
|
450
|
-
This SDK implements dual attribution:
|
|
451
|
-
|
|
452
|
-
- **Watched Channel**: Gets view metrics (minutes watched, likes, follows)
|
|
453
|
-
- **Scene Owner**: Gets referral metrics (unique visitors, traffic driven)
|
|
454
|
-
|
|
455
|
-
Your channel (linked to your API key) receives credit for all traffic your scene drives to thestatic.tv channels.
|
|
456
|
-
|
|
457
|
-
## Types
|
|
458
|
-
|
|
459
|
-
```typescript
|
|
460
|
-
interface Channel {
|
|
461
|
-
id: string
|
|
462
|
-
slug: string
|
|
463
|
-
name: string
|
|
464
|
-
streamUrl: string
|
|
465
|
-
isLive: boolean
|
|
466
|
-
currentViewers: number
|
|
467
|
-
poster: string | null
|
|
468
|
-
logo: string | null
|
|
469
|
-
description: string
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
interface Vod {
|
|
473
|
-
id: string
|
|
474
|
-
title: string
|
|
475
|
-
url: string
|
|
476
|
-
thumbnail: string | null
|
|
477
|
-
channelId: string
|
|
478
|
-
}
|
|
479
|
-
```
|
|
480
|
-
|
|
481
|
-
## Requirements
|
|
482
|
-
|
|
483
|
-
- Decentraland SDK 7.0.0 or higher
|
|
484
|
-
- API key from thestatic.tv
|
|
112
|
+
- **Docs**: [thestatic.tv/info](https://thestatic.tv/info)
|
|
113
|
+
- **Examples**: [github.com/thestatic-tv/thestatic-dcl-starter](https://github.com/thestatic-tv/thestatic-dcl-starter)
|
|
485
114
|
|
|
486
115
|
## Support
|
|
487
116
|
|
|
488
|
-
- Documentation: [thestatic.tv/info](https://thestatic.tv/info)
|
|
489
|
-
- Issues: [GitHub Issues](https://github.com/thestatic-tv/dcl-sdk/issues)
|
|
490
117
|
- Discord: [thestatic.tv Discord](https://discord.gg/thestatic)
|
|
118
|
+
- Issues: [GitHub Issues](https://github.com/thestatic-tv/dcl-sdk/issues)
|
|
491
119
|
|
|
492
120
|
## License
|
|
493
121
|
|
package/dist/index.d.mts
CHANGED
|
@@ -45,6 +45,12 @@ interface StaticTVConfig {
|
|
|
45
45
|
* ```
|
|
46
46
|
*/
|
|
47
47
|
videoScreen?: Entity;
|
|
48
|
+
/**
|
|
49
|
+
* Fallback video URL to play when no stream is active or stopVideo() is called.
|
|
50
|
+
* Prevents "broken screen" appearance. Set to empty string to disable.
|
|
51
|
+
* @default 'https://media.thestatic.tv/fallback-loop.mp4'
|
|
52
|
+
*/
|
|
53
|
+
fallbackVideoUrl?: string;
|
|
48
54
|
/**
|
|
49
55
|
* Scene ID for Admin Panel API calls.
|
|
50
56
|
* Optional - defaults to API key ID if not provided.
|
|
@@ -863,6 +869,9 @@ declare class StaticTVClient {
|
|
|
863
869
|
private _standardFeaturesEnabled;
|
|
864
870
|
private _proFeaturesEnabled;
|
|
865
871
|
private _pendingProConfig;
|
|
872
|
+
private _featuresReadyPromise;
|
|
873
|
+
private _featuresReadyResolve;
|
|
874
|
+
private _featuresResolved;
|
|
866
875
|
/** Guide module - fetch channel lineup (standard/pro tier) */
|
|
867
876
|
guide: GuideModule | null;
|
|
868
877
|
/** Session module - track visitor sessions (all tiers, null when disabled) */
|
|
@@ -933,6 +942,33 @@ declare class StaticTVClient {
|
|
|
933
942
|
* @deprecated Use `isFree` instead. Kept for backward compatibility.
|
|
934
943
|
*/
|
|
935
944
|
get isLite(): boolean;
|
|
945
|
+
/**
|
|
946
|
+
* Wait for tier confirmation from the server.
|
|
947
|
+
* Resolves when the SDK knows which features are available.
|
|
948
|
+
*
|
|
949
|
+
* Use this instead of polling `isLite` or `isFree` in a while loop.
|
|
950
|
+
*
|
|
951
|
+
* @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
|
|
956
|
+
*
|
|
957
|
+
* // Wait for tier to be confirmed
|
|
958
|
+
* const tier = await staticTV.onFeaturesReady()
|
|
959
|
+
*
|
|
960
|
+
* // Now modules are guaranteed to be initialized (or confirmed unavailable)
|
|
961
|
+
* if (staticTV.guideUI) {
|
|
962
|
+
* await staticTV.guideUI.init()
|
|
963
|
+
* console.log('Guide ready!')
|
|
964
|
+
* }
|
|
965
|
+
*
|
|
966
|
+
* if (tier === 'free') {
|
|
967
|
+
* console.log('Upgrade at thestatic.tv for Guide & Chat!')
|
|
968
|
+
* }
|
|
969
|
+
* ```
|
|
970
|
+
*/
|
|
971
|
+
onFeaturesReady(): Promise<SDKTier>;
|
|
936
972
|
/**
|
|
937
973
|
* Make an authenticated API request
|
|
938
974
|
* @internal
|
|
@@ -968,9 +1004,15 @@ declare class StaticTVClient {
|
|
|
968
1004
|
playVideo(url: string): void;
|
|
969
1005
|
/**
|
|
970
1006
|
* Stop video playback on the configured videoScreen entity.
|
|
1007
|
+
* If fallbackVideoUrl is configured (default), plays the fallback loop.
|
|
971
1008
|
* Called by Admin Panel.
|
|
972
1009
|
*/
|
|
973
1010
|
stopVideo(): void;
|
|
1011
|
+
/**
|
|
1012
|
+
* Internal video player - handles both regular videos and fallback
|
|
1013
|
+
* @internal
|
|
1014
|
+
*/
|
|
1015
|
+
private _playVideoInternal;
|
|
974
1016
|
/**
|
|
975
1017
|
* Get the currently playing video URL
|
|
976
1018
|
*/
|
|
@@ -995,6 +1037,11 @@ declare class StaticTVClient {
|
|
|
995
1037
|
* @internal
|
|
996
1038
|
*/
|
|
997
1039
|
private _initProModules;
|
|
1040
|
+
/**
|
|
1041
|
+
* Resolve the features ready promise (only once)
|
|
1042
|
+
* @internal
|
|
1043
|
+
*/
|
|
1044
|
+
private _resolveFeaturesReady;
|
|
998
1045
|
/**
|
|
999
1046
|
* Called by SessionModule when server returns the tier
|
|
1000
1047
|
* Enables modules based on tier level
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,12 @@ interface StaticTVConfig {
|
|
|
45
45
|
* ```
|
|
46
46
|
*/
|
|
47
47
|
videoScreen?: Entity;
|
|
48
|
+
/**
|
|
49
|
+
* Fallback video URL to play when no stream is active or stopVideo() is called.
|
|
50
|
+
* Prevents "broken screen" appearance. Set to empty string to disable.
|
|
51
|
+
* @default 'https://media.thestatic.tv/fallback-loop.mp4'
|
|
52
|
+
*/
|
|
53
|
+
fallbackVideoUrl?: string;
|
|
48
54
|
/**
|
|
49
55
|
* Scene ID for Admin Panel API calls.
|
|
50
56
|
* Optional - defaults to API key ID if not provided.
|
|
@@ -863,6 +869,9 @@ declare class StaticTVClient {
|
|
|
863
869
|
private _standardFeaturesEnabled;
|
|
864
870
|
private _proFeaturesEnabled;
|
|
865
871
|
private _pendingProConfig;
|
|
872
|
+
private _featuresReadyPromise;
|
|
873
|
+
private _featuresReadyResolve;
|
|
874
|
+
private _featuresResolved;
|
|
866
875
|
/** Guide module - fetch channel lineup (standard/pro tier) */
|
|
867
876
|
guide: GuideModule | null;
|
|
868
877
|
/** Session module - track visitor sessions (all tiers, null when disabled) */
|
|
@@ -933,6 +942,33 @@ declare class StaticTVClient {
|
|
|
933
942
|
* @deprecated Use `isFree` instead. Kept for backward compatibility.
|
|
934
943
|
*/
|
|
935
944
|
get isLite(): boolean;
|
|
945
|
+
/**
|
|
946
|
+
* Wait for tier confirmation from the server.
|
|
947
|
+
* Resolves when the SDK knows which features are available.
|
|
948
|
+
*
|
|
949
|
+
* Use this instead of polling `isLite` or `isFree` in a while loop.
|
|
950
|
+
*
|
|
951
|
+
* @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
|
|
952
|
+
*
|
|
953
|
+
* @example
|
|
954
|
+
* ```typescript
|
|
955
|
+
* const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
|
|
956
|
+
*
|
|
957
|
+
* // Wait for tier to be confirmed
|
|
958
|
+
* const tier = await staticTV.onFeaturesReady()
|
|
959
|
+
*
|
|
960
|
+
* // Now modules are guaranteed to be initialized (or confirmed unavailable)
|
|
961
|
+
* if (staticTV.guideUI) {
|
|
962
|
+
* await staticTV.guideUI.init()
|
|
963
|
+
* console.log('Guide ready!')
|
|
964
|
+
* }
|
|
965
|
+
*
|
|
966
|
+
* if (tier === 'free') {
|
|
967
|
+
* console.log('Upgrade at thestatic.tv for Guide & Chat!')
|
|
968
|
+
* }
|
|
969
|
+
* ```
|
|
970
|
+
*/
|
|
971
|
+
onFeaturesReady(): Promise<SDKTier>;
|
|
936
972
|
/**
|
|
937
973
|
* Make an authenticated API request
|
|
938
974
|
* @internal
|
|
@@ -968,9 +1004,15 @@ declare class StaticTVClient {
|
|
|
968
1004
|
playVideo(url: string): void;
|
|
969
1005
|
/**
|
|
970
1006
|
* Stop video playback on the configured videoScreen entity.
|
|
1007
|
+
* If fallbackVideoUrl is configured (default), plays the fallback loop.
|
|
971
1008
|
* Called by Admin Panel.
|
|
972
1009
|
*/
|
|
973
1010
|
stopVideo(): void;
|
|
1011
|
+
/**
|
|
1012
|
+
* Internal video player - handles both regular videos and fallback
|
|
1013
|
+
* @internal
|
|
1014
|
+
*/
|
|
1015
|
+
private _playVideoInternal;
|
|
974
1016
|
/**
|
|
975
1017
|
* Get the currently playing video URL
|
|
976
1018
|
*/
|
|
@@ -995,6 +1037,11 @@ declare class StaticTVClient {
|
|
|
995
1037
|
* @internal
|
|
996
1038
|
*/
|
|
997
1039
|
private _initProModules;
|
|
1040
|
+
/**
|
|
1041
|
+
* Resolve the features ready promise (only once)
|
|
1042
|
+
* @internal
|
|
1043
|
+
*/
|
|
1044
|
+
private _resolveFeaturesReady;
|
|
998
1045
|
/**
|
|
999
1046
|
* Called by SessionModule when server returns the tier
|
|
1000
1047
|
* Enables modules based on tier level
|
package/dist/index.js
CHANGED
|
@@ -2540,7 +2540,7 @@ var AdminPanelUIModule = class {
|
|
|
2540
2540
|
{
|
|
2541
2541
|
uiTransform: { height: this.s(24) },
|
|
2542
2542
|
uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
|
|
2543
|
-
value: "Edit slots at thestatic.tv
|
|
2543
|
+
value: "Edit slots at thestatic.tv",
|
|
2544
2544
|
fontSize: t.labelSmall,
|
|
2545
2545
|
color: C.cyan,
|
|
2546
2546
|
onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -2695,7 +2695,7 @@ var AdminPanelUIModule = class {
|
|
|
2695
2695
|
{
|
|
2696
2696
|
uiTransform: { height: this.s(24) },
|
|
2697
2697
|
uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
|
|
2698
|
-
value: "Manage at thestatic.tv
|
|
2698
|
+
value: "Manage at thestatic.tv",
|
|
2699
2699
|
fontSize: t.labelSmall,
|
|
2700
2700
|
color: C.cyan,
|
|
2701
2701
|
onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -2821,7 +2821,7 @@ var AdminPanelUIModule = class {
|
|
|
2821
2821
|
{
|
|
2822
2822
|
uiTransform: { height: this.s(24) },
|
|
2823
2823
|
uiBackground: { color: import_math5.Color4.create(0, 0, 0, 0) },
|
|
2824
|
-
value: `thestatic.tv/scene/${this.config.sceneId}
|
|
2824
|
+
value: `thestatic.tv/scene/${this.config.sceneId}`,
|
|
2825
2825
|
fontSize: t.labelSmall,
|
|
2826
2826
|
color: C.cyan,
|
|
2827
2827
|
onMouseDown: () => (0, import_RestrictedActions3.openExternalUrl)({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -3494,6 +3494,7 @@ function setupStaticUI(client) {
|
|
|
3494
3494
|
// src/StaticTVClient.ts
|
|
3495
3495
|
var import_ecs3 = require("@dcl/sdk/ecs");
|
|
3496
3496
|
var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
|
|
3497
|
+
var DEFAULT_FALLBACK_VIDEO = "https://media.thestatic.tv/fallback-loop.mp4";
|
|
3497
3498
|
var KEY_TYPE_CHANNEL = "channel";
|
|
3498
3499
|
var KEY_TYPE_SCENE = "scene";
|
|
3499
3500
|
var StaticTVClient = class {
|
|
@@ -3535,6 +3536,7 @@ var StaticTVClient = class {
|
|
|
3535
3536
|
this._standardFeaturesEnabled = false;
|
|
3536
3537
|
this._proFeaturesEnabled = false;
|
|
3537
3538
|
this._pendingProConfig = null;
|
|
3539
|
+
this._featuresResolved = false;
|
|
3538
3540
|
/** Guide module - fetch channel lineup (standard/pro tier) */
|
|
3539
3541
|
this.guide = null;
|
|
3540
3542
|
/** Session module - track visitor sessions (all tiers, null when disabled) */
|
|
@@ -3555,6 +3557,9 @@ var StaticTVClient = class {
|
|
|
3555
3557
|
// --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
|
|
3556
3558
|
// =============================================================================
|
|
3557
3559
|
this._currentVideoUrl = "";
|
|
3560
|
+
this._featuresReadyPromise = new Promise((resolve) => {
|
|
3561
|
+
this._featuresReadyResolve = resolve;
|
|
3562
|
+
});
|
|
3558
3563
|
this.config = {
|
|
3559
3564
|
autoStartSession: true,
|
|
3560
3565
|
sessionHeartbeatInterval: 3e4,
|
|
@@ -3572,6 +3577,7 @@ var StaticTVClient = class {
|
|
|
3572
3577
|
this.interactions = null;
|
|
3573
3578
|
this.guideUI = null;
|
|
3574
3579
|
this.chatUI = null;
|
|
3580
|
+
this._resolveFeaturesReady("free");
|
|
3575
3581
|
return;
|
|
3576
3582
|
}
|
|
3577
3583
|
if (config.apiKey.startsWith("dclk_")) {
|
|
@@ -3582,12 +3588,14 @@ var StaticTVClient = class {
|
|
|
3582
3588
|
console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
|
|
3583
3589
|
this._disabled = true;
|
|
3584
3590
|
this._keyType = null;
|
|
3591
|
+
this._resolveFeaturesReady("free");
|
|
3585
3592
|
return;
|
|
3586
3593
|
}
|
|
3587
3594
|
this.session = new SessionModule(this);
|
|
3588
3595
|
if (this._keyType === KEY_TYPE_CHANNEL) {
|
|
3589
3596
|
this._tier = "standard";
|
|
3590
3597
|
this._initStandardModules();
|
|
3598
|
+
this._resolveFeaturesReady("standard");
|
|
3591
3599
|
}
|
|
3592
3600
|
if (this.config.autoStartSession) {
|
|
3593
3601
|
fetchUserData().then(() => {
|
|
@@ -3638,6 +3646,35 @@ var StaticTVClient = class {
|
|
|
3638
3646
|
get isLite() {
|
|
3639
3647
|
return this.isFree;
|
|
3640
3648
|
}
|
|
3649
|
+
/**
|
|
3650
|
+
* Wait for tier confirmation from the server.
|
|
3651
|
+
* Resolves when the SDK knows which features are available.
|
|
3652
|
+
*
|
|
3653
|
+
* Use this instead of polling `isLite` or `isFree` in a while loop.
|
|
3654
|
+
*
|
|
3655
|
+
* @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
|
|
3656
|
+
*
|
|
3657
|
+
* @example
|
|
3658
|
+
* ```typescript
|
|
3659
|
+
* const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
|
|
3660
|
+
*
|
|
3661
|
+
* // Wait for tier to be confirmed
|
|
3662
|
+
* const tier = await staticTV.onFeaturesReady()
|
|
3663
|
+
*
|
|
3664
|
+
* // Now modules are guaranteed to be initialized (or confirmed unavailable)
|
|
3665
|
+
* if (staticTV.guideUI) {
|
|
3666
|
+
* await staticTV.guideUI.init()
|
|
3667
|
+
* console.log('Guide ready!')
|
|
3668
|
+
* }
|
|
3669
|
+
*
|
|
3670
|
+
* if (tier === 'free') {
|
|
3671
|
+
* console.log('Upgrade at thestatic.tv for Guide & Chat!')
|
|
3672
|
+
* }
|
|
3673
|
+
* ```
|
|
3674
|
+
*/
|
|
3675
|
+
async onFeaturesReady() {
|
|
3676
|
+
return this._featuresReadyPromise;
|
|
3677
|
+
}
|
|
3641
3678
|
/**
|
|
3642
3679
|
* Make an authenticated API request
|
|
3643
3680
|
* @internal
|
|
@@ -3695,17 +3732,7 @@ var StaticTVClient = class {
|
|
|
3695
3732
|
if (screen !== void 0) {
|
|
3696
3733
|
this.log(`Playing video: ${url}`);
|
|
3697
3734
|
this._currentVideoUrl = url;
|
|
3698
|
-
|
|
3699
|
-
import_ecs3.VideoPlayer.deleteFrom(screen);
|
|
3700
|
-
}
|
|
3701
|
-
import_ecs3.VideoPlayer.create(screen, {
|
|
3702
|
-
src: url,
|
|
3703
|
-
playing: true,
|
|
3704
|
-
volume: 1
|
|
3705
|
-
});
|
|
3706
|
-
import_ecs3.Material.setBasicMaterial(screen, {
|
|
3707
|
-
texture: import_ecs3.Material.Texture.Video({ videoPlayerEntity: screen })
|
|
3708
|
-
});
|
|
3735
|
+
this._playVideoInternal(url, false);
|
|
3709
3736
|
if (this.guideUI) {
|
|
3710
3737
|
const videos = this.guideUI.getVideos();
|
|
3711
3738
|
const video = videos.find((v) => v.src === url);
|
|
@@ -3720,22 +3747,55 @@ var StaticTVClient = class {
|
|
|
3720
3747
|
}
|
|
3721
3748
|
/**
|
|
3722
3749
|
* Stop video playback on the configured videoScreen entity.
|
|
3750
|
+
* If fallbackVideoUrl is configured (default), plays the fallback loop.
|
|
3723
3751
|
* Called by Admin Panel.
|
|
3724
3752
|
*/
|
|
3725
3753
|
stopVideo() {
|
|
3726
3754
|
const screen = this.config.videoScreen;
|
|
3727
|
-
if (screen !== void 0
|
|
3728
|
-
this.log("Stopping video");
|
|
3729
|
-
import_ecs3.VideoPlayer.getMutable(screen).playing = false;
|
|
3730
|
-
this._currentVideoUrl = "";
|
|
3755
|
+
if (screen !== void 0) {
|
|
3731
3756
|
if (this.guideUI) {
|
|
3732
3757
|
this.guideUI.currentVideoId = null;
|
|
3733
3758
|
}
|
|
3759
|
+
const fallbackUrl = this.config.fallbackVideoUrl;
|
|
3760
|
+
const fallbackDisabled = fallbackUrl === "";
|
|
3761
|
+
if (fallbackDisabled) {
|
|
3762
|
+
if (import_ecs3.VideoPlayer.has(screen)) {
|
|
3763
|
+
this.log("Stopping video (no fallback)");
|
|
3764
|
+
import_ecs3.VideoPlayer.getMutable(screen).playing = false;
|
|
3765
|
+
}
|
|
3766
|
+
this._currentVideoUrl = "";
|
|
3767
|
+
} else {
|
|
3768
|
+
const url = fallbackUrl || DEFAULT_FALLBACK_VIDEO;
|
|
3769
|
+
this.log(`Playing fallback: ${url}`);
|
|
3770
|
+
this._currentVideoUrl = "";
|
|
3771
|
+
this._playVideoInternal(url, true);
|
|
3772
|
+
}
|
|
3734
3773
|
}
|
|
3735
3774
|
if (this.config.onVideoStop) {
|
|
3736
3775
|
this.config.onVideoStop();
|
|
3737
3776
|
}
|
|
3738
3777
|
}
|
|
3778
|
+
/**
|
|
3779
|
+
* Internal video player - handles both regular videos and fallback
|
|
3780
|
+
* @internal
|
|
3781
|
+
*/
|
|
3782
|
+
_playVideoInternal(url, isFallback = false) {
|
|
3783
|
+
const screen = this.config.videoScreen;
|
|
3784
|
+
if (screen === void 0) return;
|
|
3785
|
+
if (import_ecs3.VideoPlayer.has(screen)) {
|
|
3786
|
+
import_ecs3.VideoPlayer.deleteFrom(screen);
|
|
3787
|
+
}
|
|
3788
|
+
import_ecs3.VideoPlayer.create(screen, {
|
|
3789
|
+
src: url,
|
|
3790
|
+
playing: true,
|
|
3791
|
+
loop: isFallback,
|
|
3792
|
+
// Loop fallback videos
|
|
3793
|
+
volume: 1
|
|
3794
|
+
});
|
|
3795
|
+
import_ecs3.Material.setBasicMaterial(screen, {
|
|
3796
|
+
texture: import_ecs3.Material.Texture.Video({ videoPlayerEntity: screen })
|
|
3797
|
+
});
|
|
3798
|
+
}
|
|
3739
3799
|
/**
|
|
3740
3800
|
* Get the currently playing video URL
|
|
3741
3801
|
*/
|
|
@@ -3818,6 +3878,16 @@ var StaticTVClient = class {
|
|
|
3818
3878
|
});
|
|
3819
3879
|
this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
|
|
3820
3880
|
}
|
|
3881
|
+
/**
|
|
3882
|
+
* Resolve the features ready promise (only once)
|
|
3883
|
+
* @internal
|
|
3884
|
+
*/
|
|
3885
|
+
_resolveFeaturesReady(tier) {
|
|
3886
|
+
if (!this._featuresResolved) {
|
|
3887
|
+
this._featuresResolved = true;
|
|
3888
|
+
this._featuresReadyResolve(tier);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3821
3891
|
/**
|
|
3822
3892
|
* Called by SessionModule when server returns the tier
|
|
3823
3893
|
* Enables modules based on tier level
|
|
@@ -3839,6 +3909,7 @@ var StaticTVClient = class {
|
|
|
3839
3909
|
this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
|
|
3840
3910
|
}
|
|
3841
3911
|
}
|
|
3912
|
+
this._resolveFeaturesReady(tier);
|
|
3842
3913
|
}
|
|
3843
3914
|
/**
|
|
3844
3915
|
* @deprecated Use `_enableFeaturesForTier` instead
|
package/dist/index.mjs
CHANGED
|
@@ -2497,7 +2497,7 @@ var AdminPanelUIModule = class {
|
|
|
2497
2497
|
{
|
|
2498
2498
|
uiTransform: { height: this.s(24) },
|
|
2499
2499
|
uiBackground: { color: Color45.create(0, 0, 0, 0) },
|
|
2500
|
-
value: "Edit slots at thestatic.tv
|
|
2500
|
+
value: "Edit slots at thestatic.tv",
|
|
2501
2501
|
fontSize: t.labelSmall,
|
|
2502
2502
|
color: C.cyan,
|
|
2503
2503
|
onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -2652,7 +2652,7 @@ var AdminPanelUIModule = class {
|
|
|
2652
2652
|
{
|
|
2653
2653
|
uiTransform: { height: this.s(24) },
|
|
2654
2654
|
uiBackground: { color: Color45.create(0, 0, 0, 0) },
|
|
2655
|
-
value: "Manage at thestatic.tv
|
|
2655
|
+
value: "Manage at thestatic.tv",
|
|
2656
2656
|
fontSize: t.labelSmall,
|
|
2657
2657
|
color: C.cyan,
|
|
2658
2658
|
onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -2778,7 +2778,7 @@ var AdminPanelUIModule = class {
|
|
|
2778
2778
|
{
|
|
2779
2779
|
uiTransform: { height: this.s(24) },
|
|
2780
2780
|
uiBackground: { color: Color45.create(0, 0, 0, 0) },
|
|
2781
|
-
value: `thestatic.tv/scene/${this.config.sceneId}
|
|
2781
|
+
value: `thestatic.tv/scene/${this.config.sceneId}`,
|
|
2782
2782
|
fontSize: t.labelSmall,
|
|
2783
2783
|
color: C.cyan,
|
|
2784
2784
|
onMouseDown: () => openExternalUrl3({ url: this.config.footerLink || `https://thestatic.tv/scene/${this.config.sceneId}` })
|
|
@@ -3451,6 +3451,7 @@ function setupStaticUI(client) {
|
|
|
3451
3451
|
// src/StaticTVClient.ts
|
|
3452
3452
|
import { VideoPlayer, Material } from "@dcl/sdk/ecs";
|
|
3453
3453
|
var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
|
|
3454
|
+
var DEFAULT_FALLBACK_VIDEO = "https://media.thestatic.tv/fallback-loop.mp4";
|
|
3454
3455
|
var KEY_TYPE_CHANNEL = "channel";
|
|
3455
3456
|
var KEY_TYPE_SCENE = "scene";
|
|
3456
3457
|
var StaticTVClient = class {
|
|
@@ -3492,6 +3493,7 @@ var StaticTVClient = class {
|
|
|
3492
3493
|
this._standardFeaturesEnabled = false;
|
|
3493
3494
|
this._proFeaturesEnabled = false;
|
|
3494
3495
|
this._pendingProConfig = null;
|
|
3496
|
+
this._featuresResolved = false;
|
|
3495
3497
|
/** Guide module - fetch channel lineup (standard/pro tier) */
|
|
3496
3498
|
this.guide = null;
|
|
3497
3499
|
/** Session module - track visitor sessions (all tiers, null when disabled) */
|
|
@@ -3512,6 +3514,9 @@ var StaticTVClient = class {
|
|
|
3512
3514
|
// --- VIDEO PLAYBACK (Internal handler for videoScreen) ---
|
|
3513
3515
|
// =============================================================================
|
|
3514
3516
|
this._currentVideoUrl = "";
|
|
3517
|
+
this._featuresReadyPromise = new Promise((resolve) => {
|
|
3518
|
+
this._featuresReadyResolve = resolve;
|
|
3519
|
+
});
|
|
3515
3520
|
this.config = {
|
|
3516
3521
|
autoStartSession: true,
|
|
3517
3522
|
sessionHeartbeatInterval: 3e4,
|
|
@@ -3529,6 +3534,7 @@ var StaticTVClient = class {
|
|
|
3529
3534
|
this.interactions = null;
|
|
3530
3535
|
this.guideUI = null;
|
|
3531
3536
|
this.chatUI = null;
|
|
3537
|
+
this._resolveFeaturesReady("free");
|
|
3532
3538
|
return;
|
|
3533
3539
|
}
|
|
3534
3540
|
if (config.apiKey.startsWith("dclk_")) {
|
|
@@ -3539,12 +3545,14 @@ var StaticTVClient = class {
|
|
|
3539
3545
|
console.warn("[TheStatic] Invalid API key format - get your key at thestatic.tv/dashboard");
|
|
3540
3546
|
this._disabled = true;
|
|
3541
3547
|
this._keyType = null;
|
|
3548
|
+
this._resolveFeaturesReady("free");
|
|
3542
3549
|
return;
|
|
3543
3550
|
}
|
|
3544
3551
|
this.session = new SessionModule(this);
|
|
3545
3552
|
if (this._keyType === KEY_TYPE_CHANNEL) {
|
|
3546
3553
|
this._tier = "standard";
|
|
3547
3554
|
this._initStandardModules();
|
|
3555
|
+
this._resolveFeaturesReady("standard");
|
|
3548
3556
|
}
|
|
3549
3557
|
if (this.config.autoStartSession) {
|
|
3550
3558
|
fetchUserData().then(() => {
|
|
@@ -3595,6 +3603,35 @@ var StaticTVClient = class {
|
|
|
3595
3603
|
get isLite() {
|
|
3596
3604
|
return this.isFree;
|
|
3597
3605
|
}
|
|
3606
|
+
/**
|
|
3607
|
+
* Wait for tier confirmation from the server.
|
|
3608
|
+
* Resolves when the SDK knows which features are available.
|
|
3609
|
+
*
|
|
3610
|
+
* Use this instead of polling `isLite` or `isFree` in a while loop.
|
|
3611
|
+
*
|
|
3612
|
+
* @returns Promise that resolves with the confirmed tier ('free', 'standard', or 'pro')
|
|
3613
|
+
*
|
|
3614
|
+
* @example
|
|
3615
|
+
* ```typescript
|
|
3616
|
+
* const staticTV = new StaticTVClient({ apiKey: 'dcls_...' })
|
|
3617
|
+
*
|
|
3618
|
+
* // Wait for tier to be confirmed
|
|
3619
|
+
* const tier = await staticTV.onFeaturesReady()
|
|
3620
|
+
*
|
|
3621
|
+
* // Now modules are guaranteed to be initialized (or confirmed unavailable)
|
|
3622
|
+
* if (staticTV.guideUI) {
|
|
3623
|
+
* await staticTV.guideUI.init()
|
|
3624
|
+
* console.log('Guide ready!')
|
|
3625
|
+
* }
|
|
3626
|
+
*
|
|
3627
|
+
* if (tier === 'free') {
|
|
3628
|
+
* console.log('Upgrade at thestatic.tv for Guide & Chat!')
|
|
3629
|
+
* }
|
|
3630
|
+
* ```
|
|
3631
|
+
*/
|
|
3632
|
+
async onFeaturesReady() {
|
|
3633
|
+
return this._featuresReadyPromise;
|
|
3634
|
+
}
|
|
3598
3635
|
/**
|
|
3599
3636
|
* Make an authenticated API request
|
|
3600
3637
|
* @internal
|
|
@@ -3652,17 +3689,7 @@ var StaticTVClient = class {
|
|
|
3652
3689
|
if (screen !== void 0) {
|
|
3653
3690
|
this.log(`Playing video: ${url}`);
|
|
3654
3691
|
this._currentVideoUrl = url;
|
|
3655
|
-
|
|
3656
|
-
VideoPlayer.deleteFrom(screen);
|
|
3657
|
-
}
|
|
3658
|
-
VideoPlayer.create(screen, {
|
|
3659
|
-
src: url,
|
|
3660
|
-
playing: true,
|
|
3661
|
-
volume: 1
|
|
3662
|
-
});
|
|
3663
|
-
Material.setBasicMaterial(screen, {
|
|
3664
|
-
texture: Material.Texture.Video({ videoPlayerEntity: screen })
|
|
3665
|
-
});
|
|
3692
|
+
this._playVideoInternal(url, false);
|
|
3666
3693
|
if (this.guideUI) {
|
|
3667
3694
|
const videos = this.guideUI.getVideos();
|
|
3668
3695
|
const video = videos.find((v) => v.src === url);
|
|
@@ -3677,22 +3704,55 @@ var StaticTVClient = class {
|
|
|
3677
3704
|
}
|
|
3678
3705
|
/**
|
|
3679
3706
|
* Stop video playback on the configured videoScreen entity.
|
|
3707
|
+
* If fallbackVideoUrl is configured (default), plays the fallback loop.
|
|
3680
3708
|
* Called by Admin Panel.
|
|
3681
3709
|
*/
|
|
3682
3710
|
stopVideo() {
|
|
3683
3711
|
const screen = this.config.videoScreen;
|
|
3684
|
-
if (screen !== void 0
|
|
3685
|
-
this.log("Stopping video");
|
|
3686
|
-
VideoPlayer.getMutable(screen).playing = false;
|
|
3687
|
-
this._currentVideoUrl = "";
|
|
3712
|
+
if (screen !== void 0) {
|
|
3688
3713
|
if (this.guideUI) {
|
|
3689
3714
|
this.guideUI.currentVideoId = null;
|
|
3690
3715
|
}
|
|
3716
|
+
const fallbackUrl = this.config.fallbackVideoUrl;
|
|
3717
|
+
const fallbackDisabled = fallbackUrl === "";
|
|
3718
|
+
if (fallbackDisabled) {
|
|
3719
|
+
if (VideoPlayer.has(screen)) {
|
|
3720
|
+
this.log("Stopping video (no fallback)");
|
|
3721
|
+
VideoPlayer.getMutable(screen).playing = false;
|
|
3722
|
+
}
|
|
3723
|
+
this._currentVideoUrl = "";
|
|
3724
|
+
} else {
|
|
3725
|
+
const url = fallbackUrl || DEFAULT_FALLBACK_VIDEO;
|
|
3726
|
+
this.log(`Playing fallback: ${url}`);
|
|
3727
|
+
this._currentVideoUrl = "";
|
|
3728
|
+
this._playVideoInternal(url, true);
|
|
3729
|
+
}
|
|
3691
3730
|
}
|
|
3692
3731
|
if (this.config.onVideoStop) {
|
|
3693
3732
|
this.config.onVideoStop();
|
|
3694
3733
|
}
|
|
3695
3734
|
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Internal video player - handles both regular videos and fallback
|
|
3737
|
+
* @internal
|
|
3738
|
+
*/
|
|
3739
|
+
_playVideoInternal(url, isFallback = false) {
|
|
3740
|
+
const screen = this.config.videoScreen;
|
|
3741
|
+
if (screen === void 0) return;
|
|
3742
|
+
if (VideoPlayer.has(screen)) {
|
|
3743
|
+
VideoPlayer.deleteFrom(screen);
|
|
3744
|
+
}
|
|
3745
|
+
VideoPlayer.create(screen, {
|
|
3746
|
+
src: url,
|
|
3747
|
+
playing: true,
|
|
3748
|
+
loop: isFallback,
|
|
3749
|
+
// Loop fallback videos
|
|
3750
|
+
volume: 1
|
|
3751
|
+
});
|
|
3752
|
+
Material.setBasicMaterial(screen, {
|
|
3753
|
+
texture: Material.Texture.Video({ videoPlayerEntity: screen })
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3696
3756
|
/**
|
|
3697
3757
|
* Get the currently playing video URL
|
|
3698
3758
|
*/
|
|
@@ -3775,6 +3835,16 @@ var StaticTVClient = class {
|
|
|
3775
3835
|
});
|
|
3776
3836
|
this.log(`Pro features enabled (admin panel) - sceneId: ${sceneId}`);
|
|
3777
3837
|
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Resolve the features ready promise (only once)
|
|
3840
|
+
* @internal
|
|
3841
|
+
*/
|
|
3842
|
+
_resolveFeaturesReady(tier) {
|
|
3843
|
+
if (!this._featuresResolved) {
|
|
3844
|
+
this._featuresResolved = true;
|
|
3845
|
+
this._featuresReadyResolve(tier);
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3778
3848
|
/**
|
|
3779
3849
|
* Called by SessionModule when server returns the tier
|
|
3780
3850
|
* Enables modules based on tier level
|
|
@@ -3796,6 +3866,7 @@ var StaticTVClient = class {
|
|
|
3796
3866
|
this.log("Pro tier detected but no video config - call enableProFeatures() or set videoScreen to enable admin panel");
|
|
3797
3867
|
}
|
|
3798
3868
|
}
|
|
3869
|
+
this._resolveFeaturesReady(tier);
|
|
3799
3870
|
}
|
|
3800
3871
|
/**
|
|
3801
3872
|
* @deprecated Use `_enableFeaturesForTier` instead
|
package/package.json
CHANGED