Skip to main content
Deploy the generated-video app before starting this walkthrough. Use its application ID, such as your-account/your-video-app. Do not append /start-session or supply a URL.
The WMA browser API is experimental. This walkthrough uses @fal-ai/client@1.11.0-alpha.5, independently of the stable Python fal package. The JavaScript latest version does not necessarily include these APIs. Pin the version shown below.

Install the client and server proxy

Run this command in your web application:
Keep FAL_KEY in your server environment. Never put the key in browser code or a NEXT_PUBLIC_ environment variable. The browser sends authenticated requests through your server proxy. The proxy adds the fal credential. For a Next.js App Router application, create app/api/fal/proxy/route.ts:
The explicit URL patterns permit WMA signaling and the app ICE fallback. The default proxy patterns do not include wma.fal.run. This minimal route is suitable for a local development app. Before exposing it, require your application’s user authentication and authorization on the route. Limit access to the endpoints that your application needs, and apply appropriate rate limits. The proxy’s fal credential does not authenticate your application’s users. WMA signaling needs streaming responses. Keep response streaming enabled on your proxy and hosting platform. The Next.js Pages Router proxy does not support streaming responses. Use the App Router integration above.

Receive video and send controls

Add these elements to a page:
Run the following TypeScript in the browser after the elements exist. Use your application’s bundler to resolve the package imports. Replace the application ID with the ID from your deployment.
Select Connect, then Fast sunset. The app updates the video and acknowledges the command in the message area. Select Disconnect to release the session. In a component framework, also call close() when the component unmounts. fal.realtime.open() returns a handle immediately. Its ready promise resolves when the session is ready. Use onState for lifecycle changes, onData for raw message strings, and onMedia for incoming streams. Parse and validate onData messages before your application uses their values.

Session states

A session has four states. The session moves from opening to live, and then ends in failed or closed. fal.realtime.open() returns the session in the opening state. onState reports each later change. Both end states are final. A failed session does not change to closed later.

Show connection progress

Your UI can show three phases:
  1. Waiting for a runner. The state is opening. The client prepares its network candidates, and then the bridge gets a runner. If no runner is warm, this phase includes the cold start of a runner. The client does not tell you if it waits for capacity or for a runner to start.
  2. Connecting. The state is live. A runner accepted the session. The browser and the runner complete the WebRTC connection.
  3. Streaming. The video element starts to play the media.
For apps that return media, onMedia can fire just before or just after the live state. Attach the stream when onMedia delivers it. Use the playing event of the video element to find when the media plays. You can call send() before the connection is complete. The client keeps up to 64 messages and sends them in order when the control channel opens. If you send more, the client drops the oldest messages.

Limit the wait for a runner

By default, the client does not set a deadline for the bridge to get a runner. To stop the wait after a fixed time, set negotiationTimeoutMs. This version of the openSession() function from the walkthrough also shows the three phases:
A failed session does not report closed. After a failure, the status keeps the error text until the next connection. The timeout starts when the client sends its offer. The timeout stops when the answer of the runner arrives. The timeout does not limit the WebRTC connection after the answer. If no runner answers before the timeout, the session changes to failed, and onError receives the error. To cancel from your UI, call close() or use an abortSignal. The session then changes to closed.

Choose the media direction

The client options must match the deployed app’s contract. These directions are from the browser’s perspective: receive: [] creates a session without incoming media tracks. Control messages can still travel in both directions. For audio output, request receive: ["audio"] and attach the received stream to an audio element.

Send camera video

Call getUserMedia() from an explicit user action, such as a Connect button. Camera capture requires HTTPS or localhost and browser permission. This function connects to the camera-effects example. It returns a cleanup function for your Disconnect button or component unmount handler.
The client does not own your camera stream. Stop its tracks when you finish, including after connection failures. Disable repeated Connect actions while opening. Retain the returned cleanup function and call it when leaving the page. For a UI that cancels during opening, retain the session handle immediately and call close() before ready resolves. For motion measurements, change receive to [] and use the motion app ID. Read its JSON measurements through onData. That app does not return video.

Verify network and lifecycle behavior

The WMA extension requests ICE configuration through the authenticated bridge. To test a TURN-only path, add iceTransportPolicy: "relay" to the options passed to fal.realtime.open(). This test requires working TURN provisioning. Do not substitute a public STUN server for TURN. Use the returned session’s connection information when diagnosing network failures:
Disconnect before creating a replacement session. Keep the endpoint ID and each app’s media contract consistent. For server cleanup and billing checks, see the WMA deployment guide.