A track is one stream of audio or video, like your camera or mic. This page covers a track's whole life: create it, send it, show it, mute it, and remove it.
Use this page when the simple enableCamera() buttons from
Audio & Video aren't enough: for example a preview
screen, a custom video source like a canvas, or a quality indicator per
person. All code runs in your app (@livqeno/rtc unless noted).
Two ways to publish
The quick way (most apps)
const camera = await room.enableCamera();
const mic = await room.enableMicrophone();
const share = await room.enableScreenShare();Each call captures the device, sends it, and returns the LocalTrack.
If that device was already on, it returns undefined.
The step-by-step way
Use this when you need the track before sending it. For example, for a preview before joining, a specific device, or a check before going live.
const track = await client.createCameraTrack(deviceId);
// inspect, attach locally for a preview…
await room.publish(track);When you're done:
await room.unpublish(track);
track.stop(); // release the camera light| Call | What it does |
|---|---|
room.unpublish(track) | Stops sending the track. |
track.stop() | Releases the camera or mic (the camera light turns off). |
Do both when you're done with a track you created yourself.
A source Livqeno does not capture
Want to send something the SDK can't capture itself? For example, a
whiteboard drawn on a canvas.captureStream(), a Web Audio graph, a video
file, or a virtual camera. Wrap your own MediaStreamTrack (the browser's
raw audio/video track object):
const canvasTrack = canvas.captureStream(30).getVideoTracks()[0];
const track = client.createCustomTrack(canvasTrack, { source: 'camera' });
await room.publish(track);sourcetells others what the track is, so they know which tile to put it in. Default:camerafor video,microphonefor audio.- Muting, stats, and events work the same as for any other track.
- You stop your own source (the canvas, the oscillator, the file).
room.unpublish(track)stops the track itself. - You can't build a
LocalTrackby hand. Always usecreateCustomTrack().
Show a track
To show a track, attach it to an <video> or <audio> element. You
usually do this for remote tracks (other people) in trackSubscribed, and
for your own camera if you want a self-view.
const element = track.attach();
container.append(element);attach() creates the element for you. It sets autoplay, playsInline,
and muted correctly. Without these, video often doesn't play,
especially on iOS Safari.
Want to use your own element?
track.attach(myVideoElement);Clean up when you're done:
track.detach(); // every element this track is attached to
track.detach(myVideoElement); // just onedetach() stops playback. With adaptiveStream on (the default), a video
detached from every element is also paused at the server, so it stops
using bandwidth too. See Pause a video.
Mute is not unpublish
await camera.mute(); // stop sending, stay published
await camera.unmute(); // instant| Mute | Unpublish | |
|---|---|---|
| The track | Stays in the room | Is removed |
| Coming back | Instant | Needs to publish again |
| Others get | trackMuted / trackUnmuted | trackUnpublished |
| Others' UI | Keeps your tile, shows a muted icon | Removes your tile |
| Use it for | "Mic off" button | Turning a feature off |
mute() and unmute() only work on your own tracks (LocalTrack).
You can't mute someone else. You can only stop subscribing to them.
Track kinds
Your layout usually depends on what a track is: a camera goes in the person's tile, a screen share goes in the big tile.
type TrackKind = 'camera' | 'microphone' | 'screenShare' | 'screenShareAudio' | 'unknown';WebRTC itself doesn't know if a stream is a camera or a screen. So Livqeno labels each track when it's published. This is how other users know to show a screen share in the big tile.
A track that arrives without a label is unknown. Livqeno doesn't guess.
TrackKind tells you how a track was captured — camera, mic, or screen. A
remote participant's LiveTrackInfo.kind, from the server, only tells you
whether it's audio or video. Use trackKindToMediaKind() to turn one into
the other when you need to compare them:
type MediaKind = 'audio' | 'video' | 'unknown';
function trackKindToMediaKind(kind: TrackKind): MediaKind;This only works one way. You can turn 'camera' into 'video', but you
can't turn 'video' back into 'camera' or 'screenShare' — once you only
have the media type, that detail is gone.
Devices
Let users choose which camera, mic, or speaker to use (web only):
const cameras = await client.getDevices('videoinput');
const mics = await client.getDevices('audioinput');
const speakers = await client.getDevices('audiooutput');
await room.setCameraDevice(cameras[0].deviceId);
await room.setMicrophoneDevice(mics[0].deviceId);
await room.setSpeakerDevice(speakers[0].deviceId);- Switching a device swaps the media on the track you're already sending. Nobody else in the room notices.
setSpeakerDevice()needsHTMLMediaElement.setSinkId. Safari doesn't support it, so it throwsDEVICE_NOT_FOUNDthere.
Update your list when devices are plugged in or out:
const stop = client.onDeviceChange(async () => {
setCameras(await client.getDevices('videoinput'));
});On Flutter, room.switchCamera() flips between the front and back camera.
Stats for one track
Use per-track stats to explain why one person looks bad, for example a "poor connection" badge on just bob's tile.
const stats = await track.getStats();
// bitrateBps (bits per second), packetsLost, packetLossPercent,
// jitterMs, roundTripTimeMs, codec, frameWidth, frameHeight, framesPerSecondbitrateBpsisundefinedon the first sample. It needs two to compare.roundTripTimeMsonly appears on tracks you send.codeconly appears on video you receive.
For the whole room at once, use
room.getConnectionStats().
Simulcast layers
Simulcast means your video is sent in several qualities at once (where the platform supports it). The media server then sends each viewer the quality that suits them: a viewer on a weak phone network gets the low one, a viewer on fast Wi-Fi gets the high one. Nobody's bad connection drags down everyone else.
- By default the SDK chooses for you. On the web,
adaptiveStreamis on by default: each remote video gets the quality that fits its tile size. - To choose yourself on the web, call
track.setLayer('low' | 'medium' | 'high' | 'auto')on a remote video track. For example,'low'for a tiny thumbnail and'auto'to hand control back to the SDK. On Flutter, useLivqenoRoom.requestLayer(participant, kind, layer). - Hidden tiles are paused, not just shrunk: see Pause a video.
- Your own camera only sends the qualities somebody is actually watching. If every viewer is on a small tile, the high-quality copy is switched off until someone needs it, which saves your upload and CPU. This is automatic.
- Switching quality works with VP8, VP9, and H.264 simulcast, and with VP9 and AV1 SVC (see Choose a video codec). With H.265 it can't switch, so use a single quality there.
See Known limitations.
Choose a video codec
By default the camera publishes the browser's default codec (VP8 in
Chrome) as three simulcast qualities. Set videoCodec to pick another:
const client = createRTCClient({ token, endpoint, videoCodec: 'vp9' });final livqeno = Livqeno(token: token, endpoint: endpoint, videoCodec: LivqenoVideoCodec.vp9);const livqeno = new Livqeno({ token, endpoint, videoCodec: 'h264' }); // 'vp8' | 'h264'videoCodec | What is sent | Use it for |
|---|---|---|
'vp8' (default in Chrome) | Three simulcast qualities | Calls, the safest choice |
'h264' | Three simulcast qualities | Hardware encoding on phones; a live-stream host, whose HLS/RTMP output can then copy the video instead of re-encoding it |
'vp9' | SVC: one stream with three spatial and three temporal layers inside it (L3T3_KEY) | Better quality per bit, especially screen content |
'av1' | SVC, as VP9 | Best compression; costs more CPU to encode |
With SVC the media server thins the one stream per viewer by dropping
layers, so track.setLayer(), adaptive streaming and
track.layer work exactly as with simulcast. room.simulcastStatus('camera')
reports 'svc'. If the browser can't encode the codec, or can't do
L3T3_KEY, the camera falls back to VP8 simulcast and the call carries
on.
preferredVideoCodec (from earlier releases) is an alias of videoCodec;
when both are set, videoCodec wins.
React Native supports 'vp8' and 'h264' only: react-native-webrtc can't
set an SVC mode.
Audio redundancy (RED)
Every SDK offers Opus with RED (RFC 2198) first. Each audio packet also carries the previous one, so a single lost packet is recovered from the next and the listener hears nothing. It costs roughly 30 kbps more upload. Listeners whose app doesn't support RED get plain Opus from the server.
On by default. Turn it off with audioRed: false (web and React Native) or
Livqeno(audioRed: false) (Flutter). It is off automatically while
end-to-end encryption is configured.
Pause a video
A remote video you can't see still costs download bandwidth and decoding, which adds up in a big grid. The server can stop sending it without unsubscribing: resuming takes a fraction of a second and no renegotiation.
Automatically (web, default). With adaptiveStream on, the SDK watches
every element a remote video is attached to. When none of them can be
seen for half a second, it pauses the video. It counts as hidden when the
element is scrolled out of view, detached, removed from the page, or zero
size, or when the browser tab is in the background (a picture-in-picture
video keeps playing). It resumes as soon as the element is visible again.
By hand, on any video you receive:
track.setEnabled(false); // pause: nothing is sent to you
track.setEnabled(true); // resume
track.isEnabled; // false while paused by yousetEnabled(false) wins over everything else: neither adaptive streaming
nor setLayer() turns it back on. A layer pinned with setLayer() is
never paused for being hidden, because pinning means you want that video.
Audio is never paused.
Renderers the SDK can't watch (a <canvas>, WebGL, or a React Native
view) can report visibility themselves with track.setVisible(visible).
React Native's LivqenoVideoView does this for you; see
React Native.
Is it playing?
Every remote track says whether the server is sending it:
track.subscriptionState | Meaning | Show |
|---|---|---|
'active' | Video is arriving. | The video. |
'paused_bandwidth' | Your connection can't carry even the lowest quality right now. The server resumes it when it can. | A "paused: weak connection" overlay, not a frozen frame. |
'paused_client' | You paused it (or it was never asked for; see below). | Whatever your app shows for an off tile. |
track.layer is the quality being sent: 'low', 'medium', 'high' or
'off'. Both change with the room's trackSubscriptionStateChanged event.
Join with video off
In a large room, join with every remote video paused and only turn on the ones you show:
const client = createRTCClient({ ...grant, autoSubscribe: false });Each remote video then starts as 'paused_client'. It starts arriving
when you attach it to a visible element (with adaptiveStream on), or
when you call track.setEnabled(true) or track.setLayer(...). Audio
always arrives.
Next steps
- Audio & video: the per-platform reference for camera and mic buttons.
- Screen sharing · Events · Diagnostics