Zeli AvatarDeveloper docs

JavaScript SDK / Media

Media

Inbound audio and video arrive on the WebRTC media plane. The SDK can attach them to a <video> element for you, correct defaults included, or hand you the raw tracks.

The one-call path

const { session, muted, reason } = await client.streamToVideoElement("stage");

target is a <video> element, its id, or a CSS selector. An id is tried first and a selector second, because both are things a developer will reasonably pass and guessing wrong should not be an error. Failing to resolve one throws ConfigurationError.

The call sets playsInline so iOS does not take over the whole screen, sets srcObject, and starts playback.

session.streamToVideoElement(target) does the same for a session you already have.

The autoplay problem

Chrome and Safari will not start an unmuted media element without a prior user gesture. Muted autoplay is always allowed. So the SDK tries with sound, falls back to muted, and returns which happened.

mutedbooleanOptional

True when the browser refused unmuted playback and the element was muted to get the picture moving. Draw an unmute control when this is set, and call session.unmute() from the click handler.

reasonstringOptional

Why it was muted, when it was. Absent on a clean unmuted start.

if (muted) {
  unmuteButton.hidden = false;
  unmuteButton.onclick = () => session.unmute();
}
Reported, not thrown

A blocked unmuted start is a state the browser puts you in, not a failure of the connection. Muting silently would leave you concluding the audio pipeline was broken, which is a worse outcome than an honest report. An app given muted playback needs to draw an unmute control; an app that got sound must not.

session.unmute()

Must be called from inside a user gesture handler, a click or a tap. Called from anywhere else the browser simply refuses again, which looks like the method is broken when it is the policy doing exactly what it says. Throws SessionError if nothing has been attached yet.

The retry is attempted on any play failure rather than only on NotAllowedError: browsers are not consistent about which error name they use for the autoplay block, and the cost of a pointless second attempt is one rejected promise, while the cost of missing the block is a blank avatar.

Doing it by hand

streamToVideoElement is connect() plus the attach. If you want the session without the element:

const session = await client.connect();
videoEl.playsInline = true;
videoEl.srcObject = session.mediaStream as MediaStream;
await videoEl.play();   // you now own the autoplay problem described above

The attach helpers are exported if you want the behaviour without the session wiring:

import { attachStream, resolveVideoElement, unmuteElement } from "@zeligate/zeli-avatar";
 
const el = resolveVideoElement("#stage video");
const { muted } = await attachStream(el, session.mediaStream);
// from a click handler:
await unmuteElement(el);

They work against a small structural VideoElementLike shape rather than HTMLVideoElement, so the package builds and runs under Node, where the DOM lib does not exist.

Tracks

MemberTypeDescription
session.tracksMediaStreamTrackLike[]Inbound tracks, in arrival order.
session.mediaStreamunknownThe tracks as a MediaStream for srcObject, or null under Node.

ZeliEvent.TRACK fires once per inbound track as it arrives, which is the hook to use if you want to route audio and video separately.

import { ZeliEvent, MediaKind } from "@zeligate/zeli-avatar";
 
client.on(ZeliEvent.TRACK, (track) => {
  if (track.kind === MediaKind.VIDEO) attachVideo(track);
  else attachAudio(track);
});
Under Node there is no MediaStream

Node has no MediaStream class, so session.mediaStream is null and session.tracks is the whole story. See Installation for supplying WebRTC.

Choosing what to receive

Pass SessionOptions to connect() or streamToVideoElement():

const session = await client.connect({
  receiveAudio: true,
  receiveVideo: true,
  idleAvatarId: "01-presenter-male",
});

receiveAudio and receiveVideo both default to true. idleAvatarId names a separate clip to hold between utterances; omit it and the box uses the requested avatar's own idle clip.

Zeli Avatar · real-time avatars over WebRTC · self-hostable · AU data residency · source