# EZOpenSDK Android 对讲

## ⛔ 强制约束（违反即为 Bug）

### 1. 对讲播放器的 Handler 必须处理对讲消息

- **禁止** 创建空 Handler（只打日志不处理消息）
- **必须** 在 Handler 的 `handleMessage` 中处理以下消息：
  - `EZRealPlayConstants.MSG_REALPLAY_VOICETALK_SUCCESS` → 对讲成功，更新 UI 为对讲中状态
  - `EZRealPlayConstants.MSG_REALPLAY_VOICETALK_FAIL` → 对讲失败，从 `msg.obj` 获取 `ErrorInfo`，提示错误
  - `EZRealPlayConstants.MSG_REALPLAY_VOICETALK_STOP` → 对讲停止，恢复预览声音
- 对讲失败时**必须**从 `msg.obj` 强转为 `ErrorInfo` 获取错误码（与预览/回放失败处理方式一致）

```java
// ✅ 正确 — Handler 中处理对讲消息
Handler talkHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case EZRealPlayConstants.MSG_REALPLAY_VOICETALK_SUCCESS:
                // 对讲成功，更新 UI
                break;
            case EZRealPlayConstants.MSG_REALPLAY_VOICETALK_FAIL:
                ErrorInfo errorInfo = (ErrorInfo) msg.obj;
                // 对讲失败，提示错误
                break;
        }
    }
};

// ❌ 错误 — 空 Handler，对讲成功/失败无法通知 UI
Handler talkHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        Log.d(TAG, "收到消息 what=" + msg.what); // 只打日志，不处理
    }
};
```

---

## 对讲模块 import 参考

```java
import com.videogo.openapi.EZOpenSDK;
import com.videogo.openapi.EZPlayer;
import com.videogo.openapi.EZConstants;                                  // EZTalkbackCapability、EZRealPlayConstants
import com.videogo.openapi.EZConstants.EZRealPlayConstants;              // MSG_REALPLAY_VOICETALK_SUCCESS/FAIL/STOP
import com.videogo.errorlayer.ErrorInfo;                                 // 对讲失败时的错误对象
```

## 一、对讲能力判断

通过 EZDeviceInfo 判断设备对讲能力：
```java
EZConstants.EZTalkbackCapability capability = deviceInfo.isSupportTalk();
// 或使用工具类（兼容子设备）
EZConstants.EZTalkbackCapability capability = EZBusinessTool.isSupportTalk(deviceInfo, cameraInfo);
```

| 枚举值 | 说明 |
|--------|------|
| EZTalkbackNoSupport | 不支持对讲 |
| EZTalkbackFullDuplex | 支持全双工对讲 |
| EZTalkbackHalfDuplex | 支持半双工对讲 |

也可通过能力集位数判断：supportExtShort 第 2 位，1-全双工，3-半双工，4-同时支持。
NVR 设备通道对讲：supportExtShort 第 192 位，1-支持。

## 二、权限申请（对讲前必须）

对讲功能需要录音相关权限，**必须在发起对讲前动态申请**，否则对讲无声音或直接失败。

### 需要申请的权限

| 权限 | 说明 |
|------|------|
| `Manifest.permission.RECORD_AUDIO` | 录音权限（危险权限，必须动态申请） |
| `Manifest.permission.MODIFY_AUDIO_SETTINGS` | 修改音频设置权限（危险权限，必须动态申请） |

### AndroidManifest.xml 声明

```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
```

### 动态申请示例

```java
// 对讲按钮点击时检查权限
@TargetApi(Build.VERSION_CODES.M)
private void checkAndRequestPermission() {
    List<String> lackedPermission = new ArrayList<>();
    if (!(checkSelfPermission(Manifest.permission.MODIFY_AUDIO_SETTINGS) == PackageManager.PERMISSION_GRANTED)) {
        lackedPermission.add(Manifest.permission.MODIFY_AUDIO_SETTINGS);
    }
    if (!(checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED)) {
        lackedPermission.add(Manifest.permission.RECORD_AUDIO);
    }
    // 权限都已经有了
    if (lackedPermission.size() == 0) {
        startTalk();
    } else {
        // 请求所缺少的权限，在 onRequestPermissionsResult 中再看是否获得权限
        String[] requestPermissions = new String[lackedPermission.size()];
        lackedPermission.toArray(requestPermissions);
        requestPermissions(requestPermissions, 1000);
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == 1000 && hasAllPermissionsGranted(grantResults)) {
        startTalk();
    } else {
        Toast.makeText(this, "需要麦克风权限才能使用对讲功能", Toast.LENGTH_SHORT).show();
    }
}

private boolean hasAllPermissionsGranted(int[] grantResults) {
    for (int grantResult : grantResults) {
        if (grantResult != PackageManager.PERMISSION_GRANTED) {
            return false;
        }
    }
    return true;
}
```

### 注意事项

- **必须**在点击对讲按钮时检查权限，不要在页面启动时提前申请（避免用户困惑）
- 用户拒绝权限后应给出 Toast 提示，不要反复弹窗
- 两个权限都授予后才开始对讲，任一被拒绝则不发起

---

## 三、全双工对讲

全双工对讲允许双方同时发送和接收语音。

### 完整流程
```java
// 1. 创建对讲播放器（需独立于预览播放器）
EZPlayer talkPlayer = EZOpenSDK.getInstance().createPlayer(deviceSerial, cameraNo);

// 2. 设置 Handler 回调
talkPlayer.setHandler(handler);

// 3. 对讲开启前，关闭预览播放器的声音（避免回音）
if (previewPlayer != null) {
    previewPlayer.closeSound();
}

// 4. 开始对讲
talkPlayer.startVoiceTalk(true);   // IPC 设备
// talkPlayer.startVoiceTalk(false); // NVR 设备通道对讲

// 5. 停止对讲
talkPlayer.stopVoiceTalk();

// 6. 对讲结束后恢复预览播放器声音
if (previewPlayer != null) {
    previewPlayer.openSound();
}

// 7. 释放对讲播放器
talkPlayer.release();
```

### 对讲消息回调
```java
@Override
public boolean handleMessage(Message msg) {
    switch (msg.what) {
        case EZRealPlayConstants.MSG_REALPLAY_VOICETALK_SUCCESS: // 对讲成功
            break;
        case EZRealPlayConstants.MSG_REALPLAY_VOICETALK_FAIL: // 对讲失败
            ErrorInfo errorInfo = (ErrorInfo) msg.obj;
            int errorCode = errorInfo.errorCode;
            break;
        case EZRealPlayConstants.MSG_REALPLAY_VOICETALK_STOP: // 对讲停止
            // 恢复预览播放器声音
            break;
    }
    return false;
}
```

## 四、半双工对讲

半双工对讲为交替模式，类似对讲机。代码实现与全双工一致，区别在于发起成功后默认为"手机端听-设备端说"模式。

### 听说模式切换
```java
// 切换到 手机端说-设备端听
talkPlayer.setVoiceTalkStatus(true);

// 切换到 手机端听-设备端说
talkPlayer.setVoiceTalkStatus(false);
```

## 五、对讲变声

在门铃、门禁等场景中保护隐私。需设备开通变声服务（仅国内支持）。

```java
// 对讲过程中调用
talkPlayer.startVoiceChange(EZConstants.EZVoiceChangeType type, callback);
```

| 枚举值 | 效果 |
|--------|------|
| EZ_VOICE_CHANGE_TYPE_NORMAL | 原音 |
| EZ_VOICE_CHANGE_TYPE_MAN | 大叔音 |
| EZ_VOICE_CHANGE_TYPE_CLOWN | 小丑音 |

## 六、本地音频音量回调

对讲时获取手机本地采集的音频音量，用于音柱效果展示：
```java
// 在 startVoiceTalk 之前设置
talkPlayer.setVoiceTalkLoudnessCallback(new EZVoiceTalk.OnLoudnessListener() {
    @Override
    public void onLoudness(float v) {
        // v 为分贝值（负数）
        // [-90, -40) 显示1格
        // [-40, -35) 显示2格
        // [-35, -30) 显示3格
        // [-30, -20) 显示4格
        // [-20, 0]   显示5格
    }
}, 0.15f); // 回调间隔（秒）
```

## 七、对讲麦克风控制

```java
// 打开麦克风
talkPlayer.openVoiceTalkMicrophone();

// 关闭麦克风
talkPlayer.closeVoiceTalkMicrophone();
```

## 八、对讲远端静音

```java
// 静音远端（设备端）声音
talkPlayer.setTalkRemoteMuted(true);

// 取消静音
talkPlayer.setTalkRemoteMuted(false);
```

## 九、系统 AEC（回声消除）

```java
// 使用系统回声消除
talkPlayer.setUseSystemAEC(true);
```

## 十、对讲 CallingId

```java
talkPlayer.setVoiceTalkCallingId(String callingId);
```

## 十一、注意事项

1. 对讲播放器必须独立于预览/回放播放器，播放器功能是单一的
2. **对讲前必须动态申请 RECORD_AUDIO 和 MODIFY_AUDIO_SETTINGS 权限**，两个权限都授予后才能发起对讲
3. 对讲开启前务必关闭预览播放器声音，否则会产生回音
4. 对讲结束后记得恢复预览播放器声音
4. v5.18 起对讲播放器无须配置验证码
5. IPC 设备对讲通道号：AccessToken 模式下使用实际通道号（通常为 1）；小权限 TKToken 模式下，对讲 streamToken 需使用 0 通道生成
6. 安卓对讲声音比 iOS 小是正常现象（iOS 开了 AGC 音效增强）
