restream-sdk 0.1.9 → 0.3.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/package.json +2 -2
- package/src/core.js +104 -4
- package/src/react.js +4 -2
- package/types/index.d.ts +40 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "restream-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Plug-and-play headless React hooks to add multi-platform restreaming (connect socials, go live, live chat + viewers, collaborative split-screen) with zero client backend.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"react",
|
|
56
56
|
"sdk"
|
|
57
57
|
]
|
|
58
|
-
}
|
|
58
|
+
}
|
package/src/core.js
CHANGED
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
// into goLive() — this module never captures or publishes media itself.
|
|
6
6
|
|
|
7
7
|
export const PLATFORMS = ["twitch", "kick", "youtube", "facebook", "instagram", "tiktok", "restream"];
|
|
8
|
+
// FALLBACK only. The real routing (oauth | restream | off) is fetched per-client from
|
|
9
|
+
// GET /api/v1/connect/platforms → `routing`, so the platform team controls native↔Restream
|
|
10
|
+
// entirely from the admin with no SDK/app change. These statics are used solely if that
|
|
11
|
+
// fetch hasn't happened yet (e.g. a sync call before loadRouting()).
|
|
12
|
+
const NATIVE_CONNECT_PLATFORMS = new Set(["youtube", "facebook"]);
|
|
13
|
+
const RESTREAM_BRIDGE_PLATFORMS = new Set(["tiktok"]);
|
|
8
14
|
|
|
9
15
|
const TERMINAL_STATUS = new Set(["stopped", "failed", "ended", "error", "completed"]);
|
|
10
16
|
|
|
@@ -33,6 +39,11 @@ export class RestreamStudio extends EventTarget {
|
|
|
33
39
|
this.viewers = null;
|
|
34
40
|
this.comments = [];
|
|
35
41
|
|
|
42
|
+
// Per-client routing map { <platform>: "oauth" | "restream" | "off" }, fetched from
|
|
43
|
+
// GET /connect/platforms. Null until loadRouting()/listOAuthPlatforms() runs; the
|
|
44
|
+
// connect helpers fall back to the static defaults above while it's null.
|
|
45
|
+
this._routing = null;
|
|
46
|
+
|
|
36
47
|
this._token = null;
|
|
37
48
|
this._tokenExp = 0;
|
|
38
49
|
this._es = null;
|
|
@@ -131,9 +142,57 @@ export class RestreamStudio extends EventTarget {
|
|
|
131
142
|
return this.connections;
|
|
132
143
|
}
|
|
133
144
|
|
|
134
|
-
/** Ask the backend which OAuth platforms are configured for this client.
|
|
145
|
+
/** Ask the backend which OAuth platforms are configured for this client.
|
|
146
|
+
* Also caches the per-client `routing` map ({platform: oauth|restream|off}) that
|
|
147
|
+
* drives connectPlatform()/restreamAddChannelUrl() — so native↔Restream is decided
|
|
148
|
+
* by the admin, not hardcoded here. */
|
|
135
149
|
async listOAuthPlatforms() {
|
|
136
|
-
|
|
150
|
+
const r = await this._req("GET", `/api/v1/connect/platforms${this._userQuery()}`);
|
|
151
|
+
if (r && r.routing && typeof r.routing === "object") this._routing = r.routing;
|
|
152
|
+
return r;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Load (and cache) the per-client routing map. Call once on init so the synchronous
|
|
156
|
+
* connect helpers route correctly. Returns { platform: "oauth"|"restream"|"off" }. */
|
|
157
|
+
async loadRouting() {
|
|
158
|
+
await this.listOAuthPlatforms();
|
|
159
|
+
return this._routing || {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Effective route for a platform: "oauth" | "restream" | "off" | null (unknown). Uses
|
|
163
|
+
* the cached admin routing if loaded; otherwise the static fallbacks. */
|
|
164
|
+
routeFor(platform) {
|
|
165
|
+
const p = String(platform || "").toLowerCase();
|
|
166
|
+
if (this._routing && this._routing[p]) return this._routing[p];
|
|
167
|
+
if (NATIVE_CONNECT_PLATFORMS.has(p)) return "oauth";
|
|
168
|
+
if (RESTREAM_BRIDGE_PLATFORMS.has(p)) return "restream";
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_isNativeConnect(platform) {
|
|
173
|
+
return this.routeFor(platform) === "oauth";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_isBridge(platform) {
|
|
177
|
+
return this.routeFor(platform) === "restream";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Single dynamic connect entry point — the app calls this for ANY platform and the
|
|
182
|
+
* SDK routes it per the admin's configuration (no app change when routing flips):
|
|
183
|
+
* "oauth" → native OAuth popup (connect)
|
|
184
|
+
* "restream" → Restream bridge add-channel flow (connectRestreamChannel)
|
|
185
|
+
* "off" → throws (platform disabled for this client)
|
|
186
|
+
* Loads routing first if it hasn't been cached yet. Must be called from a click.
|
|
187
|
+
* @param {string} platform
|
|
188
|
+
* @param {object} [opts] forwarded to connectRestreamChannel for bridge platforms
|
|
189
|
+
*/
|
|
190
|
+
async connectPlatform(platform, opts = {}) {
|
|
191
|
+
if (!this._routing) { try { await this.loadRouting(); } catch { /* fall back to statics */ } }
|
|
192
|
+
const route = this.routeFor(platform);
|
|
193
|
+
if (route === "oauth") return this.connect(platform);
|
|
194
|
+
if (route === "restream") return this.connectRestreamChannel(platform, opts);
|
|
195
|
+
throw new Error(`"${platform}" is not available to connect for this client (routed "off").`);
|
|
137
196
|
}
|
|
138
197
|
|
|
139
198
|
/**
|
|
@@ -155,6 +214,15 @@ export class RestreamStudio extends EventTarget {
|
|
|
155
214
|
* unknown params), so it safely falls back to the channel page.
|
|
156
215
|
*/
|
|
157
216
|
restreamAddChannelUrl(platform) {
|
|
217
|
+
const normalized = String(platform || "").toLowerCase();
|
|
218
|
+
if (this._isNativeConnect(normalized) && this.publishableKey) {
|
|
219
|
+
const q = new URLSearchParams({ pk: this.publishableKey });
|
|
220
|
+
if (this.userId) q.set("userId", this.userId);
|
|
221
|
+
return `${this.apiBase}/api/v1/connect/${platform}/start?${q}`;
|
|
222
|
+
}
|
|
223
|
+
if (normalized && !this._isBridge(normalized)) {
|
|
224
|
+
throw new Error(`"${platform}" is not routed through the Restream bridge for this client. Use native OAuth (connect) instead.`);
|
|
225
|
+
}
|
|
158
226
|
const base = "https://restream.io/channel";
|
|
159
227
|
return platform ? `${base}?add=${encodeURIComponent(platform)}` : base;
|
|
160
228
|
}
|
|
@@ -174,6 +242,23 @@ export class RestreamStudio extends EventTarget {
|
|
|
174
242
|
* closed the popup without adding one.
|
|
175
243
|
*/
|
|
176
244
|
async connectRestreamChannel(platform, { pollMs = 3000, timeoutMs = 180_000 } = {}) {
|
|
245
|
+
const normalized = String(platform || "").toLowerCase();
|
|
246
|
+
// Load routing so native↔bridge is decided by the admin, not the statics.
|
|
247
|
+
if (!this._routing) { try { await this.loadRouting(); } catch { /* fall back to statics */ } }
|
|
248
|
+
if (this._isNativeConnect(normalized)) {
|
|
249
|
+
const result = await this.connect(platform);
|
|
250
|
+
return {
|
|
251
|
+
id: `native:${platform}`,
|
|
252
|
+
platform,
|
|
253
|
+
displayName: null,
|
|
254
|
+
url: null,
|
|
255
|
+
active: !result?.closed,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (!this._isBridge(normalized)) {
|
|
259
|
+
throw new Error(`"${platform}" is not routed through the Restream bridge for this client. Use native OAuth (connect) instead.`);
|
|
260
|
+
}
|
|
261
|
+
|
|
177
262
|
// Open synchronously to preserve the click's user-activation (else it's blocked).
|
|
178
263
|
let popup = window.open("", "restream_add_channel", "width=760,height=900");
|
|
179
264
|
if (!popup) throw new Error("popup blocked — allow popups to connect accounts");
|
|
@@ -184,6 +269,7 @@ export class RestreamStudio extends EventTarget {
|
|
|
184
269
|
await this.listConnections();
|
|
185
270
|
if (!this.connections.some((c) => c.platform === "restream")) {
|
|
186
271
|
const q = await this._authQuery();
|
|
272
|
+
q.platform = normalized;
|
|
187
273
|
popup.location = `${this.apiBase}/api/v1/connect/restream/start?${new URLSearchParams(q)}`;
|
|
188
274
|
await this._awaitRestreamConnected(timeoutMs);
|
|
189
275
|
// Our OAuth callback page auto-closes the popup; reopen for the add step.
|
|
@@ -299,7 +385,7 @@ export class RestreamStudio extends EventTarget {
|
|
|
299
385
|
* Restream channels. Any field omitted falls back to the
|
|
300
386
|
* default pre-set for this creator on the platform.
|
|
301
387
|
*/
|
|
302
|
-
async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta } = {}) {
|
|
388
|
+
async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider } = {}) {
|
|
303
389
|
if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
|
|
304
390
|
const body = {
|
|
305
391
|
publisherSessionId,
|
|
@@ -321,6 +407,12 @@ export class RestreamStudio extends EventTarget {
|
|
|
321
407
|
if (destinationMeta && typeof destinationMeta === "object" && Object.keys(destinationMeta).length) {
|
|
322
408
|
body.destinationMeta = destinationMeta;
|
|
323
409
|
}
|
|
410
|
+
// Streaming provider selection per destination.
|
|
411
|
+
// Example: { youtube: "youtube_native" } to use native YouTube instead of Restream bridge.
|
|
412
|
+
// Default is "restream" for all destinations.
|
|
413
|
+
if (streamingProvider && typeof streamingProvider === "object" && Object.keys(streamingProvider).length) {
|
|
414
|
+
body.streamingProvider = streamingProvider;
|
|
415
|
+
}
|
|
324
416
|
if (this.publishableKey && this.userId) body.clientUserId = this.userId; // token mode binds server-side
|
|
325
417
|
const job = await this._req("POST", "/api/v1/jobs", body);
|
|
326
418
|
this.job = job;
|
|
@@ -457,9 +549,17 @@ export class RestreamStudio extends EventTarget {
|
|
|
457
549
|
* Invite another creator (same client) to a split-screen collab. The composite
|
|
458
550
|
* starts only when they accept. Pass your own publisherSessionId + destinations so
|
|
459
551
|
* the server can begin the merged broadcast on accept (or rely on your active job).
|
|
552
|
+
*
|
|
553
|
+
* collaboratorRestreamOn defaults to TRUE: the invited creator's connected social
|
|
554
|
+
* accounts MUST receive the side-by-side composite unless they explicitly opt out
|
|
555
|
+
* (pass `collaboratorRestreamOn: false`). This matches the server product intent in
|
|
556
|
+
* routes/collab.js and CollabService.createInvite. The bug: defaulting the SDK to
|
|
557
|
+
* `false` here sent `false` in the request body, which the server honored, so the
|
|
558
|
+
* collaborator's broadcasts/fan-out were silently skipped and the collab frame never
|
|
559
|
+
* appeared on the collaborator's own social accounts.
|
|
460
560
|
* @param {object} opts { collaboratorUserId, collaboratorRestreamOn?, publisherSessionId?, destinations?, metadata? }
|
|
461
561
|
*/
|
|
462
|
-
async requestCollab({ collaboratorUserId, collaboratorRestreamOn =
|
|
562
|
+
async requestCollab({ collaboratorUserId, collaboratorRestreamOn = true, publisherSessionId, destinations, metadata } = {}) {
|
|
463
563
|
if (!collaboratorUserId) throw new Error("collaboratorUserId is required");
|
|
464
564
|
const invite = await this._req("POST", "/api/v1/collab/request", this._withUser({
|
|
465
565
|
collaboratorUserId, collaboratorRestreamOn, publisherSessionId, destinations, metadata,
|
package/src/react.js
CHANGED
|
@@ -49,6 +49,8 @@ export function useRestream(opts) {
|
|
|
49
49
|
listRestreamChannels: useCallback(() => studio.listRestreamChannels(), [studio]),
|
|
50
50
|
connectRestreamChannel: useCallback((p, o) => studio.connectRestreamChannel(p, o), [studio]),
|
|
51
51
|
restreamAddChannelUrl: useCallback((p) => studio.restreamAddChannelUrl(p), [studio]),
|
|
52
|
+
connectPlatform: useCallback((p, o) => studio.connectPlatform(p, o), [studio]),
|
|
53
|
+
loadRouting: useCallback(() => studio.loadRouting(), [studio]),
|
|
52
54
|
arm: useCallback((o) => studio.arm(o), [studio]),
|
|
53
55
|
goLive: useCallback((o) => studio.goLive(o), [studio]),
|
|
54
56
|
stopLive: useCallback(() => studio.stopLive(), [studio]),
|
|
@@ -70,8 +72,8 @@ export function useRestream(opts) {
|
|
|
70
72
|
|
|
71
73
|
/** Convenience: just the social-connection state + actions. */
|
|
72
74
|
export function useConnections(opts) {
|
|
73
|
-
const { connections, connect, disconnect, listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl } = useRestream(opts);
|
|
74
|
-
return { connections, connect, disconnect, refresh: listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl };
|
|
75
|
+
const { connections, connect, disconnect, listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl, connectPlatform, loadRouting } = useRestream(opts);
|
|
76
|
+
return { connections, connect, disconnect, refresh: listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl, connectPlatform, loadRouting };
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
/** Convenience: live chat (incoming comments + outbound send). Pass an existing studio. */
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export type Platform = "twitch" | "kick" | "youtube" | "facebook" | "instagram" | "tiktok" | "restream";
|
|
2
2
|
|
|
3
|
+
/** How a platform is reached for this client (admin-controlled): native OAuth, the
|
|
4
|
+
* Restream bridge, or disabled. */
|
|
5
|
+
export type RoutingValue = "oauth" | "restream" | "off";
|
|
6
|
+
/** Per-client routing map returned by GET /connect/platforms (`routing`). */
|
|
7
|
+
export type Routing = Partial<Record<Platform, RoutingValue>>;
|
|
8
|
+
/** Response of listOAuthPlatforms(). `routing` is the single source of truth for
|
|
9
|
+
* native↔Restream; `platforms` is the legacy natively-connectable set. */
|
|
10
|
+
export interface OAuthPlatformsResponse {
|
|
11
|
+
platforms: Platform[];
|
|
12
|
+
routing?: Routing;
|
|
13
|
+
frontendOrigin: string;
|
|
14
|
+
redirectOrigin: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
/** A channel the user connected inside their Restream account (a fan-out target). */
|
|
4
18
|
export interface RestreamChannel {
|
|
5
19
|
id: string;
|
|
@@ -178,20 +192,31 @@ export class RestreamStudio extends EventTarget {
|
|
|
178
192
|
on(type: "error", cb: (e: Error) => void): () => void;
|
|
179
193
|
on(type: "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-snapshot", cb: (e: CollabEvent) => void): () => void;
|
|
180
194
|
listConnections(): Promise<Connection[]>;
|
|
181
|
-
listOAuthPlatforms(): Promise<
|
|
195
|
+
listOAuthPlatforms(): Promise<OAuthPlatformsResponse>;
|
|
182
196
|
listRestreamChannels(): Promise<RestreamChannel[]>;
|
|
183
|
-
/** URL of Restream's add-channel page
|
|
197
|
+
/** URL of Restream's add-channel page; YouTube/Facebook return native OAuth URLs in publishable-key mode. */
|
|
184
198
|
restreamAddChannelUrl(platform?: string): string;
|
|
185
199
|
/**
|
|
186
200
|
* One-call "Connect <platform> via Restream" — ensures the Restream account is
|
|
187
201
|
* connected, opens Restream's add-channel page, and resolves once the new channel
|
|
188
|
-
* appears.
|
|
202
|
+
* appears. YouTube and Facebook are routed to native OAuth instead. Returns the
|
|
203
|
+
* new channel, a native placeholder, or null if the user closed the popup without
|
|
189
204
|
* adding one. Must be called from a user gesture (opens a popup).
|
|
190
205
|
*/
|
|
191
206
|
connectRestreamChannel(
|
|
192
207
|
platform: string,
|
|
193
208
|
opts?: { pollMs?: number; timeoutMs?: number }
|
|
194
209
|
): Promise<RestreamChannel | null>;
|
|
210
|
+
/** Load + cache the per-client routing map; call once on init. */
|
|
211
|
+
loadRouting(): Promise<Routing>;
|
|
212
|
+
/** Effective route for a platform (uses cached admin routing, else static fallback). */
|
|
213
|
+
routeFor(platform: string): RoutingValue | null;
|
|
214
|
+
/** Single dynamic connect entry point — routes to native OAuth or the Restream bridge
|
|
215
|
+
* per the admin's configuration. Must be called from a user gesture. */
|
|
216
|
+
connectPlatform(
|
|
217
|
+
platform: string,
|
|
218
|
+
opts?: { pollMs?: number; timeoutMs?: number }
|
|
219
|
+
): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | null>;
|
|
195
220
|
connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
|
|
196
221
|
disconnect(platform: Platform): Promise<Connection[]>;
|
|
197
222
|
arm(opts: { destinations: Platform[] }): unknown;
|
|
@@ -230,13 +255,18 @@ export interface UseRestream {
|
|
|
230
255
|
connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
|
|
231
256
|
disconnect(platform: Platform): Promise<Connection[]>;
|
|
232
257
|
listConnections(): Promise<Connection[]>;
|
|
233
|
-
listOAuthPlatforms(): Promise<
|
|
258
|
+
listOAuthPlatforms(): Promise<OAuthPlatformsResponse>;
|
|
234
259
|
listRestreamChannels(): Promise<RestreamChannel[]>;
|
|
235
260
|
restreamAddChannelUrl(platform?: string): string;
|
|
236
261
|
connectRestreamChannel(
|
|
237
262
|
platform: string,
|
|
238
263
|
opts?: { pollMs?: number; timeoutMs?: number }
|
|
239
264
|
): Promise<RestreamChannel | null>;
|
|
265
|
+
connectPlatform(
|
|
266
|
+
platform: string,
|
|
267
|
+
opts?: { pollMs?: number; timeoutMs?: number }
|
|
268
|
+
): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | null>;
|
|
269
|
+
loadRouting(): Promise<Routing>;
|
|
240
270
|
arm(opts: { destinations: Platform[] }): unknown;
|
|
241
271
|
goLive(opts: GoLiveParams): Promise<Job>;
|
|
242
272
|
stopLive(): Promise<unknown>;
|
|
@@ -260,13 +290,18 @@ export function useConnections(opts: RestreamOptions): {
|
|
|
260
290
|
connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
|
|
261
291
|
disconnect(platform: Platform): Promise<Connection[]>;
|
|
262
292
|
refresh(): Promise<Connection[]>;
|
|
263
|
-
listOAuthPlatforms(): Promise<
|
|
293
|
+
listOAuthPlatforms(): Promise<OAuthPlatformsResponse>;
|
|
264
294
|
listRestreamChannels(): Promise<RestreamChannel[]>;
|
|
265
295
|
restreamAddChannelUrl(platform?: string): string;
|
|
266
296
|
connectRestreamChannel(
|
|
267
297
|
platform: string,
|
|
268
298
|
opts?: { pollMs?: number; timeoutMs?: number }
|
|
269
299
|
): Promise<RestreamChannel | null>;
|
|
300
|
+
connectPlatform(
|
|
301
|
+
platform: string,
|
|
302
|
+
opts?: { pollMs?: number; timeoutMs?: number }
|
|
303
|
+
): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | null>;
|
|
304
|
+
loadRouting(): Promise<Routing>;
|
|
270
305
|
};
|
|
271
306
|
export function useLiveChat(studio: RestreamStudio): { comments: Comment[]; send(message: string, platforms?: Platform[]): Promise<unknown> };
|
|
272
307
|
export function useViewers(studio: RestreamStudio): ViewerCounts | null;
|