> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vivix.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect with Agora

Agora example includes server-side create/close, control WebSocket, and browser playback. Use Node.js 22. The API key stays on the server; the page receives only Session credentials. This local server binds to localhost and manages one session at a time.

## 1. Prepare the files

```bash theme={null}
npm init -y
npm install agora-rtc-sdk-ng
npm install --save-dev esbuild
```

Save these three files in one directory. To start a new session directly, also save `session.json` from [Quickstart](/streaming-avatar/get-started/quickstart).

Select Agora in the create request. Replacing the frontend SDK cannot turn an existing TRTC Session into Agora.

```json theme={null}
{
  "delivery": {
    "media": {
      "transport": "agora"
    }
  }
}
```

### server.mjs

```javascript wrap theme={null}
import http from "node:http";
import { readFile } from "node:fs/promises";
const key = process.env.VIVIX_API_KEY;
if (!key) throw new Error("Set VIVIX_API_KEY");
const responseFile = process.env.SESSION_RESPONSE_FILE;
const config = responseFile ? null : JSON.parse(await readFile("session.json", "utf8"));
let imported = false;
let sessionId;
let starting = false;
async function api(path, body) {
  const res = await fetch(`https://api.vivix.ai/v1/${path}`, {
    method: "POST", headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
    body: JSON.stringify(body)
  });
  const data = await res.json();
  if (!res.ok || data.code !== 0) throw new Error(data.message || `HTTP ${res.status}`);
  return data.data;
}
http.createServer(async (req, res) => {
  try {
    if (req.method === "POST" && req.url === "/session") {
      if (starting || sessionId) { res.writeHead(409); return res.end("Close the current session first"); }
      starting = true;
      try {
        let s;
        if (responseFile) {
          if (imported) throw new Error("This saved session has already been used. Create a new session to restart.");
          const envelope = JSON.parse(await readFile(responseFile, "utf8"));
          if (envelope.code !== 0 || !envelope.data?.session_id) throw new Error("Expected a successful Create Session response");
          s = envelope.data;
          imported = true;
        } else {
          s = await api("realtime-avatar/sessions", config);
        }
        sessionId = s.session_id;
        res.setHeader("Content-Type", "application/json");
        return res.end(JSON.stringify(s));
      } finally { starting = false; }
    }
    if (req.method === "POST" && req.url === "/close") {
      if (sessionId) {
        const result = await api(`realtime-avatar/sessions/${encodeURIComponent(sessionId)}/close`, {});
        if (!["closing", "closed"].includes(result.status)) throw new Error(`Session status: ${result.status}`);
      }
      sessionId = undefined;
      return res.end("Session closure requested");
    }
    const files = { "/": ["index.html", "text/html"], "/app.js": ["app.js", "text/javascript"] };
    const file = req.method === "GET" && files[req.url];
    if (!file) { res.writeHead(404); return res.end(); }
    res.setHeader("Content-Type", file[1]);
    res.end(await readFile(file[0]));
  } catch (e) { res.writeHead(500); res.end(String(e.message)); }
}).listen(3000, "127.0.0.1", () => console.log("http://localhost:3000"));
```

### index.html

```html wrap theme={null}
<!doctype html><html lang="en"><meta charset="utf-8">
<title>Avatar player</title>
<button id="start">Start</button><button id="stop">Stop</button>
<button id="sound" hidden>Enable sound</button>
<p id="status">Ready</p><div id="video" style="width:360px;height:640px;background:#111"></div>
<script src="/app.js"></script></html>
```

### main.js

```javascript wrap theme={null}
import AgoraRTC from "agora-rtc-sdk-ng";
async function connect(s) {
  const m = s.delivery.media.agora;
  if (!m) throw new Error("Expected Agora credentials");
  const rtc = AgoraRTC.createClient({mode: "rtc", codec: "vp8"});
  const sound = document.querySelector("#sound");
  let remoteAudio, remoteVideo;
  AgoraRTC.onAutoplayFailed = () => { sound.hidden = false; };
  sound.onclick = () => {
    sound.hidden = true;
    try {
      remoteAudio?.play();
      remoteVideo?.play("video", {fit: "contain"});
    } catch (e) { sound.hidden = false; status.textContent = e.message; }
  };
  cleanup = async () => {
    sound.hidden = true; sound.onclick = null;
    AgoraRTC.onAutoplayFailed = undefined;
    remoteAudio?.stop(); remoteVideo?.stop();
    await rtc.leave(); rtc.removeAllListeners();
  };
  rtc.on("user-published", async (user, type) => {
    if (String(user.uid) !== String(m.publisher_user_id)) return;
    try {
      await rtc.subscribe(user, type);
      if (type === "video") {
        remoteVideo = user.videoTrack;
        user.videoTrack.on("first-frame-decoded", () => { status.textContent = "Video playing"; });
        user.videoTrack.play("video", {fit: "contain"});
      }
      if (type === "audio") { remoteAudio = user.audioTrack; remoteAudio.play(); }
    } catch (e) { status.textContent = e.message; }
  });
  await control(s);
  const text = String(m.user_id);
  const uid = /^\d+$/.test(text) && Number.isSafeInteger(Number(text)) ? Number(text) : text;
  await rtc.join(m.app_id, m.channel_name, m.token || null, uid);
  window.player = { ws, agora: rtc };
}
const status = document.querySelector("#status");
const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
let ws, cleanup;
let stopping = false;
async function post(path) {
  const r = await fetch(path, { method: "POST" });
  if (!r.ok) throw new Error(await r.text());
  return path === "/session" ? r.json() : null;
}
async function control(s) {
  const url = new URL(s.control.url);
  url.searchParams.set("token", s.control.client_secret);
  ws = new WebSocket(url);
  await new Promise((resolve, reject) => {
    const timer = setTimeout(() => { ws.close(); reject(new Error("Control timeout")); }, 15000);
    ws.onopen = () => { clearTimeout(timer); resolve(); };
    ws.onerror = ws.onclose = () => { clearTimeout(timer); reject(new Error("Control failed")); };
  });
  ws.onclose = () => { if (!stopping) status.textContent = "Control disconnected. Stop before starting again."; };
  ws.onerror = () => { if (!stopping) status.textContent = "Control error. Click Stop to release the session."; };
}
stop.onclick = async () => {
  stop.disabled = true;
  try { await post("/close"); stopping = true; ws?.close(); await cleanup?.(); cleanup = undefined; delete window.player; status.textContent = "Stopped"; start.disabled = false; }
  catch (e) { status.textContent = `Close failed: ${e.message}. Retry Stop.`; }
  finally { stop.disabled = false; }
};
start.onclick = async () => {
  stopping = false;
  start.disabled = true;
  stop.disabled = true;
  status.textContent = "Starting…";
  try {
    const s = await post("/session");
    status.textContent = "Connected; waiting for video";
    await connect(s);
  } catch (e) {
    status.textContent = `${e.message}. Click Stop to release the session.`;
  } finally { stop.disabled = false; }
};
```

## 2. Build and start

```bash theme={null}
npx esbuild main.js --bundle --outfile=app.js
export VIVIX_API_KEY="YOUR_API_KEY"
```

If you have already created an Agora session, save its complete response as `session-response.json` and run the command below. Start joins that session. The Quickstart creates a TRTC session by default; its response cannot be used with this player.

```bash theme={null}
SESSION_RESPONSE_FILE=./session-response.json node server.mjs
```

To let the player create a new session from `session.json` instead, run:

```bash theme={null}
node server.mjs
```

Choose one mode. Open [http://localhost:3000](http://localhost:3000) and click Start. Video plus the “Video playing” message confirms playback. Stop calls the server close endpoint and disconnects locally. A `closed` imported session cannot be started again; create one and save its new response.

## 3. FAQ

**Creation succeeded. Why is there no video?**

Check that the session is still running and its returned transport matches this player. TRTC and Agora credentials are not interchangeable. Joining the room is not a first-frame signal; the example shows Video playing only after remote video starts rendering. Check SDK errors and the publisher\_user\_id subscription filter.

**Why is video visible but audio silent?**

The browser may block autoplay. Click Enable sound when it appears; you do not need a new session. This example plays remote media without opening the microphone.

**What should I do when Stop fails?**

Retry Stop if the request fails. A closing response means the closure request was accepted; retrieve the Session if you need to confirm its final state.

Continue with [conversation](/streaming-avatar/interaction/conversation) or [external audio](/streaming-avatar/integrate/pure-rendering). See [session management](/streaming-avatar/integrate/sessions) for server-side cleanup policies.

### Closing the page

Users can close the page directly; the browser disconnects media and control. To end the server session automatically afterward, add `"auto_close": {"disconnected_timeout_seconds": 60}` at the top level of `session.json`. Closure starts after all control connections have been disconnected for 60 seconds. Use Stop when you want to end immediately. See [session management](/streaming-avatar/integrate/sessions) for duration rules.
