restream-sdk 0.1.3 → 0.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restream-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Plug-and-play headless React hooks to add multi-platform restreaming (connect socials, go live, live chat + viewers) with zero client backend.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/core.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // (publisherSessionId) is produced by the client's existing stream and passed
5
5
  // into goLive() — this module never captures or publishes media itself.
6
6
 
7
- export const PLATFORMS = ["twitch", "kick", "youtube", "facebook", "instagram"];
7
+ export const PLATFORMS = ["twitch", "kick", "youtube", "facebook", "instagram", "tiktok"];
8
8
 
9
9
  const TERMINAL_STATUS = new Set(["stopped", "failed", "ended", "error", "completed"]);
10
10
 
@@ -129,6 +129,125 @@ export class RestreamStudio extends EventTarget {
129
129
  return this.connections;
130
130
  }
131
131
 
132
+ /** Ask the backend which OAuth platforms are configured for this client. */
133
+ async listOAuthPlatforms() {
134
+ return this._req("GET", `/api/v1/connect/platforms${this._userQuery()}`);
135
+ }
136
+
137
+ /**
138
+ * List the channels the user connected inside their Restream account (the
139
+ * platforms Restream fans out to, e.g. TikTok). Requires a connected Restream
140
+ * account. Returns [{ id, platform, displayName, url, active }]; pass the chosen
141
+ * ids to goLive({ destinations: ["restream"], restreamChannels: [...] }).
142
+ */
143
+ async listRestreamChannels() {
144
+ const r = await this._req("GET", `/api/v1/connect/restream/channels${this._userQuery()}`);
145
+ return r?.channels || [];
146
+ }
147
+
148
+ /**
149
+ * URL of Restream's add-channel page. Restream's public API has NO endpoint to
150
+ * connect a new platform (e.g. TikTok) — that consent flow lives only on
151
+ * restream.io. So "Connect TikTok" deep-links the user here, then we auto-detect
152
+ * the new channel via the API. The `add` hint is best-effort (Restream ignores
153
+ * unknown params), so it safely falls back to the channel page.
154
+ */
155
+ restreamAddChannelUrl(platform) {
156
+ const base = "https://restream.io/channel";
157
+ return platform ? `${base}?add=${encodeURIComponent(platform)}` : base;
158
+ }
159
+
160
+ /**
161
+ * One-call "Connect <platform> via Restream" for FOMO-style UIs where the user
162
+ * only ever sees "Connect TikTok / YouTube / Facebook" (Restream stays in the
163
+ * background). Must be called from a click (it opens a popup).
164
+ *
165
+ * Flow: ensures our Restream API connection exists (runs Restream OAuth if not) →
166
+ * opens Restream's add-channel page → polls until the new channel of `platform`
167
+ * appears in the account.
168
+ *
169
+ * @param {string} platform e.g. "tiktok" | "youtube" | "facebook"
170
+ * @param {object} [opts] { pollMs=3000, timeoutMs=180000 }
171
+ * @returns {Promise<RestreamChannel|null>} the new channel, or null if the user
172
+ * closed the popup without adding one.
173
+ */
174
+ async connectRestreamChannel(platform, { pollMs = 3000, timeoutMs = 180_000 } = {}) {
175
+ // Open synchronously to preserve the click's user-activation (else it's blocked).
176
+ let popup = window.open("", "restream_add_channel", "width=760,height=900");
177
+ if (!popup) throw new Error("popup blocked — allow popups to connect accounts");
178
+
179
+ try {
180
+ // 1. We need our own API connection to the user's Restream account to read +
181
+ // detect their channels. If it's missing, run the Restream OAuth first.
182
+ await this.listConnections();
183
+ if (!this.connections.some((c) => c.platform === "restream")) {
184
+ const q = await this._authQuery();
185
+ popup.location = `${this.apiBase}/api/v1/connect/restream/start?${new URLSearchParams(q)}`;
186
+ await this._awaitRestreamConnected(timeoutMs);
187
+ // Our OAuth callback page auto-closes the popup; reopen for the add step.
188
+ if (popup.closed) popup = window.open("", "restream_add_channel", "width=760,height=900");
189
+ }
190
+
191
+ // 2. Snapshot existing channels of this platform so we detect the NEW one.
192
+ const before = new Set(
193
+ (await this.listRestreamChannels()).filter((c) => c.platform === platform).map((c) => c.id),
194
+ );
195
+
196
+ // 3. Send the user to Restream's add-channel page.
197
+ const url = this.restreamAddChannelUrl(platform);
198
+ if (popup && !popup.closed) popup.location = url;
199
+ else popup = window.open(url, "restream_add_channel", "width=760,height=900");
200
+
201
+ // 4. Poll until the new channel appears (or the popup closes / we time out).
202
+ return await this._awaitNewRestreamChannel(platform, before, popup, { pollMs, timeoutMs });
203
+ } catch (err) {
204
+ try { popup && popup.close(); } catch { /* ignore */ }
205
+ throw err;
206
+ }
207
+ }
208
+
209
+ // Resolve once a Restream API connection exists (postMessage from our callback,
210
+ // with a polling fallback). Rejects on timeout.
211
+ _awaitRestreamConnected(timeoutMs) {
212
+ const deadline = Date.now() + timeoutMs;
213
+ return new Promise((resolve, reject) => {
214
+ const cleanup = () => { window.removeEventListener("message", onMsg); clearInterval(timer); };
215
+ const finish = () => { cleanup(); this.listConnections().catch(() => {}); resolve(); };
216
+ const onMsg = (e) => {
217
+ if (this._apiOrigin && e.origin !== this._apiOrigin) return;
218
+ const d = e.data;
219
+ if (d && typeof d === "object" && d.platform === "restream" && d.type === "restream:connected") finish();
220
+ };
221
+ const timer = setInterval(async () => {
222
+ try {
223
+ await this.listConnections();
224
+ if (this.connections.some((c) => c.platform === "restream")) return finish();
225
+ } catch { /* keep polling */ }
226
+ if (Date.now() > deadline) { cleanup(); reject(new Error("timed out connecting Restream account")); }
227
+ }, 1500);
228
+ window.addEventListener("message", onMsg);
229
+ });
230
+ }
231
+
232
+ // Poll listRestreamChannels() until a channel of `platform` not in `before`
233
+ // shows up. Resolves the channel, or null on popup-close/timeout with nothing new.
234
+ _awaitNewRestreamChannel(platform, before, popup, { pollMs, timeoutMs }) {
235
+ const deadline = Date.now() + timeoutMs;
236
+ const pick = (chans) => chans.find((c) => c.platform === platform && !before.has(c.id)) || null;
237
+ return new Promise((resolve) => {
238
+ const done = (v) => { clearInterval(timer); resolve(v); };
239
+ const tick = async () => {
240
+ let found = null;
241
+ try { found = pick(await this.listRestreamChannels()); } catch { /* transient */ }
242
+ if (found) return done(found);
243
+ if (Date.now() > deadline || (popup && popup.closed)) {
244
+ try { return done(pick(await this.listRestreamChannels())); } catch { return done(null); }
245
+ }
246
+ };
247
+ const timer = setInterval(tick, pollMs);
248
+ });
249
+ }
250
+
132
251
  /** Open the hosted OAuth popup for a platform; resolves when connected (or the popup closes). */
133
252
  async connect(platform) {
134
253
  // Open the popup synchronously, inside the click's user-activation window —
@@ -175,7 +294,7 @@ export class RestreamStudio extends EventTarget {
175
294
  * @param {boolean} [opts.recording]
176
295
  * @param {string[]} [opts.trackNames] tracks actually published, e.g. ["audio","video"]
177
296
  */
178
- async goLive({ publisherSessionId, destinations, overlay, recording, trackNames } = {}) {
297
+ async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels } = {}) {
179
298
  if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
180
299
  const body = {
181
300
  publisherSessionId,
@@ -184,6 +303,8 @@ export class RestreamStudio extends EventTarget {
184
303
  recording,
185
304
  };
186
305
  if (trackNames && trackNames.length) body.trackNames = trackNames;
306
+ // Per-platform selection for the "restream" fan-out bridge (channel ids or platform names).
307
+ if (restreamChannels && restreamChannels.length) body.restreamChannels = restreamChannels;
187
308
  if (this.publishableKey && this.userId) body.clientUserId = this.userId; // token mode binds server-side
188
309
  const job = await this._req("POST", "/api/v1/jobs", body);
189
310
  this.job = job;
package/src/react.js CHANGED
@@ -40,6 +40,10 @@ export function useRestream(opts) {
40
40
  connect: useCallback((p) => studio.connect(p), [studio]),
41
41
  disconnect: useCallback((p) => studio.disconnect(p), [studio]),
42
42
  listConnections: useCallback(() => studio.listConnections(), [studio]),
43
+ listOAuthPlatforms: useCallback(() => studio.listOAuthPlatforms(), [studio]),
44
+ listRestreamChannels: useCallback(() => studio.listRestreamChannels(), [studio]),
45
+ connectRestreamChannel: useCallback((p, o) => studio.connectRestreamChannel(p, o), [studio]),
46
+ restreamAddChannelUrl: useCallback((p) => studio.restreamAddChannelUrl(p), [studio]),
43
47
  arm: useCallback((o) => studio.arm(o), [studio]),
44
48
  goLive: useCallback((o) => studio.goLive(o), [studio]),
45
49
  stopLive: useCallback(() => studio.stopLive(), [studio]),
@@ -51,8 +55,8 @@ export function useRestream(opts) {
51
55
 
52
56
  /** Convenience: just the social-connection state + actions. */
53
57
  export function useConnections(opts) {
54
- const { connections, connect, disconnect, listConnections } = useRestream(opts);
55
- return { connections, connect, disconnect, refresh: listConnections };
58
+ const { connections, connect, disconnect, listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl } = useRestream(opts);
59
+ return { connections, connect, disconnect, refresh: listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl };
56
60
  }
57
61
 
58
62
  /** Convenience: live chat (incoming comments + outbound send). Pass an existing studio. */
package/types/core.d.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  export { RestreamStudio, PLATFORMS } from "./index";
3
3
  export type {
4
4
  Platform,
5
+ RestreamChannel,
5
6
  RestreamOptions,
6
7
  Connection,
7
8
  Job,
package/types/index.d.ts CHANGED
@@ -1,4 +1,13 @@
1
- export type Platform = "twitch" | "kick" | "youtube" | "facebook" | "instagram";
1
+ export type Platform = "twitch" | "kick" | "youtube" | "facebook" | "instagram" | "tiktok" | "restream";
2
+
3
+ /** A channel the user connected inside their Restream account (a fan-out target). */
4
+ export interface RestreamChannel {
5
+ id: string;
6
+ platform: string;
7
+ displayName: string | null;
8
+ url: string | null;
9
+ active: boolean;
10
+ }
2
11
  export const PLATFORMS: Platform[];
3
12
 
4
13
  export interface RestreamOptions {
@@ -60,6 +69,12 @@ export interface GoLiveParams {
60
69
  recording?: boolean;
61
70
  /** Track names actually published (e.g. ["audio","video"] or ["video"]). */
62
71
  trackNames?: string[];
72
+ /**
73
+ * For the "restream" fan-out destination: which Restream channels to relay to
74
+ * (channel ids from listRestreamChannels(), or platform names like "tiktok").
75
+ * Omit to use whichever channels are currently active in the Restream account.
76
+ */
77
+ restreamChannels?: string[];
63
78
  }
64
79
 
65
80
  export interface OverlayUpdate {
@@ -84,6 +99,20 @@ export class RestreamStudio extends EventTarget {
84
99
  on(type: "comments", cb: (c: Comment[]) => void): () => void;
85
100
  on(type: "error", cb: (e: Error) => void): () => void;
86
101
  listConnections(): Promise<Connection[]>;
102
+ listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
103
+ listRestreamChannels(): Promise<RestreamChannel[]>;
104
+ /** URL of Restream's add-channel page (best-effort deep-link to `platform`). */
105
+ restreamAddChannelUrl(platform?: string): string;
106
+ /**
107
+ * One-call "Connect <platform> via Restream" — ensures the Restream account is
108
+ * connected, opens Restream's add-channel page, and resolves once the new channel
109
+ * appears. Returns the new channel, or null if the user closed the popup without
110
+ * adding one. Must be called from a user gesture (opens a popup).
111
+ */
112
+ connectRestreamChannel(
113
+ platform: string,
114
+ opts?: { pollMs?: number; timeoutMs?: number }
115
+ ): Promise<RestreamChannel | null>;
87
116
  connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
88
117
  disconnect(platform: Platform): Promise<Connection[]>;
89
118
  arm(opts: { destinations: Platform[] }): unknown;
@@ -111,6 +140,13 @@ export interface UseRestream {
111
140
  connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
112
141
  disconnect(platform: Platform): Promise<Connection[]>;
113
142
  listConnections(): Promise<Connection[]>;
143
+ listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
144
+ listRestreamChannels(): Promise<RestreamChannel[]>;
145
+ restreamAddChannelUrl(platform?: string): string;
146
+ connectRestreamChannel(
147
+ platform: string,
148
+ opts?: { pollMs?: number; timeoutMs?: number }
149
+ ): Promise<RestreamChannel | null>;
114
150
  arm(opts: { destinations: Platform[] }): unknown;
115
151
  goLive(opts: GoLiveParams): Promise<Job>;
116
152
  stopLive(): Promise<unknown>;
@@ -125,6 +161,13 @@ export function useConnections(opts: RestreamOptions): {
125
161
  connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
126
162
  disconnect(platform: Platform): Promise<Connection[]>;
127
163
  refresh(): Promise<Connection[]>;
164
+ listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
165
+ listRestreamChannels(): Promise<RestreamChannel[]>;
166
+ restreamAddChannelUrl(platform?: string): string;
167
+ connectRestreamChannel(
168
+ platform: string,
169
+ opts?: { pollMs?: number; timeoutMs?: number }
170
+ ): Promise<RestreamChannel | null>;
128
171
  };
129
172
  export function useLiveChat(studio: RestreamStudio): { comments: Comment[]; send(message: string, platforms?: Platform[]): Promise<unknown> };
130
173
  export function useViewers(studio: RestreamStudio): ViewerCounts | null;