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