@thestatic-tv/dcl-sdk 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/dist/index.mjs ADDED
@@ -0,0 +1,490 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
8
+ // src/utils/http.ts
9
+ async function request(url, options = {}) {
10
+ const { timeout = 1e4, ...fetchOptions } = options;
11
+ const controller = new AbortController();
12
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
13
+ try {
14
+ const response = await fetch(url, {
15
+ ...fetchOptions,
16
+ signal: controller.signal
17
+ });
18
+ if (!response.ok) {
19
+ const errorData = await response.json().catch(() => ({}));
20
+ throw new Error(errorData.error || `HTTP ${response.status}`);
21
+ }
22
+ return response.json();
23
+ } finally {
24
+ clearTimeout(timeoutId);
25
+ }
26
+ }
27
+
28
+ // src/modules/guide.ts
29
+ var GuideModule = class {
30
+ // 30 seconds
31
+ constructor(client) {
32
+ this.cachedChannels = null;
33
+ this.cacheTimestamp = 0;
34
+ this.cacheDuration = 3e4;
35
+ this.client = client;
36
+ }
37
+ /**
38
+ * Get all channels from thestatic.tv
39
+ * Results are cached for 30 seconds
40
+ */
41
+ async getChannels(forceRefresh = false) {
42
+ const now = Date.now();
43
+ if (!forceRefresh && this.cachedChannels && now - this.cacheTimestamp < this.cacheDuration) {
44
+ this.client.log("Returning cached channels");
45
+ return this.cachedChannels;
46
+ }
47
+ const response = await this.client.request("/guide");
48
+ this.cachedChannels = response.channels;
49
+ this.cacheTimestamp = now;
50
+ this.client.log(`Fetched ${response.channels.length} channels`);
51
+ return response.channels;
52
+ }
53
+ /**
54
+ * Get only live channels
55
+ */
56
+ async getLiveChannels(forceRefresh = false) {
57
+ const channels = await this.getChannels(forceRefresh);
58
+ return channels.filter((c) => c.isLive);
59
+ }
60
+ /**
61
+ * Get a specific channel by slug
62
+ */
63
+ async getChannel(slug) {
64
+ const channels = await this.getChannels();
65
+ return channels.find((c) => c.slug === slug);
66
+ }
67
+ /**
68
+ * Get VODs (videos on demand)
69
+ */
70
+ async getVods() {
71
+ const response = await this.client.request("/guide");
72
+ return response.vods;
73
+ }
74
+ /**
75
+ * Clear the channel cache
76
+ */
77
+ clearCache() {
78
+ this.cachedChannels = null;
79
+ this.cacheTimestamp = 0;
80
+ }
81
+ };
82
+
83
+ // src/utils/identity.ts
84
+ function getPlayerWallet() {
85
+ try {
86
+ const { getPlayer } = __require("@dcl/sdk/src/players");
87
+ const player = getPlayer();
88
+ return player?.userId ?? null;
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ function getPlayerDisplayName() {
94
+ try {
95
+ const { getPlayer } = __require("@dcl/sdk/src/players");
96
+ const player = getPlayer();
97
+ return player?.name ?? null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ // src/modules/session.ts
104
+ var SessionModule = class {
105
+ constructor(client) {
106
+ this.sessionId = null;
107
+ this.heartbeatInterval = null;
108
+ this.isActive = false;
109
+ this.client = client;
110
+ }
111
+ /**
112
+ * Get the appropriate session endpoint based on key type
113
+ */
114
+ getEndpoint() {
115
+ return this.client.isLite ? "/scene-session" : "/session";
116
+ }
117
+ /**
118
+ * Start a new session
119
+ * Called automatically if autoStartSession is true
120
+ */
121
+ async startSession(metadata) {
122
+ if (this.isActive) {
123
+ this.client.log("Session already active");
124
+ return this.sessionId;
125
+ }
126
+ try {
127
+ const response = await this.client.request(this.getEndpoint(), {
128
+ method: "POST",
129
+ body: JSON.stringify({
130
+ action: "enter",
131
+ walletAddress: getPlayerWallet(),
132
+ dclDisplayName: getPlayerDisplayName(),
133
+ metadata
134
+ })
135
+ });
136
+ if (response.success && response.sessionId) {
137
+ this.sessionId = response.sessionId;
138
+ this.isActive = true;
139
+ this.startHeartbeat();
140
+ this.client.log(`Session started: ${this.sessionId}`);
141
+ return this.sessionId;
142
+ }
143
+ return null;
144
+ } catch (error) {
145
+ this.client.log(`Failed to start session: ${error}`);
146
+ return null;
147
+ }
148
+ }
149
+ /**
150
+ * Start the heartbeat interval
151
+ */
152
+ startHeartbeat() {
153
+ if (this.heartbeatInterval) return;
154
+ const interval = this.client.getConfig().sessionHeartbeatInterval || 3e4;
155
+ this.heartbeatInterval = setInterval(() => {
156
+ this.sendHeartbeat();
157
+ }, interval);
158
+ }
159
+ /**
160
+ * Send a session heartbeat
161
+ */
162
+ async sendHeartbeat() {
163
+ if (!this.sessionId || !this.isActive) return;
164
+ try {
165
+ await this.client.request(this.getEndpoint(), {
166
+ method: "POST",
167
+ body: JSON.stringify({
168
+ action: "heartbeat",
169
+ sessionId: this.sessionId
170
+ })
171
+ });
172
+ this.client.log("Session heartbeat sent");
173
+ } catch (error) {
174
+ this.client.log(`Session heartbeat failed: ${error}`);
175
+ }
176
+ }
177
+ /**
178
+ * End the current session
179
+ */
180
+ async endSession() {
181
+ if (!this.isActive) return;
182
+ if (this.heartbeatInterval) {
183
+ clearInterval(this.heartbeatInterval);
184
+ this.heartbeatInterval = null;
185
+ }
186
+ if (this.sessionId) {
187
+ try {
188
+ await this.client.request(this.getEndpoint(), {
189
+ method: "POST",
190
+ body: JSON.stringify({
191
+ action: "leave",
192
+ sessionId: this.sessionId
193
+ })
194
+ });
195
+ this.client.log("Session ended");
196
+ } catch (error) {
197
+ this.client.log(`Failed to end session: ${error}`);
198
+ }
199
+ }
200
+ this.sessionId = null;
201
+ this.isActive = false;
202
+ }
203
+ /**
204
+ * Get the current session ID
205
+ */
206
+ getSessionId() {
207
+ return this.sessionId;
208
+ }
209
+ /**
210
+ * Check if a session is currently active
211
+ */
212
+ isSessionActive() {
213
+ return this.isActive;
214
+ }
215
+ };
216
+
217
+ // src/modules/heartbeat.ts
218
+ var HeartbeatModule = class {
219
+ constructor(client) {
220
+ this.watchInterval = null;
221
+ this.currentChannel = null;
222
+ this.isWatching = false;
223
+ this.client = client;
224
+ }
225
+ /**
226
+ * Start tracking video watching for a channel
227
+ * Sends heartbeats every minute while watching
228
+ *
229
+ * @param channelSlug The slug of the channel being watched
230
+ */
231
+ startWatching(channelSlug) {
232
+ if (this.isWatching && this.currentChannel === channelSlug) {
233
+ this.client.log(`Already watching ${channelSlug}`);
234
+ return;
235
+ }
236
+ if (this.isWatching && this.currentChannel !== channelSlug) {
237
+ this.stopWatching();
238
+ }
239
+ this.currentChannel = channelSlug;
240
+ this.isWatching = true;
241
+ this.sendHeartbeat();
242
+ const interval = this.client.getConfig().watchHeartbeatInterval || 6e4;
243
+ this.watchInterval = setInterval(() => {
244
+ this.sendHeartbeat();
245
+ }, interval);
246
+ this.client.log(`Started watching ${channelSlug}`);
247
+ }
248
+ /**
249
+ * Stop tracking video watching
250
+ */
251
+ stopWatching() {
252
+ if (!this.isWatching) return;
253
+ if (this.watchInterval) {
254
+ clearInterval(this.watchInterval);
255
+ this.watchInterval = null;
256
+ }
257
+ this.client.log(`Stopped watching ${this.currentChannel}`);
258
+ this.currentChannel = null;
259
+ this.isWatching = false;
260
+ }
261
+ /**
262
+ * Send a watching heartbeat (1 heartbeat = 1 minute watched)
263
+ */
264
+ async sendHeartbeat() {
265
+ if (!this.currentChannel || !this.isWatching) return;
266
+ try {
267
+ const sessionId = this.client.session.getSessionId();
268
+ await this.client.request("/heartbeat", {
269
+ method: "POST",
270
+ body: JSON.stringify({
271
+ channelSlug: this.currentChannel,
272
+ walletAddress: getPlayerWallet(),
273
+ sessionId
274
+ })
275
+ });
276
+ this.client.log(`Heartbeat sent for ${this.currentChannel}`);
277
+ } catch (error) {
278
+ this.client.log(`Heartbeat failed: ${error}`);
279
+ }
280
+ }
281
+ /**
282
+ * Get the currently watched channel
283
+ */
284
+ getCurrentChannel() {
285
+ return this.currentChannel;
286
+ }
287
+ /**
288
+ * Check if currently watching
289
+ */
290
+ isCurrentlyWatching() {
291
+ return this.isWatching;
292
+ }
293
+ };
294
+
295
+ // src/modules/interactions.ts
296
+ var InteractionsModule = class {
297
+ constructor(client) {
298
+ this.client = client;
299
+ }
300
+ /**
301
+ * Like a channel
302
+ * Requires the user to be connected with a wallet
303
+ *
304
+ * @param channelSlug The slug of the channel to like
305
+ * @returns The interaction response or null if failed
306
+ */
307
+ async like(channelSlug) {
308
+ const walletAddress = getPlayerWallet();
309
+ if (!walletAddress) {
310
+ this.client.log("Cannot like: wallet not connected");
311
+ return null;
312
+ }
313
+ try {
314
+ const response = await this.client.request("/interact", {
315
+ method: "POST",
316
+ body: JSON.stringify({
317
+ action: "like",
318
+ channelSlug,
319
+ walletAddress
320
+ })
321
+ });
322
+ if (response.alreadyExists) {
323
+ this.client.log(`Already liked ${channelSlug}`);
324
+ } else {
325
+ this.client.log(`Liked ${channelSlug}`);
326
+ }
327
+ return response;
328
+ } catch (error) {
329
+ this.client.log(`Failed to like ${channelSlug}: ${error}`);
330
+ return null;
331
+ }
332
+ }
333
+ /**
334
+ * Follow a channel
335
+ * Requires the user to be connected with a wallet
336
+ *
337
+ * @param channelSlug The slug of the channel to follow
338
+ * @returns The interaction response or null if failed
339
+ */
340
+ async follow(channelSlug) {
341
+ const walletAddress = getPlayerWallet();
342
+ if (!walletAddress) {
343
+ this.client.log("Cannot follow: wallet not connected");
344
+ return null;
345
+ }
346
+ try {
347
+ const response = await this.client.request("/interact", {
348
+ method: "POST",
349
+ body: JSON.stringify({
350
+ action: "follow",
351
+ channelSlug,
352
+ walletAddress
353
+ })
354
+ });
355
+ if (response.alreadyExists) {
356
+ this.client.log(`Already following ${channelSlug}`);
357
+ } else {
358
+ this.client.log(`Followed ${channelSlug}`);
359
+ }
360
+ return response;
361
+ } catch (error) {
362
+ this.client.log(`Failed to follow ${channelSlug}: ${error}`);
363
+ return null;
364
+ }
365
+ }
366
+ };
367
+
368
+ // src/StaticTVClient.ts
369
+ var DEFAULT_BASE_URL = "https://thestatic.tv/api/v1/dcl";
370
+ var KEY_TYPE_CHANNEL = "channel";
371
+ var KEY_TYPE_SCENE = "scene";
372
+ var StaticTVClient = class {
373
+ /**
374
+ * Create a new StaticTVClient
375
+ *
376
+ * @param config Configuration options
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * // Full access with channel key
381
+ * const staticTV = new StaticTVClient({
382
+ * apiKey: 'dclk_your_channel_key_here',
383
+ * debug: true
384
+ * });
385
+ *
386
+ * // Lite mode with scene key (visitors only)
387
+ * const staticTV = new StaticTVClient({
388
+ * apiKey: 'dcls_your_scene_key_here'
389
+ * });
390
+ * ```
391
+ */
392
+ constructor(config) {
393
+ if (!config.apiKey) {
394
+ throw new Error("StaticTVClient: apiKey is required");
395
+ }
396
+ if (config.apiKey.startsWith("dclk_")) {
397
+ this._keyType = KEY_TYPE_CHANNEL;
398
+ } else if (config.apiKey.startsWith("dcls_")) {
399
+ this._keyType = KEY_TYPE_SCENE;
400
+ } else {
401
+ throw new Error("StaticTVClient: invalid apiKey format. Must start with dclk_ or dcls_");
402
+ }
403
+ this.config = {
404
+ autoStartSession: true,
405
+ sessionHeartbeatInterval: 3e4,
406
+ watchHeartbeatInterval: 6e4,
407
+ debug: false,
408
+ ...config
409
+ };
410
+ this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
411
+ this.session = new SessionModule(this);
412
+ if (this._keyType === KEY_TYPE_CHANNEL) {
413
+ this.guide = new GuideModule(this);
414
+ this.heartbeat = new HeartbeatModule(this);
415
+ this.interactions = new InteractionsModule(this);
416
+ } else {
417
+ this.guide = null;
418
+ this.heartbeat = null;
419
+ this.interactions = null;
420
+ }
421
+ if (this.config.autoStartSession) {
422
+ this.session.startSession().catch((err) => {
423
+ this.log(`Auto-start session failed: ${err}`);
424
+ });
425
+ }
426
+ this.log(`StaticTVClient initialized (${this._keyType} mode)`);
427
+ }
428
+ /**
429
+ * Get the key type (channel or scene)
430
+ */
431
+ get keyType() {
432
+ return this._keyType;
433
+ }
434
+ /**
435
+ * Check if this is a lite (scene-only) client
436
+ */
437
+ get isLite() {
438
+ return this._keyType === KEY_TYPE_SCENE;
439
+ }
440
+ /**
441
+ * Make an authenticated API request
442
+ * @internal
443
+ */
444
+ async request(endpoint, options = {}) {
445
+ const url = `${this.baseUrl}${endpoint}`;
446
+ return request(url, {
447
+ ...options,
448
+ headers: {
449
+ "Content-Type": "application/json",
450
+ "x-dcl-api-key": this.config.apiKey,
451
+ ...options.headers
452
+ }
453
+ });
454
+ }
455
+ /**
456
+ * Log a message if debug is enabled
457
+ * @internal
458
+ */
459
+ log(message, ...args) {
460
+ if (this.config.debug) {
461
+ console.log(`[StaticTV] ${message}`, ...args);
462
+ }
463
+ }
464
+ /**
465
+ * Get the current configuration
466
+ * @internal
467
+ */
468
+ getConfig() {
469
+ return this.config;
470
+ }
471
+ /**
472
+ * Cleanup when done (call before scene unload)
473
+ */
474
+ async destroy() {
475
+ if (this.heartbeat) {
476
+ this.heartbeat.stopWatching();
477
+ }
478
+ await this.session.endSession();
479
+ this.log("StaticTVClient destroyed");
480
+ }
481
+ };
482
+ export {
483
+ GuideModule,
484
+ HeartbeatModule,
485
+ InteractionsModule,
486
+ KEY_TYPE_CHANNEL,
487
+ KEY_TYPE_SCENE,
488
+ SessionModule,
489
+ StaticTVClient
490
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@thestatic-tv/dcl-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Connect your Decentraland scene to thestatic.tv - full channel lineup, metrics tracking, and interactions",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
21
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
22
+ "prepublishOnly": "npm run build",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "keywords": [
26
+ "decentraland",
27
+ "dcl",
28
+ "sdk",
29
+ "streaming",
30
+ "thestatic",
31
+ "metaverse",
32
+ "video",
33
+ "live-streaming"
34
+ ],
35
+ "author": "TheStatic.tv",
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/thestatic-tv/dcl-sdk"
40
+ },
41
+ "homepage": "https://thestatic.tv",
42
+ "bugs": {
43
+ "url": "https://github.com/thestatic-tv/dcl-sdk/issues"
44
+ },
45
+ "peerDependencies": {
46
+ "@dcl/sdk": ">=7.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@dcl/sdk": "^7.0.0",
50
+ "tsup": "^8.0.0",
51
+ "typescript": "^5.0.0"
52
+ }
53
+ }