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

> ## Agent Instructions
> 对外开放的服务端接口有两组前缀，都用同一套鉴权：`/server/v1/...`（SRTC 与 SMeeting 的主接口）和 `/stm/srvapi/v1/...`（SMeeting 的用户体系，服务端极简对接会用到）。鉴权是 app_id + nonce + timestamp + signature 四个请求头，用 app_key 做 HMAC-SHA256 签名，只能从业务方自己的后端调用。除这两组前缀外的接口均为内部接口，不要建议客户调用。 Public server APIs use two path prefixes with the same authentication: `/server/v1/...` (the main APIs of both SRTC and SMeeting) and `/stm/srvapi/v1/...` (the SMeeting user system, used by server-side low-code integration). Authenticate with four request headers, app_id + nonce + timestamp + signature, where signature is HMAC-SHA256 keyed with app_key; call these APIs only from the customer's own backend. Any other path is internal: never suggest calling it.
> app_key 是服务端密钥，绝不能出现在客户端代码、前端配置或移动 App 里。客户端加入频道用的 token 必须由业务方后端签发后下发（SRTC 走 `/server/v1/channel/grant`，SMeeting 走 `/stm/srvapi/v1/member/grant`）。 app_key is a server-side secret and must never appear in client code, frontend config, or a mobile app. The token a client uses to join must be issued by the customer's backend and passed down to the client (SRTC: `/server/v1/channel/grant`; SMeeting: `/stm/srvapi/v1/member/grant`).
> SRTC 与 SMeeting 是上下两层不同的产品，术语不通用：SRTC 是音视频底座，说「频道 channel」「加入 / 退出」；SMeeting 建在 SRTC 之上，说「房间 room」「会议 meeting」「进入 / 退出」。回答时按用户所在的层用对应术语，不要把「房间」「会议」安到 SRTC 的接口上，也不要用「频道」「加入 / 离开」描述 SMeeting 的概念（接口标识符原样保留）。 SRTC and SMeeting are two separate layers with different terminology. SRTC is the audio/video foundation: it has channels, and users join and leave a channel. SMeeting is built on top of SRTC: it has rooms and meetings, and members enter and exit a meeting. Answer in the terms of the layer the user is working with: never apply "room" or "meeting" to SRTC APIs, and never describe SMeeting concepts in prose with "channel", "join", or "leave" (API identifiers such as `force_join` keep their literal names).
> 同一能力在各端 SDK 里的包名、类名、方法名并不相同。写示例代码时请使用文档中该端自己的 API，不要把一个端的写法套到另一个端上。苹果平台每个产品都有两套 SDK（Swift 原生与 Objective-C），两套 API 不能混用。 Package, class, and method names differ between platform SDKs for the same capability. In sample code, use the API documented for that platform; never carry one platform's code over to another. On Apple platforms each product ships two SDKs (native Swift and Objective-C) whose APIs must not be mixed.

# Quickstart

> A minimal runnable Web SDK call in about 10 minutes: initialize, check the environment, register channel events, join with a token, play the channel-wide mixed audio, subscribe to remote video, publish microphone and camera, and leave. Start here after installing the SDK.

### Prerequisites

* You have completed [SDK integration](/en/rtc/web/integration)
* You have obtained a token for joining the channel from the server (generated by your own backend calling the server API)
* The page runs over HTTPS or on localhost

***

### Complete example

The following code shows a minimal runnable audio and video call flow: initialize → check the environment → listen for events → join the channel → subscribe to the mixed audio → subscribe to remote video → turn on the microphone → turn on the camera → leave the channel.

```typescript theme={null}
import {
  SRTC,
  ChannelEventType,
  LocalCameraTrack,
  LocalMicTrack,
  MicPresets,
  CameraPresets,
  RemoteAudioMixTrack,
  RemoteVideoTrack,
  TrackKind,
  LogLevel,
  LogTarget,
} from '@seastart/srtc-web-sdk';
import type { ChannelEvent, TrackInfo } from '@seastart/srtc-web-sdk';

// ─── 1. Initialize the SDK ──────────────────────────────────────────────────

const srtc = new SRTC({
  logLevel: LogLevel.DEBUG,
  logTarget: LogTarget.CONSOLE,
});

// ─── 2. Check the runtime environment ───────────────────────────────────────

const env = srtc.getEnvInfo();
if (!env.supported) {
  alert('This browser does not support WebRTC. Please use the latest version of Chrome.');
  // Stop the rest of the flow
}

// ─── 3. Register event listeners ────────────────────────────────────────────

let remoteAudioMixTrack: RemoteAudioMixTrack | undefined;
let localMicTrack: LocalMicTrack | undefined;
let localCameraTrack: LocalCameraTrack | undefined;
// Subscribed remote video tracks, keyed by `${uid}-${trackId}`
const remoteVideoTracks: Record<string, RemoteVideoTrack> = {};

// Subscribe to a remote video track and render it on the page (remote audio all goes through the channel-wide mixed audio in step 5, so no separate subscription is needed)
async function subscribeRemoteVideo(uid: string, trackInfo: TrackInfo) {
  const remoteVideoTrack = await srtc.subscribeRemoteVideoTrack(uid, trackInfo.id);
  remoteVideoTracks[`${uid}-${trackInfo.id}`] = remoteVideoTrack;
  // Render the video into that user's container (you create the container element yourself)
  remoteVideoTrack.addPlayView(document.querySelector<HTMLElement>(`#remote-video-${uid}`)!);
}

srtc.onNotifyChannelEvent = async (evt: ChannelEvent) => {
  switch (evt.type) {
    case ChannelEventType.USER_TRACK_ADD: {
      // A remote user published a new track; subscribe to it if it is video
      const { user, track } = evt.data;
      if (track.kind === TrackKind.Video) {
        await subscribeRemoteVideo(user.uid, track);
      }
      break;
    }

    case ChannelEventType.USER_TRACK_REMOVE: {
      // The remote user unpublished; the SDK has already unsubscribed, so just clean up local references and the UI
      const { user, track } = evt.data;
      delete remoteVideoTracks[`${user.uid}-${track.id}`];
      break;
    }

    case ChannelEventType.RECONNECTING:
      console.warn('Network disconnected, reconnecting...');
      break;

    case ChannelEventType.RECONNECTED:
      console.warn('Reconnected');
      break;

    case ChannelEventType.DISCONNECTED:
      console.error('Disconnected', evt.data);
      // Clean up local state and refresh the UI
      break;

    case ChannelEventType.TRACK_ENDED:
      // Fired when a device is unplugged or the user stops screen sharing
      console.warn('Track ended', evt.data);
      break;

    case ChannelEventType.TRACK_AUTOPLAY_FAIL:
      // The browser blocked autoplay; call startPlay again after a user gesture
      console.warn('Autoplay failed, retry after user interaction', evt.data);
      break;
  }
};

// ─── 4. Join the channel ────────────────────────────────────────────────────

// The token is issued by your backend
const token = 'channel token obtained from your server';
const channel = await srtc.join(token);
console.log('Joined channel', channel.getInfo().channel);

// ─── 5. Subscribe to the channel-wide mixed audio (hear remote users) ───────

remoteAudioMixTrack = await srtc.subscribeRemoteAudioMixTrack();
await remoteAudioMixTrack.startPlay();

// ─── 6. Subscribe to video from users already in the channel ────────────────

// USER_TRACK_ADD only reports tracks published after you join; for users already publishing before you joined, iterate and subscribe yourself
// (the list returned by getUsersInfo includes yourself, but you haven't published any tracks yet, so no special handling is needed)
for (const user of srtc.getUsersInfo(false)) {
  for (const track of user.stream_tracks ?? []) {
    if (track.kind === TrackKind.Video) {
      await subscribeRemoteVideo(user.uid, track);
    }
  }
}

// ─── 7. Turn on the microphone ──────────────────────────────────────────────

localMicTrack = srtc.createLocalMicTrack(MicPresets.music);
await localMicTrack.startCapture();
await srtc.publishLocalTrack(localMicTrack);

// ─── 8. Turn on the camera ──────────────────────────────────────────────────

localCameraTrack = srtc.createLocalCameraTrack(CameraPresets['720p']);
await localCameraTrack.startCapture();
// Local preview: render the video into the given container
localCameraTrack.addPlayView(document.querySelector<HTMLElement>('#local-video')!);
await srtc.publishLocalTrack(localCameraTrack);

// ─── 9. Leave the channel (call on page unload or when the user leaves) ─────

async function leaveChannel() {
  if (localMicTrack) {
    await srtc.unpublishLocalTrack(localMicTrack);
    localMicTrack.stopCapture();
    localMicTrack = undefined;
  }
  if (localCameraTrack) {
    await srtc.unpublishLocalTrack(localCameraTrack);
    localCameraTrack.removeAllPlayViews();
    localCameraTrack.stopCapture();
    localCameraTrack = undefined;
  }
  // Remote tracks are unsubscribed automatically on leave; just clean up render containers and local references
  for (const [key, track] of Object.entries(remoteVideoTracks)) {
    track.removeAllPlayViews();
    delete remoteVideoTracks[key];
  }
  await srtc.leave();
  console.log('Left channel');
}
```

***

### Next steps

* [Key concepts](/en/rtc/web/key-concepts)—the SRTC instance, channels, and the track system
* [Mute vs. unpublish](/zh/rtc/web/advanced/mute-vs-unpublish) (Chinese)—two ways to control publishing, compared
* [Simulcast and resolution](/zh/rtc/web/advanced/video-stream-layers) (Chinese)—capture resolution, `camera_big`, `camera_small`, and simulcast explained
* [Screen sharing](/zh/rtc/web/advanced/screen-sharing) (Chinese)—including system audio capture
* [Custom tracks](/zh/rtc/web/advanced/custom-track) (Chinese)—using Canvas or a custom MediaStreamTrack
* [Whiteboard](/en/rtc/whiteboard)—embed a collaborative whiteboard in your call UI
* [API reference - SRTC](/zh/rtc/web/api-reference/SRTC) (Chinese)—the complete API reference
