restream-sdk 0.1.4 → 0.1.6

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.6",
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", "tiktok"];
7
+ export const PLATFORMS = ["twitch", "kick", "youtube", "facebook", "instagram", "tiktok", "restream"];
8
8
 
9
9
  const TERMINAL_STATUS = new Set(["stopped", "failed", "ended", "error", "completed"]);
10
10
 
@@ -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 —
@@ -190,8 +293,11 @@ export class RestreamStudio extends EventTarget {
190
293
  * @param {object} [opts.overlay] { topBanner?, bottomBanner?, htmlBanner?, enabled? }
191
294
  * @param {boolean} [opts.recording]
192
295
  * @param {string[]} [opts.trackNames] tracks actually published, e.g. ["audio","video"]
296
+ * @param {object} [opts.streamMeta] { title?, description?, tags?:string[] } applied to
297
+ * Restream channels. Any field omitted falls back to the
298
+ * default pre-set for this creator on the platform.
193
299
  */
194
- async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels } = {}) {
300
+ async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta } = {}) {
195
301
  if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
196
302
  const body = {
197
303
  publisherSessionId,
@@ -202,6 +308,11 @@ export class RestreamStudio extends EventTarget {
202
308
  if (trackNames && trackNames.length) body.trackNames = trackNames;
203
309
  // Per-platform selection for the "restream" fan-out bridge (channel ids or platform names).
204
310
  if (restreamChannels && restreamChannels.length) body.restreamChannels = restreamChannels;
311
+ // Stream title/description/tags. Send only what you have — omitted fields fall
312
+ // back to the creator's pre-set default on the platform.
313
+ if (streamMeta && (streamMeta.title || streamMeta.description || (streamMeta.tags && streamMeta.tags.length))) {
314
+ body.streamMeta = streamMeta;
315
+ }
205
316
  if (this.publishableKey && this.userId) body.clientUserId = this.userId; // token mode binds server-side
206
317
  const job = await this._req("POST", "/api/v1/jobs", body);
207
318
  this.job = job;
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
@@ -75,6 +75,18 @@ export interface GoLiveParams {
75
75
  * Omit to use whichever channels are currently active in the Restream account.
76
76
  */
77
77
  restreamChannels?: string[];
78
+ /**
79
+ * Stream title / description / tags applied to the Restream channels. Send only
80
+ * the fields you have — any field you omit falls back to the default pre-set for
81
+ * this creator on the platform. Omit entirely to use the pre-set defaults.
82
+ */
83
+ streamMeta?: StreamMeta;
84
+ }
85
+
86
+ export interface StreamMeta {
87
+ title?: string;
88
+ description?: string;
89
+ tags?: string[];
78
90
  }
79
91
 
80
92
  export interface OverlayUpdate {
@@ -101,6 +113,18 @@ export class RestreamStudio extends EventTarget {
101
113
  listConnections(): Promise<Connection[]>;
102
114
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
103
115
  listRestreamChannels(): Promise<RestreamChannel[]>;
116
+ /** URL of Restream's add-channel page (best-effort deep-link to `platform`). */
117
+ restreamAddChannelUrl(platform?: string): string;
118
+ /**
119
+ * One-call "Connect <platform> via Restream" — ensures the Restream account is
120
+ * connected, opens Restream's add-channel page, and resolves once the new channel
121
+ * appears. Returns the new channel, or null if the user closed the popup without
122
+ * adding one. Must be called from a user gesture (opens a popup).
123
+ */
124
+ connectRestreamChannel(
125
+ platform: string,
126
+ opts?: { pollMs?: number; timeoutMs?: number }
127
+ ): Promise<RestreamChannel | null>;
104
128
  connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>;
105
129
  disconnect(platform: Platform): Promise<Connection[]>;
106
130
  arm(opts: { destinations: Platform[] }): unknown;
@@ -130,6 +154,11 @@ export interface UseRestream {
130
154
  listConnections(): Promise<Connection[]>;
131
155
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
132
156
  listRestreamChannels(): Promise<RestreamChannel[]>;
157
+ restreamAddChannelUrl(platform?: string): string;
158
+ connectRestreamChannel(
159
+ platform: string,
160
+ opts?: { pollMs?: number; timeoutMs?: number }
161
+ ): Promise<RestreamChannel | null>;
133
162
  arm(opts: { destinations: Platform[] }): unknown;
134
163
  goLive(opts: GoLiveParams): Promise<Job>;
135
164
  stopLive(): Promise<unknown>;
@@ -146,6 +175,11 @@ export function useConnections(opts: RestreamOptions): {
146
175
  refresh(): Promise<Connection[]>;
147
176
  listOAuthPlatforms(): Promise<{ platforms: Platform[]; frontendOrigin: string; redirectOrigin: string }>;
148
177
  listRestreamChannels(): Promise<RestreamChannel[]>;
178
+ restreamAddChannelUrl(platform?: string): string;
179
+ connectRestreamChannel(
180
+ platform: string,
181
+ opts?: { pollMs?: number; timeoutMs?: number }
182
+ ): Promise<RestreamChannel | null>;
149
183
  };
150
184
  export function useLiveChat(studio: RestreamStudio): { comments: Comment[]; send(message: string, platforms?: Platform[]): Promise<unknown> };
151
185
  export function useViewers(studio: RestreamStudio): ViewerCounts | null;