> ## 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.

# C# Quickstart

> The minimal Windows SMeeting C# flow with the SMeetingNet wrapper in about 10 minutes: initialize SMeetingEngine, log in, create and enter a meeting, control the camera and microphone, play the channel audio mix, render remote video, share the screen, chat, and leave.

`SMeetingNet` wraps the `SMeeting.h` C++ API in idiomatic C#: callbacks become `event`s, media becomes
strongly typed objects, and `SMeetingEngine` / `SMeetingChannel` implement `IDisposable`, so you never
write callback vtables yourself.

The call order is identical to the [C++ quickstart](https://docs.stmlink.com/en/meeting/windows/quickstart.md). There are two layers:

* `SMeetingEngine`—process-wide, one is enough. Login, the meeting management HTTP APIs, channel lifecycle, device enumeration, IM, resource drive
* `SMeetingChannel`—one per meeting: entering, room and member data, in-meeting operations, media objects

### Initialize the SDK

```csharp theme={null}
using SMeetingNet;

SMeetingEngine.SetLanguage("en");                 // optional: language of error texts (BCP 47)
var engine = SMeetingEngine.Init();
if (engine == null)
{
    return;      // initialization failed
}
Console.WriteLine(SMeetingEngine.Version());      // e.g. 1.0.0-alpha.9

// Engine-level settings must be in place before Login — the rtc engine is built inside Login
engine.Setting.EnableStreamLog = 1;
engine.Setting.SdkLogPath = @"D:\log\";
```

`SetLanguage` / `Version` / `GetStatusMessage` are static—no engine instance needed.
Use `SMeetingEngine.GetStatusMessage(code)` for the text of a status code; its language follows
`SetLanguage` (English until you call it).

### Register engine-level events

```csharp theme={null}
engine.DeviceChanged += (s, e) => Console.WriteLine($"{e.Type} add={e.IsAdd} {e.Name}");
```

Only callbacks that **belong to no meeting** live here: device changes, default device changes, network
probe, IM, meeting-start reminder (`MeetingStartMessage`). Everything else is on `SMeetingChannel`.

### Log in

```csharp theme={null}
SMeetingStatusCode st = engine.Login(token, (code, data) =>
{
    if (code == SMeetingStatusCode.Ok)
    {
        // data is the json returned by your backend
    }
});
```

`Login` is asynchronous—the result only arrives through the callback.

### Create a meeting

```csharp theme={null}
var model = new SMeetingCreateMeetingModel
{
    Title = "My meeting",
    Content = "Meeting description",
    MeetingType = 1,   // 1 instant, 2 scheduled
    MeetingMode = 1,   // 1 normal
};

engine.CreateRoom(model, (code, data) =>
{
    if (code == SMeetingStatusCode.Ok)
    {
        // data is the meeting number
    }
});
```

If you already have a meeting id or room number, skip straight to the next step.

### Enter a meeting

Two steps: `CreateChannel` only creates the object **without entering**; configure it, then call `Enter()`.
As in C++, **register the events before `Enter()`**—the entering callbacks fire synchronously while
`enter` is still running.

```csharp theme={null}
SMeetingChannel channel = engine.CreateChannel(roomNo);   // or CreateChannelByMeetingId(meetingId)
if (channel == null)
{
    return;      // invalid roomNo
}

// Events must be wired before Enter
channel.Disconnected           += (s, e) => Console.WriteLine($"disconnected {e.Reason} {e.Code} {e.Message}");
channel.Reconnected            += (s, e) => Console.WriteLine("reconnected");
channel.Reconnecting           += (s, e) => Console.WriteLine("reconnecting");
channel.UserEntered            += (s, e) => Console.WriteLine("user entered " + e.UserData);   // member json
channel.UserExited             += (s, e) => Console.WriteLine("user left " + e.UserData);
channel.UserCameraStateChanged += (s, e) => Console.WriteLine($"{e.Uid} camera {e.NewState}");

// Channel settings: stream_model / mcu_track / enable_audio_record are read while entering only
SMeetingChannelSetting setting = channel.Setting;
setting.StreamModel = 1;             // stream media mode
setting.SpeakerInterval = 500;       // audio-level callback period (ms)
setting.StatInterval = 10000;        // network-stat callback period (ms)
setting.RoomName = "Alice";          // display name in the meeting
setting.StreamVendor = "seastart";   // vendor string expected by the backend

st = channel.Enter(pass, (code, data) =>
{
    if (code != SMeetingStatusCode.Ok)
    {
        engine.LeaveChannel(channel);   // recycle the channel object even when entering failed
        return;
    }
    Console.WriteLine("entered " + channel.ChannelId);
});
```

> [!WARNING]
>
> Set both `RoomName` (the display name) and `StreamVendor` **before** `Enter()`.
> When entering fails the `channel` object still exists and must be recycled with
> `engine.LeaveChannel(channel)`.

### Get room and member data

```csharp theme={null}
string me      = channel.GetMeJson();          // local member json
string room    = channel.GetRoomJson();        // room json
string members = channel.GetMembersJson();     // member json array
string member  = channel.GetMemberJson(uid);   // one member json
```

> [!NOTE]
>
> These return **raw json** whose schema belongs to the backend—parse it yourself. Each member's published
> tracks live in its `stream_tracks` field (`desc` / `kind` / `width` / `height` / `fps`).

### Camera

```csharp theme={null}
SMeetingLocalCamera camera = channel.GetLocalCamera();
if (camera == null) return;

camera.AddPlayView(new SMeetingHwndView(hwnd));        // hwnd is the render control's window handle
camera.SwitchCamera("Integrated Camera", 640, 480);    // optional: pick device and resolution
camera.RequestOpenCamera((code, data) => { /* result */ });
```

```csharp theme={null}
camera.RemoveAllPlayView();
camera.CloseCamera((code, data) => { /* result */ });
```

### Microphone

```csharp theme={null}
SMeetingLocalMic mic = channel.GetLocalMic();
if (mic == null) return;

mic.SwitchMic("Default Microphone");    // optional; pass "default" for the system default device
mic.RequestOpenMic((code, data) => { /* result */ });
```

```csharp theme={null}
mic.CloseMic((code, data) => { /* result */ });
```

### Speaker (hear everyone)

Remote audio fetched with an **empty uid** is the channel mix, so opening it is enough to hear everyone—
no per-member subscription:

```csharp theme={null}
SMeetingRemoteAudio speaker = channel.GetRemoteAudio(string.Empty);
if (speaker == null) return;

speaker.SwitchSpeaker("Default Speakers");     // optional; pass "default" for the system default device
speaker.OpenSpeaker();
```

```csharp theme={null}
speaker.CloseSpeaker();
```

### Play remote video

```csharp theme={null}
// uid / desc come from the member json stream_tracks: camera_big / camera_small for the camera, screen for sharing
SMeetingRemoteVideo remote = channel.GetRemoteVideo(uid, "camera_big");
if (remote == null) return;

remote.AddPlayView(new SMeetingHwndView(hwnd));
SMeetingStatusCode rst = remote.LoadRemoteVideo();      // subscribe and pull
```

```csharp theme={null}
remote.RemovePlayView(view);
remote.UnloadRemoteVideo();
```

> [!WARNING]
>
> A remote track is **not** a long-lived handle: the SDK destroys it once the member leaves, and the same
> `(uid, desc)` then resolves to `null`. Don't cache `SMeetingRemoteVideo`—re-fetch it through
> `GetRemoteVideo` on every use. Local camera / screen objects are the opposite: they stay valid for the
> lifetime of the channel.

> [!NOTE]
>
> For window rendering just use the ready-made `SMeetingHwndView(handle)`; to render into your own surface
> derive from `SMeetingView` and override `UpdatePlanes` (`GetViewType` returns `SMeetingViewType.CallBack`).
> Always `RemovePlayView` before disposing a `SMeetingView`, or the render thread may touch a freed object.

### Share the screen

```csharp theme={null}
SMeetingLocalScreen screen = channel.GetLocalScreen();
if (screen == null) return;

screen.AddScreenAudio(true);                        // whether to include system sound

// tp: 1 screen share, 2 window share
// data: "x,y,width,height" for a monitor, "hwnd:<handle>" for a window
screen.RequestShare(1, "0,0,1920,1080", (code, data) => { /* result */ });
```

```csharp theme={null}
screen.StopShare((code, data) => { /* result */ });
```

Enumerate share targets with `engine.EnumScreen()`: `Type == 5` is a monitor (build the rectangle from
`X` / `Y` / `Width` / `Height`), `Type == 0` is a window (use `Hwnd`).

### Chat and custom messages

```csharp theme={null}
// public chat: tp 1 text, 2 file, 3 image, 4 sound
channel.SendRoomChatMessage(1, "Hello everyone!", (code, data) => { });

// point-to-point custom message: note the order — target id first, content second
channel.SendRoomCustomMessage(targetUid, "{\"cmd\":1}", (code, data) => { });
```

Incoming messages arrive in the `RoomChatMessage` / `RoomCustomMessage` events.

### Host operations

```csharp theme={null}
channel.AdminRequestUserOpenCamera(uid, (code, data) => { });
channel.AdminCloseUserCamera(uid, (code, data) => { });
channel.AdminRequestUserOpenMic(uid, (code, data) => { });
channel.AdminCloseUserMic(uid, (code, data) => { });
channel.AdminKickUserOut(uid, (code, data) => { });
channel.AdminDestroyRoom((code, data) => { });          // end (dissolve) the meeting
```

### Local recording

```csharp theme={null}
SMeetingRecord record = channel.GetLocalRecord();   // one recorder per channel
if (record == null) return;

record.SetRecordFileName(@"C:\record\meeting.mp4");
record.SetWaterMask("Meeting recording");
record.SetRecordHwnd(hwnd, 0, 0, 0, 0);
record.Start();

// record.Pause();
// record.Stop();
```

### Leave and release

```csharp theme={null}
engine.LeaveChannel(channel);   // destroys the channel; its media objects become invalid at once
channel = null;                 // the object is gone, clear the reference yourself

// or leave every channel at once
engine.LeaveAllChannels();

engine.Dispose();               // release the engine
```

### Threading and lifetime

* Except for the events raised while entering, **all events arrive on SDK internal threads**—marshal to
  your UI thread yourself (WinForms: `BeginInvoke`)
* Async APIs taking an `SMeetingCallback` (`Login` / `Enter` / `RequestOpenCamera`, …) invoke the callback
  **once**, with `(SMeetingStatusCode code, string data)`
* Every media object is a **handle borrowed from its channel**: valid only while that channel is alive,
  and never to be used after leaving
* `SMeetingNet.dll` is a C++/CLI mixed-mode assembly, so its bitness must match `bin` and your project's
  target platform—see [Integration](https://docs.stmlink.com/en/meeting/windows/integration.md)

A fuller runnable example is the WinForms demo (9-grid layout, with device pickers, paging, sharing and chat).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.