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-safeEvery 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/backshorthands (or the underlyingfacingModeprop) pick the camera declaratively; changing them mid-session — or callingref.flip()— swaps the published source viareplaceTrack. No renegotiation, no new session. The built-in flip control (flipControl="auto", the default) gives touch devices the switch for free. - Preview mirroring —
mirror="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 meterOr 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:
- Getting Started — providers, auth, your first stream
- App reference — the full
SessionStreamsurface (attach/attachVideo,seek,messages/emit) - Session lifecycle — gating publishes on
whenLive, honest waiting UI