npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@reactor-models/sana-streaming

v0.1.12

Published

Strongly-typed SDK for the SanaStreaming model on Reactor

Readme

@reactor-models/sana-streaming

Typed JavaScript + React SDK for the SanaStreaming model on Reactor. Version v0.1.12.


Get started

Scaffold a starter app for SanaStreaming with create-reactor-app:

npx create-reactor-app my-app --model=sana-streaming
pnpm dlx create-reactor-app my-app --model=sana-streaming

Install

npm install @reactor-models/sana-streaming
pnpm add @reactor-models/sana-streaming

The package exports a plain-JavaScript client and a set of React bindings. Import whichever you need from @reactor-models/sana-streaming:

import { SanaStreamingModel } from "@reactor-models/sana-streaming";
import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";

React 18 or later is required when using the provider and hooks. The token-loading examples below use React 19's use(); on React 18, fetch the JWT in a useEffect and pass it to the provider once it resolves.


Authenticate

Reactor uses short-lived JWTs for session auth. You hold your API key on your server, mint a token on demand, and the client never sees the raw key. Tokens are valid for 6 hours — if one leaks, it expires on its own.

Mint a JWT with POST https://api.reactor.inc/tokens and the Reactor-API-Key header; the response JSON is { "jwt": "..." }.

JavaScript (Next.js route handler)

// app/api/reactor/token/route.ts
import { NextResponse } from "next/server";

export async function POST() {
  const res = await fetch("https://api.reactor.inc/tokens", {
    method: "POST",
    headers: { "Reactor-API-Key": process.env.REACTOR_API_KEY! },
  });
  const { jwt } = await res.json();
  return NextResponse.json({ jwt });
}

React (provider)

Call the /api/reactor/token route above from a client component and pass the result to the provider:

"use client";

import { use } from "react";
import { SanaStreamingProvider } from "@reactor-models/sana-streaming";
import { ReactorView } from "@reactor-team/js-sdk";

async function getToken() {
  const r = await fetch("/api/reactor/token", { method: "POST" });
  const { jwt } = await r.json();
  return jwt;
}

const tokenPromise = getToken();

export default function App() {
  const token = use(tokenPromise);
  return (
    <SanaStreamingProvider jwtToken={token} connectOptions={{ autoConnect: true }}>
      <ReactorView className="w-full aspect-video" />
    </SanaStreamingProvider>
  );
}

Connect

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

React

The provider takes the JWT as a prop; fetch it from the same /api/reactor/token route the Authenticate example mints:

"use client";

import { use } from "react";
import { SanaStreamingProvider, useSanaStreaming } from "@reactor-models/sana-streaming";

async function getToken() {
  const r = await fetch("/api/reactor/token", { method: "POST" });
  const { jwt } = await r.json();
  return jwt;
}

const tokenPromise = getToken();

function Controller() {
  const { status } = useSanaStreaming();
  return <span>Status: {status}</span>;
}

export default function App() {
  const token = use(tokenPromise);
  return (
    <SanaStreamingProvider jwtToken={token}>
      <Controller />
    </SanaStreamingProvider>
  );
}

Events

Client-to-model commands. The typed surface is SanaStreamingModel (one method per event) in plain JS, and useSanaStreaming() in React — every field name below matches the parameter name the method accepts.

pause

Pause editing after the current chunk; frames stop until resume. Valid only while generating. Emits generation_paused + state, or command_error if not generating or already paused.

Emits: generation_paused, state, command_error

No parameters.

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.pause();

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { pause } = useSanaStreaming();

  return <button onClick={() => pause()}>pause</button>;
}

reset

Abort the current run, clear the prompt and source video, and return to the waiting state. Valid at any time. Emits generation_reset + state; re-arm with set_video / set_prompt / start.

Emits: generation_reset, state

No parameters.

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.reset();

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { reset } = useSanaStreaming();

  return <button onClick={() => reset()}>reset</button>;
}

start

Begin streaming on main_video. In file mode requires a source video (set_video); in live mode transforms the camera track (no upload needed). A prompt is OPTIONAL in both. Emits generation_started + state, or command_error. No effect while already generating.

Emits: generation_started, state, command_error

No parameters.

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.start();

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { start } = useSanaStreaming();

  return <button onClick={() => start()}>start</button>;
}

resume

Resume editing after a pause, continuing from where it stopped. Valid only while paused. Emits generation_resumed + state, or command_error if not paused.

Emits: generation_resumed, state, command_error

No parameters.

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.resume();

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { resume } = useSanaStreaming();

  return <button onClick={() => resume()}>resume</button>;
}

setMode

Select the input source for the next start: file (edit an uploaded clip set via set_video) or live (transform the live camera input track / webcam). Set before start; treat as immutable for the run — call reset to switch. Emits state, or command_error for an unknown mode.

Emits: state, command_error

| Parameter | Type | Required | Description | |---|---|---|---| | mode | "file" \| "live" | | Input source: file or live. (default "file") |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.setMode({ mode: "file" });

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { setMode } = useSanaStreaming();

  return <button onClick={() => setMode({ mode: "file" })}>setMode</button>;
}

setSeed

Set the random seed that controls the edit's randomness, so the same source, prompt, and seed reproduce the same result. Valid at any time, but read once when generation begins — change it then reset to re-seed an in-progress run. Emits state.

Emits: state

| Parameter | Type | Required | Description | |---|---|---|---| | seed | number | | Non-negative noise seed. (min 0, default 0) |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.setSeed({ seed: 0 });

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { setSeed } = useSanaStreaming();

  return <button onClick={() => setSeed({ seed: 0 })}>setSeed</button>;
}

setVideo

Provide the source video to edit, in file mode. Required before start. Returns immediately no matter how long the clip is: the video is consumed gradually as the edit streams, not all at once up front. Emits video_accepted + state, or command_error if the file is missing or is not a video the model can read.

Emits: video_accepted, state, command_error

| Parameter | Type | Required | Description | |---|---|---|---| | video | FileRef | | Reference to a file uploaded via the Reactor presigned-URL protocol. (default null) |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

const fileRef = await sanaStreaming.uploadFile(blob);
await sanaStreaming.setVideo({ video: fileRef });

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { setVideo, uploadFile } = useSanaStreaming();

  async function handlePick(file: File) {
    const ref = await uploadFile(file);
    await setVideo({ video: ref });
  }

  return <input type="file" onChange={(e) => handlePick(e.target.files![0])} />;
}

setPrompt

Set the natural-language instruction that steers the edit. Optional and valid at any time — with no prompt the output stays close to the source video; set or change it (including while streaming) to steer the edit live, visible about one chunk later on a not-yet-shown chunk. Emits prompt_accepted + state, or command_error (the previous prompt is kept) if the prompt cannot be processed.

Emits: prompt_accepted, state, command_error

| Parameter | Type | Required | Description | |---|---|---|---| | prompt | string | | Natural-language edit instruction. (default "") |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.setPrompt({ prompt: "A sunset over the ocean" });

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { setPrompt } = useSanaStreaming();

  return <button onClick={() => setPrompt({ prompt: "A sunset over the ocean" })}>setPrompt</button>;
}

setAnchorInterval

Periodically re-ground the edit on the source video every chunks chunks (0 = never). Over a long edit the output can slowly drift away from the source; re-grounding pulls it back, at the cost of a brief visible refresh on the following chunk. Valid at any time, including while streaming (it takes effect at the next chunk boundary). Each re-ground emits anchored.

Emits: anchored

| Parameter | Type | Required | Description | |---|---|---|---| | chunks | number | | Chunks between anchors; 0 disables anchoring. (min 0, default 20) |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

await sanaStreaming.setAnchorInterval({ chunks: 20 });

React

"use client";
import { useSanaStreaming } from "@reactor-models/sana-streaming";

function Example() {
  const { setAnchorInterval } = useSanaStreaming();

  return <button onClick={() => setAnchorInterval({ chunks: 20 })}>setAnchorInterval</button>;
}

Messages

Model-to-client messages. Register a typed listener with on… on SanaStreamingModel, or a useSanaStreaming… hook in React, to receive only the messages you care about.

state

Emitted as a full snapshot of the session's observable state.

Sent on connect, after every accepted state-changing command, and at each chunk boundary, so a client can render its UI from one message instead of accumulating individual events.

Listener: onState · React hook: useSanaStreamingState

| Field | Type | Description | |---|---|---| | seed | number | Current value of the noise seed. | | paused | boolean | True while generation is paused via pause. | | running | boolean | True while the chunk loop is actively producing frames. | | started | boolean | True once start has been accepted; reset to false by reset. | | has_video | boolean | True once a source video has been set. | | has_prompt | boolean | True once an edit prompt has been set. | | current_chunk | number | Zero-based index of the last completed chunk; 0 before the first. | | current_prompt | unknown | The prompt currently steering the edit, or null. | | anchor_interval | number | Re-ground generation every this many chunks (0 = never). Set via set_anchor_interval. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onState((msg) => {
  console.log(
    "state",
    msg.seed,
    msg.paused,
    msg.running,
    msg.started,
    msg.has_video,
    msg.has_prompt,
    msg.current_chunk,
    msg.current_prompt,
    msg.anchor_interval,
  );
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingState } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingState((msg) => {
  console.log(
    "state",
    msg.seed,
    msg.paused,
    msg.running,
    msg.started,
    msg.has_video,
    msg.has_prompt,
    msg.current_chunk,
    msg.current_prompt,
    msg.anchor_interval,
  );
});

anchored

Emitted each time the edit re-grounds on the source video to pull back drift. Fires every anchor_interval chunks; the following chunk may show a brief visible refresh.

Listener: onAnchored · React hook: useSanaStreamingAnchored

| Field | Type | Description | |---|---|---| | chunk_index | number | Zero-based index of the first chunk generated after this re-ground. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onAnchored((msg) => {
  console.log("anchored", msg.chunk_index);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingAnchored } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingAnchored((msg) => {
  console.log("anchored", msg.chunk_index);
});

command_error

Emitted when a command is rejected; the session's prior state is unchanged.

Listener: onCommandError · React hook: useSanaStreamingCommandError

| Field | Type | Description | |---|---|---| | reason | string | Why the command was rejected. | | command | string | Name of the command that was rejected. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onCommandError((msg) => {
  console.log("command_error", msg.reason, msg.command);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingCommandError } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingCommandError((msg) => {
  console.log("command_error", msg.reason, msg.command);
});

chunk_complete

Emitted once per completed chunk of main_video.

Listener: onChunkComplete · React hook: useSanaStreamingChunkComplete

| Field | Type | Description | |---|---|---| | chunk_index | number | Zero-based index of the chunk that just completed. | | active_prompt | string | The prompt that drove this chunk; empty means the output stayed close to the source. | | frames_emitted | number | Edited pixel frames emitted by this chunk. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onChunkComplete((msg) => {
  console.log(
    "chunk_complete",
    msg.chunk_index,
    msg.active_prompt,
    msg.frames_emitted,
  );
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingChunkComplete } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingChunkComplete((msg) => {
  console.log(
    "chunk_complete",
    msg.chunk_index,
    msg.active_prompt,
    msg.frames_emitted,
  );
});

video_accepted

Emitted after set_video accepts + probes the source clip.

Listener: onVideoAccepted · React hook: useSanaStreamingVideoAccepted

| Field | Type | Description | |---|---|---| | width | number | Width in pixels of the source video, before cropping to the output aspect ratio. | | height | number | Height in pixels of the source video, before cropping to the output aspect ratio. | | num_frames | number | Always 0 — the source is not scanned up front, so its total length is not reported here. | | num_latent_frames | number | Always 0 — reserved; the source length is not reported here. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onVideoAccepted((msg) => {
  console.log(
    "video_accepted",
    msg.width,
    msg.height,
    msg.num_frames,
    msg.num_latent_frames,
  );
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingVideoAccepted } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingVideoAccepted((msg) => {
  console.log(
    "video_accepted",
    msg.width,
    msg.height,
    msg.num_frames,
    msg.num_latent_frames,
  );
});

prompt_accepted

Emitted when set_prompt accepts a new edit prompt.

Listener: onPromptAccepted · React hook: useSanaStreamingPromptAccepted

| Field | Type | Description | |---|---|---| | prompt | string | The prompt text that was accepted. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onPromptAccepted((msg) => {
  console.log("prompt_accepted", msg.prompt);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingPromptAccepted } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingPromptAccepted((msg) => {
  console.log("prompt_accepted", msg.prompt);
});

conditions_ready

Emitted after set_prompt / set_video so the client can tell if start will succeed (only a video is required; a prompt is optional).

Listener: onConditionsReady · React hook: useSanaStreamingConditionsReady

| Field | Type | Description | |---|---|---| | has_video | boolean | True once a source video has been set. | | has_prompt | boolean | True once an edit prompt has been set. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onConditionsReady((msg) => {
  console.log("conditions_ready", msg.has_video, msg.has_prompt);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingConditionsReady } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingConditionsReady((msg) => {
  console.log("conditions_ready", msg.has_video, msg.has_prompt);
});

generation_reset

Emitted when reset aborts the run and clears the prompt and source video.

Listener: onGenerationReset · React hook: useSanaStreamingGenerationReset

| Field | Type | Description | |---|---|---| | reason | string | Short human-readable reason the reset was issued. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationReset((msg) => {
  console.log("generation_reset", msg.reason);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingGenerationReset } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationReset((msg) => {
  console.log("generation_reset", msg.reason);
});

generation_paused

Emitted when pause succeeds and frames stop until resume.

Listener: onGenerationPaused · React hook: useSanaStreamingGenerationPaused

| Field | Type | Description | |---|---|---| | chunk_index | number | Index of the last completed chunk before pausing. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationPaused((msg) => {
  console.log("generation_paused", msg.chunk_index);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingGenerationPaused } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationPaused((msg) => {
  console.log("generation_paused", msg.chunk_index);
});

generation_resumed

Emitted when resume succeeds and frames continue from where they stopped.

Listener: onGenerationResumed · React hook: useSanaStreamingGenerationResumed

| Field | Type | Description | |---|---|---| | chunk_index | number | Index of the last completed chunk before resuming. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationResumed((msg) => {
  console.log("generation_resumed", msg.chunk_index);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingGenerationResumed } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationResumed((msg) => {
  console.log("generation_resumed", msg.chunk_index);
});

generation_started

Emitted once when start succeeds and frames begin streaming.

Listener: onGenerationStarted · React hook: useSanaStreamingGenerationStarted

| Field | Type | Description | |---|---|---| | prompt | string | The prompt active at the start of generation (may be empty). | | chunk_num | number | Total chunks the run will produce (0 — not known up front). | | frame_num | number | Approximate total edited pixel frames (0 — not known up front). |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationStarted((msg) => {
  console.log(
    "generation_started",
    msg.prompt,
    msg.chunk_num,
    msg.frame_num,
  );
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingGenerationStarted } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationStarted((msg) => {
  console.log(
    "generation_started",
    msg.prompt,
    msg.chunk_num,
    msg.frame_num,
  );
});

generation_complete

Emitted when the whole source clip has been edited.

Listener: onGenerationComplete · React hook: useSanaStreamingGenerationComplete

| Field | Type | Description | |---|---|---| | total_chunks | number | Total chunks produced by the run. |

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onGenerationComplete((msg) => {
  console.log("generation_complete", msg.total_chunks);
});
await sanaStreaming.connect(jwt);

React

import { useSanaStreamingGenerationComplete } from "@reactor-models/sana-streaming";

// Inside a React component wrapped by <SanaStreamingProvider>:
useSanaStreamingGenerationComplete((msg) => {
  console.log("generation_complete", msg.total_chunks);
});

Tracks

Named media channels between your app and the SanaStreaming model. Use the typed helpers below — SanaStreamingModel.publish<Track> / on<Track> in plain JS, and useSanaStreamingTrack or the per-track <SanaStreaming<Track>View> components in React — so track names are checked at compile time.

camera

A video channel you push media into (for example, the user's webcam).

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
await sanaStreaming.connect(jwt);

const media = await navigator.mediaDevices.getUserMedia({ video: true });
const videoTrack = media.getVideoTracks()[0];
await sanaStreaming.publishCamera(videoTrack);

// later, to stop sending:
await sanaStreaming.unpublishCamera();

React

"use client";
import { SanaStreamingCameraView } from "@reactor-models/sana-streaming";

// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
  return <SanaStreamingCameraView />;
}

main_video

A video channel you subscribe to — the model publishes this for your app to render.

JavaScript

import { SanaStreamingModel } from "@reactor-models/sana-streaming";

const sanaStreaming = new SanaStreamingModel();
sanaStreaming.onMainVideo((track, stream) => {
  // attach to a <video> element, pipe to a canvas, etc.
  videoEl.srcObject = stream;
});
await sanaStreaming.connect(jwt);

React

"use client";
import { SanaStreamingMainVideoView } from "@reactor-models/sana-streaming";

// Inside a component wrapped by <SanaStreamingProvider>:
export function Example() {
  return <SanaStreamingMainVideoView className="w-full aspect-video" />;
}