# EZOpenSDK iOS 视频通话（ERTC）

## 一、概述

视频通话功能基于 ERTC SDK 实现，用于萤石屏类设备（如 S10）的远程视频通话。

注意事项：
- EZOpenSDK v5.21 起支持视频通话，已封装 ERTC SDK
- 如需同时实现 IPC 取流和视频会议，使用 v5.21+ 即可，无需单独集成 ERTC SDK
- v5.21 以下版本与 ERTC SDK 一起集成可能有编译/运行问题

## 二、头文件导入

动态库（通过 CocoaPods 集成）导入方式：
```objc
#import <EZOpenSDKFramework/EZOpenSDKFramework.h>  // EZOpenSDK 主 API
#import <EZOpenSDKFramework/ERTC.h>                // ERTC 引擎相关类
```

> ⚠️ 注意：`EZOpenSDKFramework.h` 这个 umbrella header 中**不包含** ERTC 相关头文件，
> 必须单独 `#import <EZOpenSDKFramework/ERTC.h>` 才能使用 `ERTCEngine`、`ERTCView`、`ERTCParam` 等类。

静态库（源码集成）导入方式：
```objc
#import "ERTC.h"
```

头文件中如果只需要类型声明（属性定义等），使用前向声明避免编译依赖：
```objc
@class ERTCEngine;
@class ERTCView;
```

## 三、名词解释

| 名词 | 说明 |
|------|------|
| 项目 ID（AppId） | ERTC 的 AppId（不是 AppKey），在萤石开放平台控制台创建 |
| 客户端 token（ertcClientToken） | 客户端入会权限校验 token，由服务端生成 |
| 设备端 token（ertcDeviceToken） | 邀请设备入会用的 token，由服务端生成 |
| 房间 ID（roomId） | 视频通话房间号，由服务端生成 |
| 用户 ID（userId） | 视频通话用户 ID，由服务端生成 |
| apiUrl | 萤石开放平台地址，必须配置，如 `https://open.ys7.com` |

## 四、前置条件 - 能力集判断

视频通话功能需要根据设备能力集判断是否支持：

```objc
// EZDeviceInfo 的 isSupportVideoMeeting 属性（能力集第 818 位）
// 同时设备需要在线（status == 1）
BOOL supportVideoTalk = (deviceInfo.isSupportVideoMeeting && deviceInfo.status == 1);
```

## 五、ERTC 引擎初始化

> ⚠️ `ERTCEngineConfig.apiUrl` 必须设置，否则会返回 201002 错误（appKey 无效）。

```objc
// 打开 ERTC 日志（可选，调试时建议开启）
[[ERTCLog sharedInstance] enableFileLog];
[[ERTCLog sharedInstance] enableConsoleLog];

ERTCEngineConfig *config = [[ERTCEngineConfig alloc] init];
config.appID = appId;
config.apiUrl = @"https://open.ys7.com"; // 必须设置

[ERTCEngine createWithConfig:config instanceBlock:^(ERTCEngine *instance, NSError *error) {
    if (!error) {
        self.ertcEngine = instance;
    }
}];
```

## 六、客户端进入房间

> ⚠️ `ERTCParam.apiUrl` 也必须设置，否则进入房间会失败。

### ERTCView 初始化

`ERTCView` 是 `UIView` 的子类，用于本地预览和远端视频渲染。直接 `alloc init` 或 `initWithFrame:` 创建即可：

```objc
// 本地预览视图
ERTCView *localPlayView = [[ERTCView alloc] init];
localPlayView.backgroundColor = [UIColor blackColor];
localPlayView.fillMode = ERTCVideoFillMode_Fill; // 本地画面建议使用填充模式

// 远端视频视图
ERTCView *remotePlayView = [[ERTCView alloc] init];
remotePlayView.backgroundColor = [UIColor blackColor];
remotePlayView.fillMode = ERTCVideoFillMode_Fit; // 远端画面建议使用适应模式
```

fillMode 可选值：
- `ERTCVideoFillMode_Fill` — 填充模式，画面可能被裁剪（适合本地预览）
- `ERTCVideoFillMode_Fit` — 适应模式，画面完整显示可能有黑边（适合远端画面）

### 进入房间

```objc
// 设置视频参数
ERTCVideoEncParam *videoParam = [[ERTCVideoEncParam alloc] init];
videoParam.videoResolution = ERTCVideoResolution_1280_720;
videoParam.videoFps = ERTCVideoFPS_10;
videoParam.videoBitrate = 500 * 1024;
videoParam.resMode = ERTCVideoResolutionModePortrait;
[self.ertcEngine setVideoEncoderParam:videoParam];

// 开关本地视频
[self.ertcEngine enableLocalVideo:YES];

// 开启本地音频推流（必须主动调用，否则设备端听不到手机端声音）
[self.ertcEngine enableLocalAudio:YES];

// 音频编码类型（必须，S10 使用 AAC）
[self.ertcEngine setAudioEncoderType:ERTCAudioEncodeType_AAC];

// 设置填充模式及本地预览
self.localPlayView.fillMode = ERTCVideoFillMode_Fill;
[self.ertcEngine setLocalPreview:self.localPlayView withRegionID:0];

// 设置事件代理
self.ertcEngine.delegate = self;

// 进入房间（apiUrl 必须设置）
ERTCParam *param = [[ERTCParam alloc] init];
param.roomId = roomId;
param.userId = userId;
param.token = ertcClientToken;
param.appID = appId;
param.apiUrl = @"https://open.ys7.com"; // 必须设置
[self.ertcEngine enterRoom:param withScene:ERTCAppScene_VideoCall];
```

## 七、邀请设备入会

客户端发起视频通话时，**进入房间成功后**（在 `onEnterRoomSuccess` 回调中）邀请设备：

```objc
// 在 onEnterRoomSuccess 回调中调用
[EZOpenSDK inviteDeviceEnterMeeting:appId ertcToken:ertcDeviceToken roomId:roomId deviceSerial:deviceSerial cameraNo:cameraNo account:userId completion:^(NSError *error) {
    if (error) {
        // 邀请失败
    }
}];
```

### 取消邀请
```objc
[EZOpenSDK cancelInviteDeviceEnterMeeting:appId ertcToken:ertcDeviceToken roomId:roomId deviceSerial:deviceSerial cameraNo:cameraNo account:userId completion:^(NSError *error) {
}];
```

### 拒绝设备呼叫
```objc
[EZOpenSDK rejectVideoCallReqFromDevice:appId ertcToken:ertcToken roomId:roomId deviceSerial:deviceSerial cameraNo:cameraNo account:userId completion:^(NSError *error) {
}];
```

## 八、订阅设备端码流

```objc
#pragma mark - ERTCDelegate

- (void)onEnterRoomSuccess:(NSInteger)result {
    if (self.isClientCallDevice) {
        [self inviteDeviceEnterMeeting];
    }
}

- (void)onRemoteUserEnterRoom:(NSString *)userId {
    self.isRemoteUserEnterRoom = YES;
}

- (void)onRemoteUserLeaveRoom:(NSString *)userId reason:(NSInteger)reason {
    [self hangUp];
}

/// ⚠️ 注意：该回调可能有重复消息，需要用字典记录状态做过滤
- (void)onUserVideoAvailable:(NSString *)userId available:(BOOL)available {
    // 过滤重复回调
    BOOL previousValue = [[self.remoteUserDict objectForKey:userId] boolValue];
    if (previousValue == available && [self.remoteUserDict objectForKey:userId]) {
        return;
    }
    [self.remoteUserDict setValue:@(available) forKey:userId];
    
    if (available) {
        self.remotePlayView.fillMode = ERTCVideoFillMode_Fit;
        [self.ertcEngine subscribe:YES forUser:userId withStream:ERTCVideoStreamTypeBig];
        [self.ertcEngine setRemoteView:self.remotePlayView forUser:userId withRegionID:0];
    } else {
        [self.ertcEngine subscribe:NO forUser:userId withStream:ERTCVideoStreamTypeBig];
        [self.ertcEngine setRemoteView:nil forUser:userId withRegionID:0];
    }
}

- (void)onError:(NSInteger)errCode {
    // ERTC 错误码参考 ERTCError.h
    // 201002 = appKey 无效或 apiUrl 未配置
    // 310002 = accessToken 异常或过期
    [self hangUp];
}
```

## 九、工具栏功能

### 麦克风开关
```objc
[self.ertcEngine enableLocalAudio:enable];
```

### 扬声器/耳机切换
```objc
[self.ertcEngine setEnableSpeakerphone:YES]; // 扬声器
```

### 前后摄像头切换
```objc
[self.ertcEngine switchCamera];
```

### 摄像头禁用/启用
```objc
[self.ertcEngine enableLocalVideo:enable];
// 关闭摄像头时需要同时移除预览
[self.ertcEngine setLocalPreview:enable ? self.localPlayView : nil withRegionID:0];
```

### 挂断
```objc
// 如果是客户端呼叫设备且设备未接听，先取消邀请
if (self.isClientCallDevice && !self.isRemoteUserEnterRoom) {
    [self cancelInviteDeviceEnterMeeting];
}
[self.ertcEngine exitRoom];
self.ertcEngine.delegate = nil;
self.ertcEngine = nil;
[ERTCEngine destroySharedIntance];
```

## 十、超时处理

客户端呼叫设备时，建议设置 15 秒超时：
```objc
if (self.videoTalkSecond > 15 && !self.isRemoteUserEnterRoom) {
    [self cancelInviteDeviceEnterMeeting];
    [self releaseERTC];
}
```

## 十一、设备端呼叫客户端

设备端发起呼叫时，开发者的服务端会接收到呼叫消息，通过长链接通知客户端。客户端收到通知后使用上述方法进入对应房间即可。

注意：应用与服务端的长链接需要开发者自行实现。

## 十二、画面窗口切换

本地画面和远端画面支持全屏/小窗切换：
```objc
// 切换窗口位置后，需要重新设置本地预览（否则画面不刷新）
[self.ertcEngine setLocalPreview:nil withRegionID:0];
[self.ertcEngine setLocalPreview:self.localPlayView withRegionID:0];
```

## 十三、常见错误码

| 错误码 | 含义 | 解决方案 |
|--------|------|----------|
| 201002 | appKey 无效 | 检查 `ERTCEngineConfig.apiUrl` 和 `ERTCParam.apiUrl` 是否设置 |
| 310002 | accessToken 异常或过期 | 更新 ertcToken 后重新入会 |
| 101002 | 进入房间参数错误 | 检查 roomId、userId、token 是否正确 |
| 101006 | 无效 token | token 过期或与 appId 不匹配 |
| 101007 | 进入房间超时 | 网络问题或服务器不可达 |

## 十四、权限要求

视频通话需要以下系统权限：
- **相机权限**（`NSCameraUsageDescription`）
- **麦克风权限**（`NSMicrophoneUsageDescription`）

需要在发起视频通话前检查并请求这两个权限。
