docs
TypeScript SDK

Inputs & Outputs

Copy-paste recipes — send the camera or mic up, render video, audio, and images coming back

Every recipe on this page is one snippet against the published @urun-sh/react / @urun-sh/core. A session's media is named streams: the browser sends by attaching a local track to a stream, and shows by rendering the inbound track a stream produces. The stream name is the contract with Python — session.stream("video")ctx.stream("video").

I want to…Use
Send the camera (incl. phone front/back)<Camera visible />
Send the camera without showing it<Camera /> (viewless is the default) or manual attachVideo
Send the mic<Mic /> (send-only) / <Voice /> (voice loop)
Play audio out — just sound<Audio /> (invisible by default)
Play audio out — with a player<Audio controls />
Show video out<Video /> (or useSessionTrack + your own <video>)
Show images out<Image /> (progressive), or the data lane for stills

All snippets assume a session from useApp():

const app = useApp()
const session = app.generate({ prompt: 'a sunset' })   // render-safe

Every media component takes an explicit session prop — or inherits one from the nearest <Session>, so a page's media blocks need nothing but stream names:

import { Session, Camera, Video, Audio } from '@urun-sh/react'

<Session session={session}>
  <Camera stream="video" />       {/* send, viewless */}
  <Video stream="video" />        {/* show the runtime's output */}
  <Audio stream="audio" />        {/* play the runtime's audio */}
</Session>

Send the camera

Camera captures the camera and publishes it on a named stream (default "video"). It is viewless by default — pass visible for the self-view. On touch devices with more than one camera, the visible self-view gets a front/back flip button automatically once active.

import { Camera } from '@urun-sh/react'

<Camera
  session={session}
  stream="video"            // matches ctx.stream("video") / ctx.inbound("video")
  front                     // selfie camera (default is the rear camera)
  visible                   // render the self-view (omit for viewless sending)
  onError={(e) => console.error(e)}
/>
  • Phone front/back — the front / back shorthands (or the underlying facingMode prop) pick the camera declaratively; changing them mid-session — or calling ref.flip() — swaps the published source via replaceTrack. No renegotiation, no new session. The built-in flip control (flipControl="auto", the default) gives touch devices the switch for free.
  • Preview mirroringmirror="auto" (default) mirrors only the front camera preview, like every selfie view; the published track is never mirrored.
  • Capture defaults — 960×720 @ 8fps ideal (model-sampling rates). Pass constraints={{ frameRate: { ideal: 30 } }} for motion-critical apps.

The Python side consumes it as the head of a pipeline: ctx.stream("video") | ….

Send without showing (viewless)

Viewless is Camera's default — mounting it is the intent to send:

// 1 — declarative: capture/publish/flip logic, no DOM output
<Camera session={session} stream="video" />
// 2 — manual: your own getUserMedia, published with attachVideo
const camera = await navigator.mediaDevices.getUserMedia({ video: true })
await session.whenLive()                                   // gate publishes on live
await session.stream('video').attachVideo(camera.getVideoTracks()[0])
// later: await session.stream('video').detachVideo()

attachVideo is the video producer (attach is mic/audio); a second attachVideo call swaps the source via replaceTrack. In viewless mode you can still own a preview yourself through onStream.

Send the mic

Send-only (the runtime listens, nothing comes back on this name) — Mic is the viewless capture-only component; pass visible for a minimal input-level indicator:

import { Mic } from '@urun-sh/react'

<Mic session={session} stream="mic" />           // ctx.stream("mic") on Python
<Mic session={session} stream="mic" visible />   // + level meter

Or manually, with your own getUserMedia:

const media = await navigator.mediaDevices.getUserMedia({
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
})
await session.whenLive()
await session.stream('mic').attach(media.getAudioTracks()[0])
// stop: await session.stream('mic').detach()

For a voice loop — mic up and assistant speech back on the same name — use Voice, which handles the capture, the live+attach dance, speakerphone-safe constraints (mono, echo cancellation), and gesture-safe playback in one component:

import { Voice } from '@urun-sh/react'

<Voice session={session} stream="audio" onError={(e) => console.error(e)} />

One capture per page (iOS)

Every media block on a page shares one capture controller by default (sharedCaptureController() from @urun-sh/core) — iOS WebKit allows a single active getUserMedia capture per page. Camera, Mic, and Voice already cooperate; keep manual captures to one at a time.

Output audio

Audio plays a session's inbound audio and handles the browser autoplay dance. It renders an invisible element by default — just sound; pass controls for the native player UI:

import { Audio } from '@urun-sh/react'

// Just sound (default): no visible UI
<Audio session={session} stream="audio" />

// With a native audio player
<Audio session={session} stream="audio" controls />

When the browser blocks unmuted autoplay, onUnlockChange(false) fires — render a "tap to enable audio" affordance and resume from that gesture (resumeUrunAudioContext() from @urun-sh/react covers the shared AudioContext). For level meters, feed the track from onTrack into useUrunAudioLevel.

Output video

Video renders a session's named inbound stream — persistent srcObject, the iOS-safe attribute set (autoPlay muted playsInline), and an honest empty state (placeholder) until the first track arrives:

import { Video } from '@urun-sh/react'

<Video session={session} stream="video" placeholder={<Spinner />} />

The manual pattern underneath — the latest inbound track into your own <video> element:

import { useSessionTrack, type SessionInterface } from '@urun-sh/react'
import { useEffect, useRef } from 'react'

function Player({ session }: { session: SessionInterface }) {
  const track = useSessionTrack(session, 'video')
  const ref = useRef<HTMLVideoElement>(null)

  useEffect(() => {
    if (track && ref.current) ref.current.srcObject = new MediaStream([track])
  }, [track])

  return <video ref={ref} autoPlay muted playsInline />
}

autoPlay muted playsInline is the reliable combination for live video on every browser, including iOS. For player chrome (controls, seek/scrub, VOD playback of a recording URL), VideoPlayer ships behind the @urun-sh/react/video subpath — video.js is an optional peer, so install video.js and import from the subpath only in apps that use it:

import { VideoPlayer } from '@urun-sh/react/video'

<VideoPlayer src="https://…/recording.mp4" controls />  // VOD (a DVR recording URL)

For live session streams use the root entry's <Video> — a raw <video> + MediaStream needs no player framework.

Output images

Image generation on uRun streams progressively as H.264 video — the runtime emits each finished step as an intra frame on a named session stream, so the browser shows the image filling in live. Image is the semantic wrapper over that lane (image-appropriate defaults; stream name defaults to "image"):

import { Image } from '@urun-sh/react'

<Image session={session} stream="image" />

The final frame is the image; the Output video pattern renders the same lane by hand.

For discrete stills (a URL or metadata per finished image), emit on a named data stream and render each message:

import { useStreamMessages, type SessionInterface } from '@urun-sh/react'

function Gallery({ session }: { session: SessionInterface }) {
  const images = useStreamMessages(session, 'images')   // ctx.stream("images").emit({url}) on Python
  return (
    <div>
      {images.map(({ at, payload }) => {
        const img = payload as { url: string; alt?: string }
        return <img key={at} src={img.url} alt={img.alt ?? ''} />
      })}
    </div>
  )
}

(The built-in ImageFrame component renders the same {src, alt, caption} shape with Zod-validated props — see Component Registry.)

Related:

On this page