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 +1 -1
- package/src/core.js +103 -0
- package/src/react.js +4 -2
- package/types/index.d.ts +22 -0
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
|
@@ -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;
|