Participants
room.localParticipant.identity; // this session's own identity
room.localParticipant.tracks; // LocalTrack[] currently published
room.remoteParticipants; // RemoteParticipant[]
participant.identity;
participant.metadata; // opaque, set when the token was minted
participant.tracks; // RemoteTrack[] currently subscribed
room.on('participantJoined', (participant) => {});
room.on('participantLeft', (participant) => {});'use client';
import { useLocalParticipant, useRemoteParticipants, useParticipants } from '@ravenkash/react';
function Roster() {
const local = useLocalParticipant();
const remote = useRemoteParticipants();
const everyone = useParticipants(); // local first, then remote — the order a grid renders in
return everyone.map((p) => <p key={p.identity}>{p.identity}</p>);
}Each hook re-renders only when its own slice changes — useRemoteParticipants() doesn't re-render on a local mute.
import { useParticipants, useRemoteParticipants } from '@ravenkash/react-native';
function Roster({ room }) {
const everyone = useParticipants(room);
return everyone.map((p) => <Text key={p.identity}>{p.identity}</Text>);
}Takes room directly rather than reading it from context — see
React Native SDK for why.
room.localParticipant.identity;
room.remoteParticipants; // List<RavenParticipant>
room.participants; // local first, then remote
room.participantChanges.listen((participants) {
// rebuild — fires on join/leave and on any track change
});metadata is whatever your backend attached when it minted the token —
Livqeno never inspects or interprets it.
Device selection
Web-only — React Native and Flutter select devices through the OS, not a JavaScript device-enumeration API.
const devices = await client.getDevices();
// { deviceId, label, kind }[] — kind: 'videoinput' | 'audioinput' | 'audiooutput'
// labels are populated only once permission has been granted at least once
await room.setCameraDevice(deviceId);
await room.setMicrophoneDevice(deviceId);
await room.setSpeakerDevice(deviceId); // where the browser supports setSinkId — not Safari; throws DEVICE_NOT_FOUND there
const unsubscribe = client.onDeviceChange(() => {
// re-enumerate — a camera or mic was connected or disconnected
});On React Native, the closest equivalent is switching front/rear camera
— see Flutter's switchCamera() for the Dart SDK's version of the same phone-only concern.
Sending data
await room.sendData('hello'); // string or Uint8Array
room.on('dataReceived', (payload, participant) => {
console.log(new TextDecoder().decode(payload), participant?.identity);
});Works in an empty room with no camera or microphone published. The first
sendData() has to negotiate a data channel, which takes one round trip;
the SDK does that itself and resolves once your payload is on the wire.
Anything sent while the channel is still coming up is queued and delivered
in order, so you never have to publish media first or retry.
Receiving needs a listener, and the listener is what sets it up. Livqeno
fans data out over each recipient's own channel, so subscribing to
dataReceived is what provisions yours — do it once after joining, before
you expect anything to arrive. A message sent to a participant whose
channel is still coming up is not delivered to them: sendData() reaches
whoever is connected at the moment it goes out, and is not a queue that
waits for stragglers.
await room.sendData('hello');
room.on('dataReceived', (payload, participant) => {
console.log(new TextDecoder().decode(payload), participant?.identity);
});The same Room class as web — no React Native-specific data API.
Requires the token's publishData grant — throws PERMISSION_DENIED
otherwise. Deliberately minimal: no reliability options, no
per-participant targeting. If you need routed, ordered, or persisted
messages between participants, that's what Chat is for
— it's a first-class service, not a fallback bolted onto the data
channel.
Track model
Track (base: kind, mediaStreamTrack, mediaStream, isMuted, attach(), detach())
├── LocalTrack (+ mute(), unmute(), stop())
└── RemoteTrack
track.attach() / track.attach(existingElement) and track.detach()
are the SDK's only media-element helpers — it's an SDK, not a UI
component library:
room.on('trackSubscribed', (track, participant) => {
videoElement.appendChild(track.attach());
});
room.on('trackUnsubscribed', (track) => {
track.detach().forEach((el) => el.remove());
});import { ParticipantView } from '@ravenkash/react';
<ParticipantView participant={participant} /><ParticipantView> wraps attach/detach for you — an optional,
genuinely optional component; the hooks work with any UI you build
instead.
import { RavenVideoView } from '@ravenkash/react-native';
<RavenVideoView participant={participant} room={room} style={{ flex: 1 }} />Pass room and the view follows track changes — published,
unpublished, muted, resubscribed — by itself.
RavenVideoView(
participant: participant,
room: room, // enables automatic updates
kind: RavenTrackKind.camera,
fit: RavenVideoFit.cover,
)Disposes its native texture with the widget — dropping room is how a
scrolling grid leaks a native view per rebuild.
Common errors
| Error | Why | Fix |
|---|---|---|
PERMISSION_DENIED on sendData() | Token was minted without publishData. | Set publishData: true when calling tokens.create(). |
DEVICE_NOT_FOUND on setSpeakerDevice() | Browser doesn't support setSinkId (Safari). | Fall back to system output selection — there's no workaround. |
Related
- Audio & Video — enabling the devices whose tracks show up here.
- RTC → Overview — the full event list.
- Chat — for anything data messages are too minimal for.