restream-sdk 0.6.0 → 0.8.0

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.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "Plug-and-play headless React hooks to add multi-platform restreaming (connect socials, go live, live chat + viewers, collaborative split-screen) with zero client backend.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/core.js CHANGED
@@ -73,7 +73,7 @@ export class RestreamStudio extends EventTarget {
73
73
 
74
74
  _emit(type, detail) { this.dispatchEvent(new CustomEvent(type, { detail })); }
75
75
 
76
- /** Subscribe to an event ("connections" | "job" | "viewers" | "comment" | "comments" | "error" | "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-snapshot"). Returns an unsubscribe fn. */
76
+ /** Subscribe to an event ("connections" | "job" | "viewers" | "comment" | "comments" | "error" | "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-reassigned" | "collab-snapshot"). Returns an unsubscribe fn. */
77
77
  on(type, cb) {
78
78
  const handler = (e) => cb(e.detail);
79
79
  this.addEventListener(type, handler);
@@ -402,6 +402,15 @@ export class RestreamStudio extends EventTarget {
402
402
  */
403
403
  async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider, layout, screenSessionId, participantOnly } = {}) {
404
404
  if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
405
+ // Idempotency guard: refuse a second goLive while a job is already live. Re-publishing a fresh
406
+ // session and calling goLive() again on reconnect / screen toggle / collaborator change is the #1
407
+ // cause of duplicate + stale jobs — those are LIVE changes, so use setStreamState() /
408
+ // startScreenShare() / acceptCollab(), NOT a new goLive. Keep ONE publisher session for the whole
409
+ // stream; call stopLive() first to intentionally start over. Only live states block, so after the
410
+ // job stops (status becomes stopping/stopped/failed/ended) a genuine restart works normally.
411
+ if (this.job?.id && ["queued", "starting", "connecting", "running"].includes(this.job.status)) {
412
+ throw new Error(`goLive blocked: a stream is already live (job ${this.job.id}, status ${this.job.status}). Use setStreamState()/startScreenShare()/acceptCollab() for live changes, and call stopLive() before starting a new stream — do not re-publish or re-goLive on reconnect/screen/collab changes.`);
413
+ }
405
414
  const body = {
406
415
  publisherSessionId,
407
416
  destinations: destinations || this._armed?.destinations || [],
@@ -793,11 +802,14 @@ export class RestreamStudio extends EventTarget {
793
802
  let evt;
794
803
  try { evt = JSON.parse(e.data); } catch { return; } // heartbeat / non-JSON
795
804
  if (!evt || !evt.type) return;
796
- // When the composite starts, transition this client onto the composite job
797
- // critical for the HOST, who was still tracking their (now-superseded) solo
798
- // job. The guest already attached via the accept response; the id guard makes
799
- // this idempotent so it doesn't restart realtime twice.
800
- if (evt.type === "collab-composite" && evt.job?.id && this.job?.id !== evt.job.id) {
805
+ // Follow a job-id change. Two events carry a new job to adopt:
806
+ // collab-composite — this client joined the composite (critical for the HOST, still tracking
807
+ // its now-superseded solo; the guest already attached via the accept response).
808
+ // collab-reassigned — this creator was moved to a FRESH solo job (collab ended, host took
809
+ // over, or this guest was swapped out). Without adopting it, the UI is orphaned on a stopped
810
+ // composite id while actually re-broadcasting under a job it can't see.
811
+ // The id guard keeps it idempotent (won't restart realtime twice).
812
+ if ((evt.type === "collab-composite" || evt.type === "collab-reassigned") && evt.job?.id && this.job?.id !== evt.job.id) {
801
813
  this.job = evt.job;
802
814
  this._emit("job", evt.job);
803
815
  this.startRealtime(evt.job.id);
package/types/index.d.ts CHANGED
@@ -161,7 +161,7 @@ export interface OverlayUpdate {
161
161
  export type RestreamEvent =
162
162
  | "connections" | "job" | "viewers" | "comment" | "comments" | "error"
163
163
  | "collab" | "collab-invite" | "collab-accepted" | "collab-declined"
164
- | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-snapshot";
164
+ | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-reassigned" | "collab-snapshot";
165
165
 
166
166
  /** A collaborative split-screen invite (A invites B; the composite starts on accept). */
167
167
  export interface CollabInvite {
@@ -230,7 +230,7 @@ export class RestreamStudio extends EventTarget {
230
230
  on(type: "error", cb: (e: Error) => void): () => void;
231
231
  on(type: "state", cb: (s: StreamState) => void): () => void;
232
232
  on(type: "screenshare", cb: (e: { active: boolean; sessionId?: string; trackName?: string }) => void): () => void;
233
- on(type: "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-snapshot", cb: (e: CollabEvent) => void): () => void;
233
+ on(type: "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-reassigned" | "collab-snapshot", cb: (e: CollabEvent) => void): () => void;
234
234
  listConnections(): Promise<Connection[]>;
235
235
  listOAuthPlatforms(): Promise<OAuthPlatformsResponse>;
236
236
  listRestreamChannels(): Promise<RestreamChannel[]>;