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 +1 -1
- package/src/core.js +113 -2
- package/src/react.js +4 -2
- package/types/index.d.ts +34 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "restream-sdk",
|
|
3
|
-
"version": "0.1.
|
|
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;
|