export type Platform = "twitch" | "kick" | "youtube" | "facebook" | "instagram" | "tiktok" | "restream"; /** How a platform is reached for this client (admin-controlled): native OAuth, the * Restream bridge, or disabled. */ export type RoutingValue = "oauth" | "restream" | "off"; /** Per-client routing map returned by GET /connect/platforms (`routing`). */ export type Routing = Partial>; /** * How a platform is CONNECTED, which RoutingValue cannot express. * * `routing:"oauth"` hides two different creator journeys: a browser consent popup, and pasting a * stream key copied from the platform's own producer page. X is the second — it has no * broadcast-creation API, so no consent flow can exist. Branch on this, not on `routeFor()`. */ export type ConnectMethod = "oauth" | "manual" | "restream" | "off"; export type ConnectMethods = Partial>; export type RequiresStreamKey = Partial>; /** Returned by connectPlatform() when the platform needs a creator-supplied stream key. */ /** * Error thrown by any SDK call that reaches the API and gets a non-2xx. * * `body` carries the server's response verbatim, including fields added after this typing was * written. `code` and `manageUrl` are lifted out of it as conveniences — every typed refusal is * identified by `code`, and the bridge refusals carry a `manageUrl` meant to be rendered as a link. */ export interface RestreamApiError extends Error { /** HTTP status, or 0 for a network/timeout failure. */ status: number; /** Machine-readable refusal code, e.g. "BRIDGE_CHANNEL_MANAGED_IN_RESTREAM". Null if absent. */ code: string | null; /** Present when the server supplied one — a page the creator should be sent to. */ manageUrl?: string; /** The parsed response body, verbatim. Authoritative. */ body: unknown; } export interface ManualConnectRequired { connected: false; requiresStreamKey: true; method: "manual"; platform: string; message: string; } /** Response of listOAuthPlatforms(). `routing` is the single source of truth for * native↔Restream; `platforms` is the legacy natively-connectable set. */ export interface OAuthPlatformsResponse { platforms: Platform[]; routing?: Routing; frontendOrigin: string; redirectOrigin: string; } /** A channel the user connected inside their Restream account (a fan-out target). */ export interface RestreamChannel { id: string; platform: string; displayName: string | null; url: string | null; active: boolean; } /** This SDK's version, sent as X-Restream-SDK-Version on every request. */ export const SDK_VERSION: string; export const PLATFORMS: Platform[]; export const SIMPLE_RESTREAM_LAYOUT_PRESETS: Array<"chart_camera" | "chart_only" | "camera_only">; export const CONTEXTUAL_RESTREAM_LAYOUT_PRESETS: Array<"screen_camera" | "screen_only" | "collab_50_50" | "collab_screen">; export interface RestreamOptions { /** Base URL of the Restream API. */ apiBase: string; /** Publishable key (pk_live_...) for zero-backend mode. */ publishableKey?: string; /** URL on your backend returning `{ token }` (hardened mode). */ tokenEndpoint?: string; /** External streamer id (required in publishableKey mode). */ userId?: string; /** Per-request timeout in ms (default 15000). */ timeoutMs?: number; } export interface Connection { platform: Platform; platformUsername?: string; platformChannelId?: string; platformUserId?: string; platformPageId?: string; connectedAt?: string; expiresAt?: string | null; /** true when the saved credential is usable now. False means the creator should reconnect. */ isActive?: boolean; /** true when OAuth must be re-granted before this destination should be armed. */ needsReauth?: boolean; /** "active" | "expiring" | "needs_reauth" for OAuth, "disabled" for inactive bridge channels. */ tokenHealth?: "active" | "expiring" | "needs_reauth" | "disabled" | string; /** Back-compat alias for tokenHealth. */ tokenStatus?: "active" | "expiring" | "needs_reauth" | "disabled" | string; hasRefreshToken?: boolean; /** Facebook: every Page the connected account manages. The active one = platform_page_id. */ pages?: Array<{ id: string; name: string }>; /** Facebook: the currently active Page id (raw snake_case, as returned by GET /connect/list). */ platform_page_id?: string; platform_user_id?: string; platform_username?: string; platform_channel_id?: string; expires_at?: string | null; [k: string]: unknown; } export interface Job { id: string; status: string; destinations?: Platform[]; broadcasts?: Array<{ platform: Platform; watchUrl?: string }>; /** * Set by stopLive(). Everything the SDK owns — its screen-share session, both SSE streams, * telemetry, pending state updates — is torn down. Deliberately NOT named localMediaStopped: * this SDK does not own the camera publisher, so it says nothing about whether the browser * has stopped broadcasting. The host app must stop its own publisher independently. */ sdkMediaStopped?: boolean; /** The backend acknowledged the stop. Only then is the stop actually complete. */ serverStopConfirmed?: boolean; /** Why the stop was not confirmed. stopLive() also rejects, so this is never silent. */ serverStopError?: string | null; [k: string]: unknown; } export interface Recording { id: string; streamJobId: string; clientUserId: string; status: "recording" | "ready" | "failed" | string; playbackReady?: boolean; playable?: boolean; /** True when the replay is still being produced (not yet playable). */ processing?: boolean; processingMessage?: string | null; storageProvider?: string; r2Key?: string | null; playbackUrl?: string | null; thumbnailUrl?: string | null; recordingThumbnailUrl?: string | null; recordingThumbnailR2Key?: string | null; recordingThumbnailSelectedAt?: string | null; thumbnailKind?: string | null; thumbnailManual?: boolean; thumbnailAuto?: boolean; thumbnailScore?: number | null; durationSeconds?: number | null; sizeBytes?: number | null; error?: string | null; startedAt?: string; completedAt?: string | null; createdAt?: string; updatedAt?: string; } export interface ViewerCounts { total?: number; platforms?: Partial>; [k: string]: unknown; } export interface Comment { id: string; platform: Platform; author: string; message: string; timestamp?: number; authorAvatar?: string | null; } export interface Overlay { topBanner?: string; bottomBanner?: string; htmlBanner?: string; enabled?: boolean; } export type RestreamLayoutPreset = | "chart_camera" | "chart_only" | "camera_only" | "screen_camera" | "screen_only" | "collab_50_50" | "collab_screen"; export type SharedSourceType = "none" | "fomo-tab" | "external-tab" | "window" | "screen" | "camera" | "unknown"; export type RestreamBrandingMode = "auto" | "on" | "off" | "minimal"; export interface RestreamPresentation { layoutPreset?: RestreamLayoutPreset; cameraVisible?: boolean; showMiniChart?: boolean; chartStrip?: boolean; previewRequired?: boolean; applyRequiresPreview?: boolean; sharedSourceType?: SharedSourceType; brandingAlreadyVisible?: boolean; branding?: { mode?: RestreamBrandingMode; brandingAlreadyVisible?: boolean; sharedSourceType?: SharedSourceType; showLogo?: boolean; showCreatorName?: boolean; showChartName?: boolean; showCoinUrl?: boolean; showLiveOnFomo?: boolean; }; content?: { creatorUsername?: string; creatorDisplayName?: string; chartName?: string; coinName?: string; coinSymbol?: string; coinUrl?: string; streamTitle?: string; chartPoints?: number[]; areaChartPoints?: number[]; }; } export interface GoLiveParams { /** The session id from your existing stream (REQUIRED). */ publisherSessionId: string; destinations?: Platform[]; overlay?: Overlay; recording?: boolean; /** Track names actually published (e.g. ["audio","video"] or ["video"]). */ trackNames?: string[]; /** Optional publisher PeerConnection. When supplied, goLive waits for fresh outbound RTP first. */ publisherPeerConnection?: RTCPeerConnection; /** Back-compat alias for publisherPeerConnection. */ pc?: RTCPeerConnection; /** Set false only if the app has already called waitUntilPublishing itself. Defaults true. */ waitForPublishing?: boolean; /** * Budget for goLive's own publish-wait, in ms. Defaults to 10000. * Prefer this over the `waitUntilPublishing()` + `waitForPublishing:false` two-step — that * pattern exists only because this budget used to be hardcoded. */ publishWaitTimeoutMs?: number; /** * Set true when the app ran its OWN publish-wait, that wait FAILED, and the app has decided to * go live regardless (e.g. rather than lose a merely-slow camera). * * This is not an error and does not block go-live — it is a declaration. Without it, a publisher * that never sends packets produces a stream the server can only diagnose as a Cloudflare "track * not found" fault, which reads like a backend problem and sends everyone to the wrong layer. * With it, the server names the real cause and stops retrying a publisher already known to be * silent. */ publishUnconfirmed?: boolean; /** * For the "restream" fan-out destination: which Restream channels to relay to * (channel ids from listRestreamChannels(), or platform names like "tiktok"). * Omit to use whichever channels are currently active in the Restream account. */ restreamChannels?: string[]; /** * Stream title / description / tags applied to the Restream channels. Send only * the fields you have — any field you omit falls back to the default pre-set for * this creator on the platform. Omit entirely to use the pre-set defaults. */ streamMeta?: StreamMeta; /** * Per-destination overrides, keyed by platform name (e.g. * `{ youtube: { title }, twitch: { title, tags } }`). Each field falls back, in * order, to: this map → `streamMeta` → the creator's pre-set per-destination * default → the creator's pre-set global default → the platform's existing value. */ destinationMeta?: DestinationMeta; /** Initial presence/layout state for the stream (same shape as setStreamState). */ layout?: StreamState; /** Creator-facing restream output presentation/branding contract. */ restreamPresentation?: RestreamPresentation; /** Arbitrary job metadata (stored on the job row, readable by the backend). */ metadata?: Record; /** * Mark this publisher session as PRE-COMPOSITED: it already contains the partner (host + guest * composited client-side onto one canvas). The backend renders it full-frame (solo) and the * per-creator collab reconciler will NOT attach the partner's feed to this job (which would * double-composite + flip to sidebyside). Folded into metadata.preComposited. */ preComposited?: boolean; /** Cloudflare session of a screen-share publish (from startScreenShare) to composite as a * separate layer. Defaults to the SDK's active screen share if omitted. */ screenSessionId?: string; /** Track name for the screen video layer. Defaults to the SDK's active screen track. */ screenTrackName?: string; /** True when the screen-share session also published tab/system audio. */ screenAudio?: boolean; /** Track names for the screen-share session when it contains video plus optional audio. */ screenTrackNames?: string[]; /** True when the shared surface already contains the camera PiP, so backend camera PiP is hidden. */ screenHasCameraPip?: boolean; sharedSourceType?: SharedSourceType; brandingAlreadyVisible?: boolean; /** * Compositing-only participant: go live with NO destinations. The job reaches `running` (so it's * discoverable as an active solo job for a per-creator collab to composite its camera) but fans * out nowhere. For a collab participant with no socials of their own (e.g. a challenger). */ participantOnly?: boolean; /** * Recording-only: pull + record the session with NO RTMP fan-out. The backend always arms * recording for these jobs, so a creator who arms no social platform still gets a replay. * Fixes the "streamed without destinations → nothing recorded anywhere" case. */ recordingOnly?: boolean; } export interface StreamMeta { title?: string; description?: string; tags?: string[]; } /** Per-platform stream metadata overrides, keyed by platform name. */ export type DestinationMeta = Partial> & Record; export interface LiveMetaUpdate { streamMeta?: StreamMeta; destinationMeta?: DestinationMeta; metadata?: Record; } export interface LiveMetaUpdateResult { ok: boolean; jobId: string; metadata?: Record; results?: Array<{ platform: string; ok: boolean; updated?: boolean; skipped?: boolean; reason?: string | null; error?: string; code?: string; }>; streamStillLive?: boolean; } /** Where the creator's frontend placed the camera while screen-sharing (template hint). */ export type CameraPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right"; /** Camera size as an enum or a percentage of the frame (1-100). */ export type CameraSize = "small" | "medium" | "large" | number; /** * Real-time presence/layout state pushed via setStreamState(). All fields optional — * partial patches merge server-side. Camera position/size only matter while screen-sharing * and are template metadata (they don't move media). */ export interface StreamState { screenShare?: boolean; camera?: boolean; collab?: boolean; cameraPosition?: CameraPosition | null; cameraSize?: CameraSize | null; /** * Cloudflare session/track the screen is published on. Set automatically by startScreenShare() * so the backend can pull the screen as a live layer and transition the layout server-side * (solo → screen_camera, collab → collab_screen). Cleared by stopScreenShare(). */ screenSessionId?: string | null; screenTrackName?: string | null; /** * Whether the SDK captured SCREEN AUDIO (tab/system) alongside the screen video. Set to true * automatically by startScreenShare({ audio: true }) when a screen-audio track was actually * returned by getDisplayMedia; false / null otherwise. When true, screenTrackNames is set to * ['audio','video'] so the backend pulls both tracks and mixes screen audio into the broadcast. */ screenAudio?: boolean | null; screenTrackNames?: string[] | null; /** True when the shared screen/tab already contains its own camera PiP, so backend should not add another PiP. */ screenHasCameraPip?: boolean | null; sharedSourceType?: SharedSourceType | null; brandingAlreadyVisible?: boolean | null; restreamPresentation?: RestreamPresentation | null; /** Server-stamped on each update (read-only on the way back). */ updatedAt?: string; } export interface ScreenShareResult { sessionId: string; trackName: string; audio: boolean; /** True when the shared source already contains a camera PiP, so backend camera PiP is hidden. */ screenHasCameraPip?: boolean; /** What kind of surface is being shared: FOMO tab, external tab, window, full screen, etc. */ sharedSourceType?: SharedSourceType; /** True when FOMO identity/branding is already visible inside the shared surface. */ brandingAlreadyVisible?: boolean; /** true when an existing screen-share session stayed alive and only its video track changed. */ replaced?: boolean; } export interface ScreenShareOptions { trackName?: string; audio?: boolean; track?: MediaStreamTrack; /** True when the shared source already contains a camera PiP. If omitted, the SDK infers it when possible. */ screenHasCameraPip?: boolean; /** Explicit source classification. If omitted, the SDK infers from Capture Handle/displaySurface. */ sharedSourceType?: SharedSourceType; /** True when FOMO logo/name/chart identity is already visible in the shared surface. */ brandingAlreadyVisible?: boolean; /** Escape hatch for internal tests. Defaults false so obvious social-preview captures are blocked. */ allowUnsafeRecursiveCapture?: boolean; } export interface SwitchScreenShareOptions { audio?: boolean; track?: MediaStreamTrack; /** True when the shared source already contains a camera PiP. If omitted, the SDK infers it when possible. */ screenHasCameraPip?: boolean; /** Explicit source classification. If omitted, the SDK infers from Capture Handle/displaySurface. */ sharedSourceType?: SharedSourceType; /** True when FOMO logo/name/chart identity is already visible in the shared surface. */ brandingAlreadyVisible?: boolean; /** Escape hatch for internal tests. Defaults false so obvious social-preview captures are blocked. */ allowUnsafeRecursiveCapture?: boolean; } export interface CameraOnlyPipOptions { muted?: boolean; autoplay?: boolean; playsInline?: boolean; } export type LiveLayoutPreset = | "chart_camera" | "chart_only" | "camera_only" | "screen_camera" | "screen_only" | "collab_50_50" | "collab_screen" | "chart" | "chart_with_camera" | "camera" | "solo" | "screen" | "screen_with_camera" | "collab" | "50_50" | "1v1" | "1v1_screen"; export interface LiveLayoutOptions { preset: LiveLayoutPreset; cameraPosition?: CameraPosition; cameraSize?: CameraSize; /** * True when the shared surface already contains the creator's own camera PiP * and the backend must hide its template camera. For explicit screen_only, * the SDK sets this true automatically. */ screenHasCameraPip?: boolean; sharedSourceType?: SharedSourceType | null; brandingAlreadyVisible?: boolean | null; branding?: Record; content?: Record; } export interface LiveLayoutResult { ok: boolean; preset: "chart_camera" | "chart_only" | "camera_only" | "screen_camera" | "screen_only" | "collab_50_50" | "collab_screen"; mode: "solo" | "host_screen" | "1v1" | "1v1_screen"; state: StreamState; modeResult: unknown; warnings?: string[]; streamStillLive?: boolean; } export interface OverlayUpdate { topBanner?: string; bottomBanner?: string; hideTop?: boolean; hideBottom?: boolean; } /** * A live restream site as returned by getDestinations(). #3 LIVE DESTINATION CONTROL — the * per-site enabled flag is mutable live via setDestinationEnabled() (zero-restart RTMP valve). */ export interface LiveDestination { /** 0-based index into the go-live destination order (use as destIndex). */ index: number; platform: string; /** direct_oauth, direct_rtmp, restream_bridge, etc. */ source?: string | null; channelUrl: string | null; liveUrl: string | null; /** verified URL when available, else liveUrl. */ watchUrl?: string | null; status: string | null; /** pending | retrying | verified | failed */ verificationStatus?: string; verificationError?: string | null; /** Same as verificationError until verified, convenient for UI display. */ error?: string | null; attempts?: number; lastVerifiedAt?: string | null; /** false = the site was disabled via setDestinationEnabled(idx,false) — its RTMP valve is muted. */ enabled: boolean; } export type RestreamEvent = | "connections" | "job" | "viewers" | "comment" | "comments" | "error" | "telemetry" | "telemetry-error" | "collab" | "collab-request-sent" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-reassigned" | "collab-snapshot"; /** A collaborative split-screen invite (A invites B; the composite starts on accept). */ export interface CollabInvite { id: string; clientId: string; issuerUserId: string; collaboratorUserId: string; issuerExternalId: string; collaboratorExternalId: string; status: "pending" | "accepted" | "declined" | "revoked" | "ended" | "expired"; collaboratorRestreamOn: boolean; streamJobId: string | null; metadata?: Record; createdAt: string; acceptedAt?: string | null; endedAt?: string | null; expiresAt: string; } export interface CollabInviteList { sent: CollabInvite[]; received: CollabInvite[]; } /** A real-time collab signaling event (from startCollabRealtime()). */ export interface CollabEvent { type: RestreamEvent; invite?: CollabInvite; job?: Job; sent?: CollabInvite[]; received?: CollabInvite[]; [k: string]: unknown; } export interface RequestCollabParams { /** The other creator's external id (same client). */ collaboratorUserId: string; /** Include the collaborator's own audience (destinations + Growth) in the composite. */ collaboratorRestreamOn?: boolean; /** Your publisher session (so the composite can start on accept). */ publisherSessionId?: string; /** Your destination platforms for the composite. */ destinations?: Platform[]; metadata?: Record; } export interface AcceptCollabParams { /** Your existing stream session — becomes the second video in the frame (REQUIRED). */ publisherSessionId: string; overlay?: Overlay; recording?: boolean; metadata?: Record; } export class RestreamStudio extends EventTarget { constructor(opts: RestreamOptions); connections: Connection[]; job: Job | null; viewers: ViewerCounts | null; comments: Comment[]; on(type: "connections", cb: (c: Connection[]) => void): () => void; on(type: "job", cb: (j: Job) => void): () => void; on(type: "viewers", cb: (v: ViewerCounts) => void): () => void; on(type: "comment", cb: (c: Comment) => void): () => void; on(type: "comments", cb: (c: Comment[]) => void): () => void; on(type: "error", cb: (e: Error) => void): () => void; on(type: "state", cb: (s: StreamState) => void): () => void; on(type: "screenshare", cb: (e: { active: boolean; sessionId?: string; trackName?: string; audio?: boolean }) => void): () => void; on(type: "telemetry", cb: (e: unknown) => void): () => void; on(type: "telemetry-error", cb: (e: Error) => void): () => void; on(type: "collab" | "collab-request-sent" | "collab-invite" | "collab-accepted" | "collab-declined" | "collab-revoked" | "collab-ended" | "collab-expired" | "collab-composite" | "collab-reassigned" | "collab-snapshot", cb: (e: CollabEvent) => void): () => void; listConnections(): Promise; listOAuthPlatforms(): Promise; listRestreamChannels(): Promise; /** URL of Restream's add-channel page; YouTube/Facebook return native OAuth URLs in publishable-key mode. */ restreamAddChannelUrl(platform?: string): string; /** * One-call "Connect via Restream" — ensures the Restream account is * connected, opens Restream's add-channel page, and resolves once the new channel * appears. YouTube and Facebook are routed to native OAuth instead. Returns the * new channel, a native placeholder, or null if the user closed the popup without * adding one. Must be called from a user gesture (opens a popup). */ connectRestreamChannel( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise; /** Load + cache the per-client routing map; call once on init. */ loadRouting(): Promise; /** Effective route for a platform, from the cached per-client routing map. * * Returns `null` when routing has not been loaded, or when this client has no entry for the * platform — NEVER a guess. Routing is a per-client admin setting, so no built-in default can * know it; one used to exist and reported `"oauth"` for youtube on a client that bridges it. * Use `isRoutingLoaded()` to tell "not loaded" from "no entry". */ routeFor(platform: string): RoutingValue | null; /** True once the per-client routing map has been fetched. Until then `routeFor()` returns null * and the routing-dependent helpers throw an explicit "routing not loaded" error rather than * assuming a route. */ isRoutingLoaded(): boolean; /** How this platform is connected. Falls back to routeFor() when the backend does not * send `connectMethod`, so an older backend behaves exactly as before. */ connectMethodFor(platform: string): ConnectMethod | null; /** True when connecting needs a creator-supplied RTMP URL + stream key (X, Instagram). */ requiresStreamKey(platform: string): boolean; /** True when that key form must also collect the creator's own ingest URL — Instagram and X * issue one per creator (Instagram's per region); Kick does not. Server-supplied. */ requiresRtmpUrlFor(platform: string): boolean; /** Store a creator-supplied RTMP URL + stream key for a manual-connect platform. */ submitStreamKey( platform: string, creds: { rtmpUrl: string; streamKey: string; username?: string } ): Promise; /** Single dynamic connect entry point — routes to native OAuth or the Restream bridge * per the admin's configuration. Must be called from a user gesture. */ connectPlatform( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | ManualConnectRequired | null>; connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>; disconnect(platform: Platform): Promise; /** Switch which Facebook Page you go live / comment as. `pageId` must be one of the connection's `pages`. Requires token (hardened) auth. Returns the refreshed connections. */ selectFacebookPage(pageId: string): Promise; arm(opts: { destinations: Platform[] }): unknown; goLive(opts: GoLiveParams): Promise; stopLive(): Promise; refreshJob(): Promise; /** * Report how many people are watching ON FOMO right now (absolute count, not a delta). * Merged into the same `total`/`platforms` that `viewers` events and GET /viewers return. * Call every 10-15s while live; the contribution expires ~45s after reports stop. */ reportViewers(count: number, jobId?: string): Promise<{ ok: boolean; total: number; platforms: Record }>; /** Current per-destination live-control status and platform failure details for the active job. */ getDestinations(): Promise<{ ok: boolean; destinations: LiveDestination[]; liveControlSupported: boolean }>; /** Recording status/video URL for a completed or active stream. Defaults to the current job. */ getRecording(jobId?: string): Promise; /** Ready replay videos for a creator feed. Defaults to this studio's userId. */ getCreatorRecordings(userId?: string, opts?: { limit?: number; offset?: number; includeProcessing?: boolean }): Promise; /** Best-effort replay thumbnail analytics. Does not affect playback. */ recordThumbnailEvent( jobId: string | undefined, opts: { eventType: "impression" | "tap" | "click" | "view"; thumbnailUrl?: string; source?: string; metadata?: Record } ): Promise<{ ok: boolean }>; /** Update platform-visible live title/description/coin metadata without restarting RTMP. */ updateLiveMeta(input: LiveMetaUpdate, jobId?: string): Promise; sendChat(message: string, platforms?: Platform[]): Promise; updateOverlay(opts: OverlayUpdate): Promise; /** Push real-time presence/layout state (screen-share/camera/collab + camera corner/size). * Call on every change; sends are coalesced (trailing-debounced). Returns the merged state. */ setStreamState(patch: StreamState): StreamState; /** Push restream output presentation/branding settings. Returns the merged local state. */ setRestreamPresentation(patch: RestreamPresentation): StreamState; /** Creator-facing live layout control. Sends state immediately, then switches mode. */ setLiveLayout(input: LiveLayoutOptions | LiveLayoutPreset): Promise; /** Register the creator's raw getUserMedia camera stream for camera-only PiP surfaces. */ setRawCameraStream(stream: MediaStream): MediaStream; /** Bind a video element to the registered raw camera stream only. Returns a cleanup function. */ bindCameraOnlyPip(videoEl: HTMLVideoElement, opts?: CameraOnlyPipOptions): () => void; /** Registered raw camera stream, or null when not set. */ readonly rawCameraStream: MediaStream | null; /** Last presence/layout state set locally. */ readonly streamState: StreamState; /** Capture the screen and publish it as its OWN Cloudflare session (a separate layer the * backend composites via templates). Pass the returned sessionId to goLive({ screenSessionId }). * Restream-only; must be called from a user gesture. * * When `audio: true` is passed (and the user picked a tab/window that exposes audio in their * browser), the screen's audio is ALSO published under trackName "screen-audio" and mixed * into the broadcast server-side (host mic + screen audio). The boolean `audio` in the result * reports whether a screen-audio track was actually published (false = requested but no track). */ startScreenShare(opts?: ScreenShareOptions): Promise; /** Replace the current screen/window/canvas video source without recreating the CF session/layout. */ switchScreenShare(opts?: SwitchScreenShareOptions): Promise; /** Stop screen sharing (close the Cloudflare track(s) + peer connection). */ stopScreenShare(): Promise; /** Cloudflare session id of the live screen-share publish, or null. */ readonly screenSessionId: string | null; /** Wait until a published Cloudflare Realtime session is genuinely transmitting media * (connectionState 'connected' AND every active sender's outbound-rtp.packetsSent > minPackets), * not merely ICE/SDP-connected. Call this RIGHT AFTER your tracks/new/publish helper resolves * and BEFORE you signal fan-out (goLive / requestCollab / acceptCollab / setMode) so the * restream-side pull never races the publisher's first RTP packets ('Track not found on * remote peer'). Pure local getStats polling; no server round-trip. Rejects on timeout / * connection failure. New in 0.9.0. */ waitUntilPublishing(pc: RTCPeerConnection, opts?: { timeoutMs?: number; minPackets?: number; pollMs?: number }): Promise<{ ok: true; senders: number }>; startRealtime(jobId: string): Promise; stopRealtime(): void; // collaborative split-screen requestCollab(opts: RequestCollabParams): Promise; acceptCollab(inviteId: string, opts: AcceptCollabParams): Promise<{ invite: CollabInvite; job: Job }>; declineCollab(inviteId: string): Promise; revokeCollab(inviteId: string): Promise; setCollabRestream(inviteId: string, on: boolean): Promise; endCollab(jobId?: string): Promise; listCollabInvites(): Promise; startCollabRealtime(): Promise; stopCollabRealtime(): void; /** Read-only viewer mode: stream a streamer's live social chat + viewer counts. */ watch(userId: string, opts?: { pollMs?: number }): Promise<{ id: string } | null>; /** Stop watching (clears the poll + realtime streams). */ unwatch(): void; destroy(): void; } export interface UseRestream { studio: RestreamStudio; connections: Connection[]; job: Job | null; viewers: ViewerCounts | null; comments: Comment[]; error: Error | null; collabInvites: CollabInviteList; /** Only accepted/live collaboration invites. Pending requests are intentionally excluded. */ activeCollabInvites: CollabInvite[]; connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>; disconnect(platform: Platform): Promise; /** Switch which Facebook Page you go live / comment as. `pageId` must be one of the connection's `pages`. Requires token (hardened) auth. Returns the refreshed connections. */ selectFacebookPage(pageId: string): Promise; listConnections(): Promise; listOAuthPlatforms(): Promise; listRestreamChannels(): Promise; restreamAddChannelUrl(platform?: string): string; connectRestreamChannel( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise; connectPlatform( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | null>; loadRouting(): Promise; arm(opts: { destinations: Platform[] }): unknown; goLive(opts: GoLiveParams): Promise; stopLive(): Promise; refreshJob(): Promise; /** * Report how many people are watching ON FOMO right now (absolute count, not a delta). * Merged into the same `total`/`platforms` that `viewers` events and GET /viewers return. * Call every 10-15s while live; the contribution expires ~45s after reports stop. */ reportViewers(count: number, jobId?: string): Promise<{ ok: boolean; total: number; platforms: Record }>; updateLiveMeta(input: LiveMetaUpdate, jobId?: string): Promise; sendChat(message: string, platforms?: Platform[]): Promise; updateOverlay(opts: OverlayUpdate): Promise; setStreamState(patch: StreamState): StreamState; setRestreamPresentation(patch: RestreamPresentation): StreamState; setLiveLayout(input: LiveLayoutOptions | LiveLayoutPreset): Promise; setRawCameraStream(stream: MediaStream): MediaStream; bindCameraOnlyPip(videoEl: HTMLVideoElement, opts?: CameraOnlyPipOptions): () => void; /** LIVE DESTINATION CONTROL (#3): list the live job's restream sites with their enabled state. */ getDestinations(): Promise<{ ok: boolean; destinations: LiveDestination[]; liveControlSupported: boolean }>; /** LIVE DESTINATION CONTROL (#3): enable/disable a single restream site mid-stream (zero-restart). */ setDestinationEnabled(destIndex: number, enabled: boolean): Promise<{ ok: boolean; destIndex: number; enabled: boolean; platform: string }>; startScreenShare(opts?: ScreenShareOptions): Promise; switchScreenShare(opts?: SwitchScreenShareOptions): Promise; stopScreenShare(): Promise; requestCollab(opts: RequestCollabParams): Promise; acceptCollab(inviteId: string, opts: AcceptCollabParams): Promise<{ invite: CollabInvite; job: Job }>; declineCollab(inviteId: string): Promise; revokeCollab(inviteId: string): Promise; setCollabRestream(inviteId: string, on: boolean): Promise; endCollab(jobId?: string): Promise; listCollabInvites(): Promise; startCollabRealtime(): Promise; stopCollabRealtime(): void; } export function cameraOnlyStream(source: MediaStream | MediaStreamTrack): MediaStream; export function assertRawCameraSource(source: MediaStream | MediaStreamTrack): MediaStreamTrack; export function bindCameraOnlyPip(videoEl: HTMLVideoElement, cameraSource: MediaStream | MediaStreamTrack, opts?: CameraOnlyPipOptions): () => void; export function useRestream(opts: RestreamOptions): UseRestream; export function useConnections(opts: RestreamOptions): { connections: Connection[]; connect(platform: Platform): Promise<{ platform: Platform; closed?: boolean }>; disconnect(platform: Platform): Promise; /** Switch which Facebook Page you go live / comment as. `pageId` must be one of the connection's `pages`. Requires token (hardened) auth. Returns the refreshed connections. */ selectFacebookPage(pageId: string): Promise; refresh(): Promise; listOAuthPlatforms(): Promise; listRestreamChannels(): Promise; restreamAddChannelUrl(platform?: string): string; connectRestreamChannel( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise; connectPlatform( platform: string, opts?: { pollMs?: number; timeoutMs?: number } ): Promise<{ platform: Platform; closed?: boolean } | RestreamChannel | null>; loadRouting(): Promise; }; export function useLiveChat(studio: RestreamStudio): { comments: Comment[]; send(message: string, platforms?: Platform[]): Promise }; export function useViewers(studio: RestreamStudio): ViewerCounts | null; export function useWatch( opts: { apiBase: string; publishableKey?: string; tokenEndpoint?: string }, userId: string ): { comments: Comment[]; viewers: ViewerCounts | null; job: Job | null };