restream-sdk 0.9.0 → 0.10.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 +1 -1
- package/src/core.js +49 -17
- package/types/index.d.ts +11 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "restream-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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
|
@@ -492,7 +492,7 @@ export class RestreamStudio extends EventTarget {
|
|
|
492
492
|
* Restream channels. Any field omitted falls back to the
|
|
493
493
|
* default pre-set for this creator on the platform.
|
|
494
494
|
*/
|
|
495
|
-
async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider, layout, screenSessionId, participantOnly } = {}) {
|
|
495
|
+
async goLive({ publisherSessionId, destinations, overlay, recording, trackNames, restreamChannels, streamMeta, destinationMeta, streamingProvider, layout, screenSessionId, participantOnly, metadata, preComposited } = {}) {
|
|
496
496
|
if (!publisherSessionId) throw new Error("publisherSessionId is required (from your existing stream session)");
|
|
497
497
|
// Idempotency guard: refuse a second goLive while a job is already live. Re-publishing a fresh
|
|
498
498
|
// session and calling goLive() again on reconnect / screen toggle / collaborator change is the #1
|
|
@@ -541,6 +541,13 @@ export class RestreamStudio extends EventTarget {
|
|
|
541
541
|
body.screenSessionId = screenId;
|
|
542
542
|
body.screenTrackName = this._screen?.trackName || "screen";
|
|
543
543
|
}
|
|
544
|
+
// Arbitrary job metadata + the pre-composited flag. preComposited:true tells the backend this
|
|
545
|
+
// publisher session already contains the partner (client-side composite / chart-canvas), so the
|
|
546
|
+
// per-creator collab reconciler must NOT attach the partner's feed to this job (it would double-
|
|
547
|
+
// composite + flip to sidebyside). Both forms land at restream_jobs.metadata.preComposited.
|
|
548
|
+
const meta = { ...(metadata || {}) };
|
|
549
|
+
if (preComposited) meta.preComposited = true;
|
|
550
|
+
if (Object.keys(meta).length) body.metadata = meta;
|
|
544
551
|
if (this.publishableKey && this.userId) body.clientUserId = this.userId; // token mode binds server-side
|
|
545
552
|
const job = await this._req("POST", "/api/v1/jobs", body);
|
|
546
553
|
this.job = job;
|
|
@@ -669,25 +676,44 @@ export class RestreamStudio extends EventTarget {
|
|
|
669
676
|
* go-live if called first, or LIVE mid-stream via the layout transition. Restream-only; not shown
|
|
670
677
|
* in the client's own player. Must be called from a user gesture (getDisplayMedia requires it).
|
|
671
678
|
*
|
|
672
|
-
*
|
|
679
|
+
* A CALLER-SUPPLIED track may be passed instead of capturing the OS screen — e.g. a
|
|
680
|
+
* `canvas.captureStream()` video track that already contains a pre-composited frame (host cam +
|
|
681
|
+
* guest cam + chart). When `track` is given we skip `getDisplayMedia` entirely (no OS picker, no
|
|
682
|
+
* permission prompt) and publish that track as the layer. The CALLER owns that track's lifecycle:
|
|
683
|
+
* we never stop it (on `stopScreenShare` or teardown), so a single continuous canvas track can
|
|
684
|
+
* survive stop/restart and coin switches with no media blip.
|
|
685
|
+
*
|
|
686
|
+
* NOTE on layout: this publishes as the SCREEN layer, so the backend composites `screen_camera`
|
|
687
|
+
* (screen full-frame + camera PiP). For a PRE-COMPOSITED pass-through feed where you do NOT want a
|
|
688
|
+
* camera PiP, publish the canvas as your PRIMARY track instead (the backend renders `solo` =
|
|
689
|
+
* full-frame, no PiP, mic audio) — see the chart-canvas integration guide.
|
|
690
|
+
*
|
|
691
|
+
* @param {object} [opts] { trackName='screen', audio=false, track?: MediaStreamTrack }
|
|
673
692
|
* @returns {Promise<{sessionId:string, trackName:string}>}
|
|
674
693
|
*/
|
|
675
|
-
async startScreenShare({ trackName = "screen", audio = false } = {}) {
|
|
676
|
-
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
|
|
677
|
-
throw new Error("Screen sharing is not supported in this environment");
|
|
678
|
-
}
|
|
694
|
+
async startScreenShare({ trackName = "screen", audio = false, track = null } = {}) {
|
|
679
695
|
if (this._screen) return { sessionId: this._screen.sessionId, trackName: this._screen.trackName };
|
|
680
696
|
|
|
681
|
-
// 1)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
697
|
+
// 1) Obtain the video track: caller-supplied (pre-composited, caller-owned) or captured screen.
|
|
698
|
+
let stream = null;
|
|
699
|
+
let videoTrack = track;
|
|
700
|
+
const callerOwnsTrack = !!track;
|
|
701
|
+
if (!callerOwnsTrack) {
|
|
702
|
+
if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
|
|
703
|
+
throw new Error("Screen sharing is not supported in this environment");
|
|
704
|
+
}
|
|
705
|
+
stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio });
|
|
706
|
+
videoTrack = stream.getVideoTracks()[0];
|
|
707
|
+
if (!videoTrack) { stream.getTracks().forEach((t) => t.stop()); throw new Error("no screen video track"); }
|
|
708
|
+
}
|
|
709
|
+
if (!videoTrack) throw new Error("startScreenShare: no video track (pass { track } or enable screen capture)");
|
|
710
|
+
const track_ = videoTrack;
|
|
685
711
|
|
|
686
712
|
try {
|
|
687
713
|
// 2) ICE servers + peer connection.
|
|
688
714
|
const { iceServers } = await this._req("GET", "/api/streams/turn-credentials");
|
|
689
715
|
const pc = new RTCPeerConnection({ iceServers, bundlePolicy: "max-bundle" });
|
|
690
|
-
const transceiver = pc.addTransceiver(
|
|
716
|
+
const transceiver = pc.addTransceiver(track_, { direction: "sendonly" });
|
|
691
717
|
|
|
692
718
|
// 3) New Cloudflare Realtime session (brokered by our backend — CF creds stay server-side).
|
|
693
719
|
const session = await this._req("POST", "/api/streams/sessions", {});
|
|
@@ -703,17 +729,19 @@ export class RestreamStudio extends EventTarget {
|
|
|
703
729
|
});
|
|
704
730
|
if (res?.sessionDescription) await pc.setRemoteDescription(new RTCSessionDescription(res.sessionDescription));
|
|
705
731
|
|
|
706
|
-
// Stop-sharing from the browser's own UI must tear us down too
|
|
707
|
-
|
|
732
|
+
// Stop-sharing from the browser's own UI must tear us down too — but ONLY for a screen we
|
|
733
|
+
// captured. A caller-supplied track's lifecycle belongs to the caller (e.g. a canvas capture
|
|
734
|
+
// that must outlive stop/restart), so we don't bind its 'ended' to our teardown.
|
|
735
|
+
if (!callerOwnsTrack) track_.addEventListener("ended", () => { this.stopScreenShare().catch(() => {}); });
|
|
708
736
|
|
|
709
|
-
this._screen = { pc, track, stream, sessionId, trackName };
|
|
737
|
+
this._screen = { pc, track: track_, stream, sessionId, trackName, callerOwnsTrack };
|
|
710
738
|
// Signal the backend WHICH session/track the screen is on so it can pull it as a live layer
|
|
711
739
|
// and transition the layout server-side (solo → screen_camera, collab → collab_screen).
|
|
712
740
|
this.setStreamState({ screenShare: true, screenSessionId: sessionId, screenTrackName: trackName });
|
|
713
741
|
this._emit("screenshare", { active: true, sessionId, trackName });
|
|
714
742
|
return { sessionId, trackName };
|
|
715
743
|
} catch (err) {
|
|
716
|
-
try { stream.getTracks().forEach((t) => t.stop()); } catch { /* ignore */ }
|
|
744
|
+
if (stream) { try { stream.getTracks().forEach((t) => t.stop()); } catch { /* ignore */ } }
|
|
717
745
|
throw err;
|
|
718
746
|
}
|
|
719
747
|
}
|
|
@@ -724,8 +752,12 @@ export class RestreamStudio extends EventTarget {
|
|
|
724
752
|
if (!s) return;
|
|
725
753
|
this._screen = null;
|
|
726
754
|
try { await this._req("POST", `/api/streams/sessions/${s.sessionId}/tracks/close`, { tracks: [{ mid: s.pc?.getTransceivers?.()[0]?.mid, trackName: s.trackName }], force: true }); } catch { /* best-effort */ }
|
|
727
|
-
|
|
728
|
-
|
|
755
|
+
// Only stop tracks WE created (captured screen). A caller-supplied track (e.g. a continuous
|
|
756
|
+
// canvas capture) is owned by the caller and must keep running across stop/restart + coin switches.
|
|
757
|
+
if (!s.callerOwnsTrack) {
|
|
758
|
+
try { s.track.stop(); } catch { /* ignore */ }
|
|
759
|
+
try { s.stream?.getTracks().forEach((t) => t.stop()); } catch { /* ignore */ }
|
|
760
|
+
}
|
|
729
761
|
try { s.pc.close(); } catch { /* ignore */ }
|
|
730
762
|
// Clear the screen session so the backend reverts the layout (screen_camera → solo,
|
|
731
763
|
// collab_screen → sidebyside).
|
package/types/index.d.ts
CHANGED
|
@@ -104,6 +104,15 @@ export interface GoLiveParams {
|
|
|
104
104
|
destinationMeta?: DestinationMeta;
|
|
105
105
|
/** Initial presence/layout state for the stream (same shape as setStreamState). */
|
|
106
106
|
layout?: StreamState;
|
|
107
|
+
/** Arbitrary job metadata (stored on the job row, readable by the backend). */
|
|
108
|
+
metadata?: Record<string, unknown>;
|
|
109
|
+
/**
|
|
110
|
+
* Mark this publisher session as PRE-COMPOSITED: it already contains the partner (host + guest
|
|
111
|
+
* composited client-side onto one canvas). The backend renders it full-frame (solo) and the
|
|
112
|
+
* per-creator collab reconciler will NOT attach the partner's feed to this job (which would
|
|
113
|
+
* double-composite + flip to sidebyside). Folded into metadata.preComposited.
|
|
114
|
+
*/
|
|
115
|
+
preComposited?: boolean;
|
|
107
116
|
/** Cloudflare session of a screen-share publish (from startScreenShare) to composite as a
|
|
108
117
|
* separate layer. Defaults to the SDK's active screen share if omitted. */
|
|
109
118
|
screenSessionId?: string;
|
|
@@ -273,7 +282,7 @@ export class RestreamStudio extends EventTarget {
|
|
|
273
282
|
/** Capture the screen and publish it as its OWN Cloudflare session (a separate layer the
|
|
274
283
|
* backend composites via templates). Pass the returned sessionId to goLive({ screenSessionId }).
|
|
275
284
|
* Restream-only; must be called from a user gesture. */
|
|
276
|
-
startScreenShare(opts?: { trackName?: string; audio?: boolean }): Promise<{ sessionId: string; trackName: string }>;
|
|
285
|
+
startScreenShare(opts?: { trackName?: string; audio?: boolean; track?: MediaStreamTrack }): Promise<{ sessionId: string; trackName: string }>;
|
|
277
286
|
/** Stop screen sharing (close the Cloudflare track + peer connection). */
|
|
278
287
|
stopScreenShare(): Promise<void>;
|
|
279
288
|
/** Cloudflare session id of the live screen-share publish, or null. */
|
|
@@ -335,7 +344,7 @@ export interface UseRestream {
|
|
|
335
344
|
sendChat(message: string, platforms?: Platform[]): Promise<unknown>;
|
|
336
345
|
updateOverlay(opts: OverlayUpdate): Promise<unknown>;
|
|
337
346
|
setStreamState(patch: StreamState): StreamState;
|
|
338
|
-
startScreenShare(opts?: { trackName?: string; audio?: boolean }): Promise<{ sessionId: string; trackName: string }>;
|
|
347
|
+
startScreenShare(opts?: { trackName?: string; audio?: boolean; track?: MediaStreamTrack }): Promise<{ sessionId: string; trackName: string }>;
|
|
339
348
|
stopScreenShare(): Promise<void>;
|
|
340
349
|
requestCollab(opts: RequestCollabParams): Promise<CollabInvite>;
|
|
341
350
|
acceptCollab(inviteId: string, opts: AcceptCollabParams): Promise<{ invite: CollabInvite; job: Job }>;
|