restream-sdk 0.1.4 → 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.4",
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
@@ -145,6 +145,109 @@ export class RestreamStudio extends EventTarget {
145
145
  return r?.channels || [];
146
146
  }
147
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
+
148
251
  /** Open the hosted OAuth popup for a platform; resolves when connected (or the popup closes). */
149
252
  async connect(platform) {
150
253
  // Open the popup synchronously, inside the click's user-activation window —
package/src/react.js CHANGED
@@ -42,6 +42,8 @@ export function useRestream(opts) {
42
42
  listConnections: useCallback(() => studio.listConnections(), [studio]),
43
43
  listOAuthPlatforms: useCallback(() => studio.listOAuthPlatforms(), [studio]),
44
44
  listRestreamChannels: useCallback(() => studio.listRestreamChannels(), [studio]),
45
+ connectRestreamChannel: useCallback((p, o) => studio.connectRestreamChannel(p, o), [studio]),
46
+ restreamAddChannelUrl: useCallback((p) => studio.restreamAddChannelUrl(p), [studio]),
45
47
  arm: useCallback((o) => studio.arm(o), [studio]),
46
48
  goLive: useCallback((o) => studio.goLive(o), [studio]),
47
49
  stopLive: useCallback(() => studio.stopLive(), [studio]),
@@ -53,8 +55,8 @@ export function useRestream(opts) {
53
55
 
54
56
  /** Convenience: just the social-connection state + actions. */
55
57
  export function useConnections(opts) {
56
- const { connections, connect, disconnect, listConnections, listOAuthPlatforms, listRestreamChannels } = useRestream(opts);
57
- return { connections, connect, disconnect, refresh: listConnections, listOAuthPlatforms, listRestreamChannels };
58
+ const { connections, connect, disconnect, listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl } = useRestream(opts);
59
+ return { connections, connect, disconnect, refresh: listConnections, listOAuthPlatforms, listRestreamChannels, connectRestreamChannel, restreamAddChannelUrl };
58
60
  }
59
61
 
60
62
  /** Convenience: live chat (incoming comments + outbound send). Pass an existing studio. */
package/types/index.d.ts CHANGED
@@ -101,6 +101,18 @@ export class RestreamStudio extends EventTarget {
101
101
  listConnections(): Promise<Connection[]>;
102
102
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
103
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>;
104
116
  connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
105
117
  disconnect(platform: Platform): Promise<Connection[]>;
106
118
  arm(opts: { destinations: Platform[] }): unknown;
@@ -130,6 +142,11 @@ export interface UseRestream {
130
142
  listConnections(): Promise<Connection[]>;
131
143
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
132
144
  listRestreamChannels(): Promise<RestreamChannel[]>;
145
+ restreamAddChannelUrl(platform?: string): string;
146
+ connectRestreamChannel(
147
+ platform: string,
148
+ opts?: { pollMs?: number; timeoutMs?: number }
149
+ ): Promise<RestreamChannel | null>;
133
150
  arm(opts: { destinations: Platform[] }): unknown;
134
151
  goLive(opts: GoLiveParams): Promise<Job>;
135
152
  stopLive(): Promise<unknown>;
@@ -146,6 +163,11 @@ export function useConnections(opts: RestreamOptions): {
146
163
  refresh(): Promise<Connection[]>;
147
164
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
148
165
  listRestreamChannels(): Promise<RestreamChannel[]>;
166
+ restreamAddChannelUrl(platform?: string): string;
167
+ connectRestreamChannel(
168
+ platform: string,
169
+ opts?: { pollMs?: number; timeoutMs?: number }
170
+ ): Promise<RestreamChannel | null>;
149
171
  };
150
172
  export function useLiveChat(studio: RestreamStudio): { comments: Comment[]; send(message: string, platforms?: Platform[]): Promise<unknown> };
151
173
  export function useViewers(studio: RestreamStudio): ViewerCounts | null;