restream-sdk 0.3.0 → 0.4.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.3.0",
3
+ "version": "0.4.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
@@ -44,6 +44,14 @@ export class RestreamStudio extends EventTarget {
44
44
  // connect helpers fall back to the static defaults above while it's null.
45
45
  this._routing = null;
46
46
 
47
+ // Real-time presence/layout state ({ screenShare, camera, collab, cameraPosition,
48
+ // cameraSize }) pushed to the server via setStreamState(). Coalesced (trailing debounce)
49
+ // so rapid toggles send once. _pendingState holds state set before goLive() (flushed once
50
+ // the job exists).
51
+ this._streamState = {};
52
+ this._pendingState = null;
53
+ this._stateTimer = null;
54
+
47
55
  this._token = null;
48
56
  this._tokenExp = 0;
49
57
  this._es = null;
@@ -385,7 +393,7 @@ export class RestreamStudio extends EventTarget {
385
393
  * Restream channels. Any field omitted falls back to the
386
394
  * default pre-set for this creator on the platform.
387
395
  */
388
- async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider } = {}) {
396
+ async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider, layout } = {}) {
389
397
  if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
390
398
  const body = {
391
399
  publisherSessionId,
@@ -418,6 +426,13 @@ export class RestreamStudio extends EventTarget {
418
426
  this.job = job;
419
427
  this._emit("job", job);
420
428
  if (job?.id) this.startRealtime(job.id);
429
+ // Push the initial presence/layout state (explicit `layout`, or whatever was set via
430
+ // setStreamState() before we had a job id). Best-effort — never block go-live on it.
431
+ const initialState = { ...(this._pendingState || {}), ...(layout || {}) };
432
+ if (job?.id && Object.keys(initialState).length) {
433
+ this._pendingState = null;
434
+ this.setStreamState(initialState).catch(() => {});
435
+ }
421
436
  return job;
422
437
  }
423
438
 
@@ -425,6 +440,7 @@ export class RestreamStudio extends EventTarget {
425
440
  if (!this.job?.id) return null;
426
441
  const r = await this._req("DELETE", `/api/v1/jobs/${this.job.id}`);
427
442
  this.stopRealtime();
443
+ if (this._stateTimer) { clearTimeout(this._stateTimer); this._stateTimer = null; } // drop any pending state POST
428
444
  this.job = { ...this.job, status: "stopping" };
429
445
  this._emit("job", this.job);
430
446
  return r;
@@ -447,6 +463,40 @@ export class RestreamStudio extends EventTarget {
447
463
  return this._req("PATCH", `/api/v1/jobs/${this.job.id}/overlay`, opts);
448
464
  }
449
465
 
466
+ /**
467
+ * Push the creator's real-time presence/layout state to the server. Call on EVERY change
468
+ * (screen-share toggled, camera on/off, collab on/off, camera moved/resized) so the server
469
+ * can swap banner templates per state and keep assets clear of the camera. Partial patches
470
+ * merge; the full merged state is sent (trailing-debounced ~150ms so rapid toggles send once).
471
+ *
472
+ * @param {object} patch
473
+ * @param {boolean} [patch.screenShare]
474
+ * @param {boolean} [patch.camera]
475
+ * @param {boolean} [patch.collab]
476
+ * @param {"top-left"|"top-right"|"bottom-left"|"bottom-right"|null} [patch.cameraPosition]
477
+ * @param {"small"|"medium"|"large"|number|null} [patch.cameraSize] enum or % (1-100)
478
+ * @returns {object} the merged local state (the network send is debounced/best-effort)
479
+ */
480
+ setStreamState(patch = {}) {
481
+ this._streamState = { ...this._streamState, ...patch };
482
+ this._emit("state", this._streamState);
483
+ if (!this.job?.id) {
484
+ // Not live yet — remember it and flush when goLive() creates the job.
485
+ this._pendingState = { ...this._streamState };
486
+ return this._streamState;
487
+ }
488
+ const jobId = this.job.id;
489
+ if (this._stateTimer) clearTimeout(this._stateTimer);
490
+ this._stateTimer = setTimeout(() => {
491
+ this._stateTimer = null;
492
+ this._req("POST", `/api/v1/jobs/${jobId}/state`, this._streamState).catch((e) => this._emit("error", e));
493
+ }, 150);
494
+ return this._streamState;
495
+ }
496
+
497
+ /** Last presence/layout state set via setStreamState() (local view). */
498
+ get streamState() { return this._streamState; }
499
+
450
500
  // ---- realtime (chat SSE + job-status/viewer polling) ----
451
501
  async startRealtime(jobId) {
452
502
  this.stopRealtime();
package/src/react.js CHANGED
@@ -57,6 +57,7 @@ export function useRestream(opts) {
57
57
  refreshJob: useCallback(() => studio.refreshJob(), [studio]),
58
58
  sendChat: useCallback((m, p) => studio.sendChat(m, p), [studio]),
59
59
  updateOverlay: useCallback((o) => studio.updateOverlay(o), [studio]),
60
+ setStreamState: useCallback((s) => studio.setStreamState(s), [studio]),
60
61
  // collaborative split-screen
61
62
  requestCollab: useCallback((o) => studio.requestCollab(o), [studio]),
62
63
  acceptCollab: useCallback((id, o) => studio.acceptCollab(id, o), [studio]),
package/types/index.d.ts CHANGED
@@ -102,6 +102,8 @@ export interface GoLiveParams {
102
102
  * default → the creator's pre-set global default → the platform's existing value.
103
103
  */
104
104
  destinationMeta?: DestinationMeta;
105
+ /** Initial presence/layout state for the stream (same shape as setStreamState). */
106
+ layout?: StreamState;
105
107
  }
106
108
 
107
109
  export interface StreamMeta {
@@ -113,6 +115,26 @@ export interface StreamMeta {
113
115
  /** Per-platform stream metadata overrides, keyed by platform name. */
114
116
  export type DestinationMeta = Partial<Record<Platform, StreamMeta>> & Record<string, StreamMeta>;
115
117
 
118
+ /** Where the creator's frontend placed the camera while screen-sharing (template hint). */
119
+ export type CameraPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
120
+ /** Camera size as an enum or a percentage of the frame (1-100). */
121
+ export type CameraSize = "small" | "medium" | "large" | number;
122
+
123
+ /**
124
+ * Real-time presence/layout state pushed via setStreamState(). All fields optional —
125
+ * partial patches merge server-side. Camera position/size only matter while screen-sharing
126
+ * and are template metadata (they don't move media).
127
+ */
128
+ export interface StreamState {
129
+ screenShare?: boolean;
130
+ camera?: boolean;
131
+ collab?: boolean;
132
+ cameraPosition?: CameraPosition | null;
133
+ cameraSize?: CameraSize | null;
134
+ /** Server-stamped on each update (read-only on the way back). */
135
+ updatedAt?: string;
136
+ }
137
+
116
138
  export interface OverlayUpdate {
117
139
  topBanner?: string;
118
140
  bottomBanner?: string;
@@ -190,6 +212,7 @@ export class RestreamStudio extends EventTarget {
190
212
  on(type: "comment", cb: (c: Comment) => void): () => void;
191
213
  on(type: "comments", cb: (c: Comment[]) => void): () => void;
192
214
  on(type: "error", cb: (e: Error) => void): () => void;
215
+ on(type: "state", cb: (s: StreamState) => void): () => void;
193
216
  on(type: "collab" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-snapshot", cb: (e: CollabEvent) => void): () => void;
194
217
  listConnections(): Promise<Connection[]>;
195
218
  listOAuthPlatforms(): Promise<OAuthPlatformsResponse>;
@@ -225,6 +248,11 @@ export class RestreamStudio extends EventTarget {
225
248
  refreshJob(): Promise<Job | null>;
226
249
  sendChat(message: string, platforms?: Platform[]): Promise<unknown>;
227
250
  updateOverlay(opts: OverlayUpdate): Promise<unknown>;
251
+ /** Push real-time presence/layout state (screen-share/camera/collab + camera corner/size).
252
+ * Call on every change; sends are coalesced (trailing-debounced). Returns the merged state. */
253
+ setStreamState(patch: StreamState): StreamState;
254
+ /** Last presence/layout state set locally. */
255
+ readonly streamState: StreamState;
228
256
  startRealtime(jobId: string): Promise<void>;
229
257
  stopRealtime(): void;
230
258
  // collaborative split-screen
@@ -273,6 +301,7 @@ export interface UseRestream {
273
301
  refreshJob(): Promise<Job | null>;
274
302
  sendChat(message: string, platforms?: Platform[]): Promise<unknown>;
275
303
  updateOverlay(opts: OverlayUpdate): Promise<unknown>;
304
+ setStreamState(patch: StreamState): StreamState;
276
305
  requestCollab(opts: RequestCollabParams): Promise<CollabInvite>;
277
306
  acceptCollab(inviteId: string, opts: AcceptCollabParams): Promise<{ invite: CollabInvite; job: Job }>;
278
307
  declineCollab(inviteId: string): Promise<CollabInvite>;