> ## 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# 快速开始

> Windows SRTC C# 封装（srtcNet）的 10 分钟最小可跑流程：初始化 SrtcEngine、订阅事件后加入 SrtcChannel、用强类型轨道发布本地音视频、显式订阅并渲染远端画面、屏幕共享与退出。

`srtcNet` 的调用顺序和 C++ 一致，只是换成了惯用的 C# 写法：事件是 `event`，
轨道返回强类型对象，频道/引擎实现 `IDisposable`。先分清楚两层对象：

* `SrtcEngine` —— 进程级，一个就够。创建/退出频道、设备枚举、IM、日志上传
* `SrtcChannel` —— 一个频道一个对象，成员查询、轨道、推拉流、本地录制都在它上面

### 初始化 SDK

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

SrtcEngine.SetLanguage("zh-CN");                       // 可选：错误文案语言
using var engine = SrtcEngine.Init(new SrtcEngineOptions { EnableLog = 1 });
if (engine == null)
{
    return;      // 初始化失败（环境/权限问题）
}
Console.WriteLine(SrtcEngine.Version());               // 例：0.2.1-alpha.14
```

`EnableLog = 0`（或传 `null`）会关掉 SDK 文件日志。初始化失败时 `Init` 返回 `null`，
可配合 `engine.LastErrorMessage` 排查。

### 注册引擎级事件

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

这里只有**不属于任何频道**的回调：设备变化、默认设备变化、网络探测、IM。
其余回调都在 `SrtcChannel` 上。

### 加入频道

分两步：`CreateChannel` 只创建对象**不入会**，配置好之后再 `Join()`。
和 C++ 一样，**事件必须在 `Join()` 之前订阅** —— `Joined` 是在 `Join()` 还没返回时同步触发的。

```csharp theme={null}
var channel = engine.CreateChannel(token);             // token 由你的后端签发
if (channel == null)
{
    return;      // token 无效 / 解码失败
}

// 事件必须在 Join 之前接好，否则会漏掉加入瞬间的事件
channel.Joined += (s, e) =>
{
    // e.ChannelJson / MeJson / MembersJson / OptionsJson 均为原始 JSON
    Console.WriteLine("已加入 " + channel.ChannelId);
};
channel.UserJoined    += (s, e) => Console.WriteLine("用户加入 " + e.UserJson);
channel.UserStreamAdded += (s, e) => Console.WriteLine($"{e.Uid} 发布流 {e.StreamJson}");
channel.CustomMessage += (s, e) => Console.WriteLine($"[{e.Action}] {e.SenderId}: {e.Message}");

// 频道级配置（stream_model / simple / mcu_track / enable_audio_record 在 Join 阶段读取）
channel.Setting.StreamModel = (int)SrtcStreamModel.Normal;

SrtcStatusCode st = channel.Join();
if (st != SrtcStatusCode.Ok)
{
    Console.WriteLine("join 失败 " + st + " " + engine.LastErrorMessage);
    return;
}
```

> [!NOTE]
>
> `UserStreamAdded` **只覆盖加入之后新发布的轨道**。比自己先进频道的人，要在 `Joined` 的
> `MembersJson` 里遍历其 `stream_tracks` 主动补订阅。

### 打开摄像头并发布

```csharp theme={null}
var cam = channel.GetCameraTrack("camera");
if (cam == null) return;

SrtcStatusCode st = cam.StartCapture(new SrtcCameraCaptureOptions
{
    Width = 640, Height = -1, MaxFps = -1
});
if (st != SrtcStatusCode.Ok) return;

st = channel.Publish(cam, new SrtcVideoPublishOptions
{
    Desc = "camera_big", MaxFps = 25,
    Simucast = new[] { new SrtcVideoPublishOptions { Desc = "camera_small", Width = 320, Height = 180, MaxFps = 15, MaxBitrate = 128 } }
});
```

> [!WARNING]
>
> 本地轨道**先 `StartCapture` 再 `Publish`**，顺序不能反。`Publish` 有视频/音频两个重载，
> 要发布音频请传 `SrtcAudioPublishOptions`。

### 打开麦克风并发布

```csharp theme={null}
var mic = channel.GetAudioTrack("mic");
mic.StartCapture(new SrtcMicCaptureOptions());          // 默认 48kHz 单声道
channel.Publish(mic, new SrtcAudioPublishOptions { Desc = "mic", Codec = (int)SrtcStreamCodec.Opus });
```

### 播放频道混音（听到所有人）

远端音频用**空 uid、空 trackId** 取频道级混音，`StartPlay` 之后即可听到所有人，无需单独订阅：

```csharp theme={null}
var speaker = channel.GetRemoteAudioTrack("", "");
speaker.StartPlay(new SrtcAudioOutputOptions());         // 传 null 表示用默认扬声器
```

### 播放远端视频

```csharp theme={null}
var remote = channel.GetRemoteVideoTrack(uid, trackId);  // uid / trackId 来自流事件
if (remote == null) return;

SrtcStatusCode st = remote.AddPlayView(new SrtcHwndView(hwnd));   // hwnd 为渲染控件的窗口句柄
if (st == SrtcStatusCode.Ok)
{
    channel.Subscribe(remote);                           // 订阅是显式的，必须自己调
}

// 取消时：先摘视图，再退订
remote.RemovePlayView(view);
channel.Unsubscribe(remote);
```

> [!NOTE]
>
> 窗口渲染用现成的 `SrtcHwndView(句柄)`；要渲染到自己的表面就从 `SrtcView` 派生并重写
> `MakeCurrent`（`GetViewType` 返回 `SrtcViewType.CallBack`）。`SrtcView` 析构前**必须先从所有轨道
> `RemovePlayView`**，否则渲染线程可能访问已释放对象。

### 共享屏幕

```csharp theme={null}
var screen = channel.GetScreenTrack("screen");
screen.AddScreenAudio(true);                             // 是否带系统声音

var cap = new SrtcScreenCaptureOptions { MaxFps = 10, ShowCursor = true };
cap.DeviceId = windowHwnd;                               // 窗口共享：句柄；显示器共享：填 X/Y/Width/Height
// cap.X = x; cap.Y = y; cap.Width = w; cap.Height = h;

screen.StartCapture(cap);
channel.Publish(screen, new SrtcVideoPublishOptions { Desc = "screen", Width = 1920, Height = 1080, MaxFps = 10, MaxBitrate = 2000 });
```

屏幕枚举 `EnumScreen()` 里 `Type == 5` 是显示器、`Type == 0` 是窗口。窗口共享把 `DeviceId`
设为 `Hwnd`；显示器共享设置 `X/Y/Width/Height` 矩形。

### 退出频道

```csharp theme={null}
channel.Dispose();          // 若还想复用 engine，只销毁频道
engine.Dispose();           // 或释放引擎，连带所有频道
```

> [!WARNING]
>
> `SrtcChannel.Dispose()` 之后它交出的**所有轨道引用立即作废**（远端/自定义轨道在原生侧就被删了）。
> 退出时先把视图从轨道上摘掉（`RemovePlayView`），再销毁频道，最后释放视图对象。

### 订阅与线程

* 除 `Joined`（在 `Join()` 内、调用线程上同步触发）外，**其余事件都来自 SDK 内部线程**，
  界面更新请自行 marshal（WinForms 用 `BeginInvoke`）到 UI 线程
* `SrtcEngine.Init` 内部会让原生在 STA 线程初始化，因此从普通 .NET 线程（默认 MTA）调用也能成功
* 常用强类型事件参数：`SrtcUserStreamEventArgs`（`Uid` / `StreamJson`）、
  `SrtcJoinedEventArgs`（`ChannelJson` / `MeJson` / `MembersJson` / `OptionsJson`）、
  `SrtcCustomMessageEventArgs`（`Action` / `SenderId` / `Name` / `Message`）

更完整的可运行示例见 WinForms Demo（九宫格布局，含设备选择、翻页、IM）。


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