# EZOpenSDK HarmonyOS 基础集成

## 一、HAR 库导入

### 1. 下载并导入 SDK
1. 从[萤石开放平台官网](https://open.ys7.com/mobile/download.html?type=app)下载 SDK 并解压
2. 在工程 `entry` 目录下创建 `libs` 文件夹
3. 将 `EZOpenSDK.har` 复制到 `entry/libs/` 下

### 2. 配置 oh-package.json5 依赖
```json
{
  "dependencies": {
    "@ohos/EZOpenSDK": "file:./libs/EZOpenSDK.har"
  }
}
```

### 3. 配置 build-profile.json5
```json
{
  "buildOption": {
    "nativeLib": {
      "filter": {
        "enableOverride": true
      }
    }
  }
}
```

### 4. 导入方式
```typescript
import { EZOpenSDK, EZPlayer, EZDeviceInfo, EZError } from '@ohos/EZOpenSDK'
```

## 二、权限配置（module.json5）

路径：`entry/src/main/module.json5` 的 `requestPermissions` 数组中配置。

### 数据存储持久化（SDK 必须）
```json
{
  "name": "ohos.permission.STORE_PERSISTENT_DATA",
  "reason": "$string:dependency_reason_wifi",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
```

### 相册权限（截图/录制保存）
```json
{
  "name": "ohos.permission.WRITE_IMAGEVIDEO",
  "reason": "{}",
  "usedScene": {}
},
{
  "name": "ohos.permission.READ_IMAGEVIDEO",
  "reason": "{}",
  "usedScene": {}
}
```

### 麦克风权限（对讲）
```json
{
  "name": "ohos.permission.MICROPHONE",
  "reason": "$string:dependency_reason_microphone",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
```

### 摄像头权限（扫码添加设备）
```json
{
  "name": "ohos.permission.CAMERA",
  "reason": "$string:dependency_reason_camera",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
```

### WiFi 权限（配网）
```json
{
  "name": "ohos.permission.SET_WIFI_INFO",
  "reason": "$string:dependency_reason_wifi",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
},
{
  "name": "ohos.permission.GET_NETWORK_INFO",
  "reason": "$string:dependency_reason_internet",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
```

## 三、SDK 初始化

### 国内客户（不需要设置域名）
```typescript
await EZOpenSDK.initLibWithAppKey('Your AppKey')
```

### 私有云 / 海外客户（需设置域名）
```typescript
await EZOpenSDK.initLibWithAppKey('Your AppKey', 'Your apiUrl', '')
```

## 四、SDK 授权

### 模式一：AccessToken 授权（推荐）
```typescript
await EZOpenSDK.setAccessToken('Your accessToken')
```

验证 accessToken 有效性：
```typescript
EZOpenSDK.getDeviceList(0, 1, (deviceList, totalCount, error) => {
  if (error) {
    // accessToken 无效或过期
  } else {
    // accessToken 有效
  }
})
```

### 模式二：小权限 TKToken 授权
此模式默认关闭，需在 initLib 之前开启：
```typescript
EZOpenSDK.enableSDKWithTKToken(true)
await EZOpenSDK.initLibWithAppKey('Your AppKey')
```

授权流程：
```typescript
await EZOpenSDK.setHttpToken('Your httpTKToken')
```

TKToken 类型说明：
| token 类型 | 说明 | 使用场景 |
|-----------|------|----------|
| httpToken | 非设备类小权限 token | 入参不含设备序列号的 API |
| deviceToken | 设备级小权限 token | 入参含设备序列号、不含通道号的 API |
| deviceGlobalToken | 通道级 global token | SDK 内部封装接口，取流必须 |
| deviceVideoToken | 通道级 video token | 入参含设备序列号和通道号的 API |
| previewStreamToken | 预览 token | startRealPlay 之前设置 |
| playbackStreamToken | 回放 token | startPlaybackFromDevice 之前设置 |
| talkStreamToken | 对讲 token | startVoiceTalk 之前设置 |

设置方法：
```typescript
await EZOpenSDK.setHttpToken(httpTKToken)
EZOpenSDK.setDeviceToken(deviceSerial, deviceToken)
EZOpenSDK.setDeviceCamerNoToken(deviceSerial, cameraNo, deviceGlobalToken, deviceVideoToken)
player.setStreamToken(streamToken)
```

注意事项：
- IPC 设备对讲使用 0 通道，对讲 streamToken 生成请使用 0 通道
- 双目设备取流使用交织流 0 通道，取流 streamToken 生成请使用 0 通道，deviceGlobalToken 和 deviceVideoToken 保持使用 1 通道

## 五、启动时序与就绪判断

### 核心约束

**所有依赖 SDK 的业务请求（getDeviceList、startRealPlay 等）必须在 `initLibWithAppKey` 完成 且 `setAccessToken` 完成之后再发起。**

`initLibWithAppKey` 是异步方法（Promise），在实际 App 启动场景中，以下并发情况可能导致时序颠倒：
- `initLibWithAppKey` 未 await → 后续代码立即执行
- 页面路由跳转后 `aboutToAppear` 中直接调用 `getDeviceList` → SDK 未初始化/无 Token → 返回空数据或错误

### 正确的启动顺序
```
EntryAbility onCreate / MainPage aboutToAppear
  ├── setDebugLogEnable / setDebugStreamEnable   （可选，必须在 initLib 之前）
  ├── enableP2P(true)                            （可选，在 initLib 之前）
  ├── enableSDKWithTKToken(true/false)           （可选，在 initLib 之前）
  ├── await initLibWithAppKey(appKey, apiUrl)    （异步，必须 await）
  ├── await setAccessToken(accessToken)          （异步，必须 await）
  ├── getDeviceList 验证有效性                    （验证成功后跳转）
  └── RouterManager.replaceUrl(deviceListPage)    → 设备列表页
```

### 推荐模式：就绪状态 + 页面跳转

在登录页面确认 SDK 初始化 + Token 验证通过后再跳转到业务页面，避免竞态：

```typescript
// MainPage.ets — 登录页面
async startExperienceAction() {
  // 1. SDK 初始化（必须 await）
  await EZOpenSDK.initLibWithAppKey(appKey, apiUrl, authUrl)
  // 2. 设置 Token（必须 await）
  await EZOpenSDK.setAccessToken(accessToken)
  // 3. 验证 Token 有效性
  EZOpenSDK.getDeviceList(0, 1, (deviceList, totalCount, error) => {
    if (error) {
      // Token 无效或网络异常，不跳转
    } else {
      // Token 有效，跳转设备列表页（此时 SDK 完全就绪）
      RouterManager.replaceUrl(RouterManager.deviceListPage)
    }
  })
}
```

设备列表页只需在 `aboutToAppear` 中直接发起请求即可，因为能进入此页面说明 SDK 已就绪：

```typescript
// EZDeviceListPage.ets
aboutToAppear() {
  // SDK 已就绪，直接加载设备列表
  this.getDeviceList()
}
```

### 常见错误

| 错误写法 | 后果 |
|----------|------|
| `initLibWithAppKey` 未加 await 就直接 `setAccessToken` | Token 设置在 SDK 初始化之前，API 调用全部失败 |
| `setAccessToken` 未加 await 就跳转设备列表页 | 列表页发起请求时 Token 还未生效 |
| 设备列表页 `aboutToAppear` 直接请求但入口页未验证 Token | 可能 Token 已过期返回空列表 |
| 在非 async 函数中调用 await | 编译错误，调用链路上所有函数必须标记 async |

### 自动登录场景

从本地缓存读取 AppKey/Token 后自动登录时，同样必须按顺序完成初始化 + 授权 + 验证，再跳转：

```typescript
async autoLogin() {
  let appKey = PreferenceUtil.getString(getContext(), 'UserAppKey')
  let token = PreferenceUtil.getString(getContext(), 'UserAccessToken')
  if (!appKey || !token) return

  await EZOpenSDK.initLibWithAppKey(appKey, apiUrl, authUrl)
  await EZOpenSDK.setAccessToken(token)
  // 验证通过后再跳转
  EZOpenSDK.getDeviceList(0, 1, (deviceList, totalCount, error) => {
    if (!error) {
      RouterManager.replaceUrl(RouterManager.deviceListPage)
    }
  })
}
```

## 六、SDK 账号切换
```typescript
// 第一步：退出前一个账号
EZOpenSDK.logout((error) => {})
// 第二步：重新初始化（如需更换 AppKey）
await EZOpenSDK.initLibWithAppKey('Your new AppKey')
// 第三步：重新授权
await EZOpenSDK.setAccessToken('Your new accessToken')
```
必须按顺序完成，否则会有异常。

## 七、SDK 调试模式

应用发布时务必关闭，否则沙盒中会产生大量码流文件。

```typescript
// 日志开关（建议 debug 打开，release 关闭），在初始化之前调用
EZOpenSDK.setDebugLogEnable(true)  // debug
EZOpenSDK.setDebugLogEnable(false) // release

// 码流缓存开关（release 必须关闭），在初始化之前调用
EZOpenSDK.setDebugStreamEnable(true)  // debug
EZOpenSDK.setDebugStreamEnable(false) // release
```

码流文件路径：
- 视频码流：`/data/app/el2/100/base/应用包名/haps/entry/cache/EZSavedStreamData`
- 对讲码流：`/data/app/el2/100/base/应用包名/haps/entry/cache/EZSavedIntercomData`
- 录制文件：`/data/app/el2/100/base/应用包名/haps/entry/cache/LocalRecord`

## 八、获取 SDK 版本号
```typescript
let version = EZOpenSDK.getVersion()
```

## 九、各大区域 ApiUrl

| 区域 | ApiUrl | AuthUrl |
|------|--------|---------|
| 国内 | https://open.ys7.com | https://openauth.ys7.com |
| 俄罗斯 | https://irusopen.ezvizru.com | https://irusopenauth.ezvizru.com |
| 亚洲 | https://isgpopen.ezvizlife.com | https://isgpopenauth.ezvizlife.com |
| 北美洲 | https://iusopen.ezvizlife.com | https://iusopenauth.ezvizlife.com |
| 南美洲 | https://isaopen.ezvizlife.com | https://isaopenauth.ezvizlife.com |
| 欧洲 | https://ieuopen.ezvizlife.com | https://ieuopenauth.ezvizlife.com |

## 十、ArkTS 严格模式编码规范

HarmonyOS 5.0+ 使用 ArkTS 严格模式，生成代码时必须遵守以下规则，否则编译器会报错。

### 1. 所有回调参数必须显式标注类型（arkts-no-any-unknown）

SDK 的回调函数参数不能依赖隐式类型推断，必须逐个标注。

```typescript
// ✅ 正确 — 每个回调参数都标注了类型
EZOpenSDK.getDeviceList(0, 20, (deviceList: EZDeviceInfo[] | null, totalCount: number, error: EZError | null) => {
  if (error) { return }
  // deviceList 使用
})

// ❌ 错误 — 回调参数无类型，编译报 arkts-no-any-unknown
EZOpenSDK.getDeviceList(0, 20, (deviceList, totalCount, error) => {
})
```

常用回调类型速查：
| API | 回调参数类型 |
|-----|-------------|
| `getDeviceList / getDeviceListEx` | `(deviceList: EZDeviceInfo[] \| null, totalCount: number, error: EZError \| null)` |
| `getDeviceInfo` | `(deviceInfo: EZDeviceInfo \| null, error: EZError \| null)` |
| `addDevice` | `(error: EZError \| null)` |
| `probeDeviceInfo` | `(deviceInfo: EZProbeDeviceInfo \| null, error: EZError \| null)` |
| `searchRecordFileFromCloud` | `(records: EZCloudRecordFile[] \| null, error: EZError \| null)` |
| `searchRecordFileFromDevice` | `(records: EZDeviceRecordFile[] \| null, error: EZError \| null)` |
| `setVideoLevel` | `(error: EZError \| null)` |
| `controlPTZMix / controlPTZEx` | `(error: EZError \| null)` |
| `startAPConfigWifi` | `(status: number, deviceSerial: string)` |
| `getDeviceDetailInfo` | `(info: EZDeviceDetailPublicInfo \| null)` |
| `stopLocalRecordExt` | `(result: boolean)` |
| `captureCamera` | `(url: string \| null, error: EZError \| null)` |
| `getAlarmList` | `(alarmList: EZAlarmInfo[] \| null, totalCount: number, error: EZError \| null)` |

### 2. catch 块不能加类型注解（arkts-no-types-in-catch）

ArkTS 的 catch 变量不允许任何类型注解。

```typescript
// ✅ 正确
try {
  await EZOpenSDK.initLibWithAppKey(appKey)
} catch (err) {
  hilog.error(0x0000, TAG, `初始化失败: ${JSON.stringify(err)}`)
}

// ❌ 错误 — catch 变量加了类型注解
try {
  await EZOpenSDK.initLibWithAppKey(appKey)
} catch (err: Error) {
  // 编译报错 arkts-no-types-in-catch
}
```

### 3. 禁止用对象字面量做类型声明（arkts-no-obj-literals-as-types）

返回值或变量类型不能使用 `{ key: Type }` 这种内联对象字面量，必须使用已有的 class 或 interface。

```typescript
// ✅ 正确 — 使用 SDK 已有类型
private getCurrentRecord(): EZCloudRecordFile | EZDeviceRecordFile | null {
  return this.cloudRecords[this.selectedIndex]
}

// ❌ 错误 — 对象字面量做类型声明
private getCurrentRecord(): { startTime: Date, stopTime: Date } | null {
  // 编译报错 arkts-no-obj-literals-as-types
}
```

### 4. 所有局部变量必须可推断出具体类型

当变量通过可选链 `?.` 或联合类型方法的返回值赋值时，编译器可能推断为 `any`，需要显式标注。

```typescript
// ✅ 正确 — 显式标注类型 + ?? 提供默认值
let picPath: string = await this.player?.capturePicture() ?? ''
let osdTime: Date | null = await this.player?.getOSDTime() ?? null
let record: EZCloudRecordFile | EZDeviceRecordFile | null = this.getCurrentRecord()

// ❌ 错误 — 可选链返回 undefined | T，推断为 any
let picPath = await this.player?.capturePicture()
let osdTime = await this.player?.getOSDTime()
```

### 5. XComponent 的 setXComponentSurfaceSize 已废弃

`setXComponentSurfaceSize` 在 API 12+ 中已废弃（编译器 WARN），但功能正常。如果要消除警告，可移除该调用（播放器会自动适应 Surface 尺寸）。

```typescript
// 当前写法（有 WARN 但不影响编译和运行）
.onLoad(() => {
  this.xComponentController.setXComponentSurfaceSize({ surfaceWidth: 1920, surfaceHeight: 1080 })
  this.player?.setPlayerView(this.xComponentController.getXComponentSurfaceId())
})

// 消除 WARN 的写法（直接移除 setXComponentSurfaceSize）
.onLoad(() => {
  this.player?.setPlayerView(this.xComponentController.getXComponentSurfaceId())
})
```

### 6. 完整的类型安全回调示例

```typescript
// 设备列表查询
EZOpenSDK.getDeviceListEx(0, 20, (deviceList: EZDeviceInfo[] | null, totalCount: number, error: EZError | null) => {
  if (error) {
    hilog.error(0x0000, TAG, `获取设备列表失败: code=${error.code}`)
    return
  }
  this.deviceList = deviceList || []
})

// 设备探测
EZOpenSDK.probeDeviceInfo(serial, '', (deviceInfo: EZProbeDeviceInfo | null, error: EZError | null) => {
  if (!error) {
    // 设备在线
  } else if (error.code === 120023) {
    // 设备离线需配网
  }
})

// AP 配网
EZOpenSDK.startAPConfigWifi(ssid, password, serial, verifyCode, (status: number, deviceSerial: string) => {
  if (status === EZWifiConfigStatus.DEVICE_PLATFORM_REGISTED) {
    // 配网成功
  }
})

// 云台控制
EZOpenSDK.controlPTZMix(serial, cameraNo, EZPTZCommand.Left, EZPTZAction.EZPTZActionStart, 2, (error: EZError | null) => {
  if (error) {
    hilog.error(0x0000, TAG, `云台控制失败: ${error.code}`)
  }
})
```
