527 lines
12 KiB
Markdown
527 lines
12 KiB
Markdown
|
|
# 通话功能模块
|
|||
|
|
|
|||
|
|
## 模块概述
|
|||
|
|
通话功能模块负责处理用户之间的语音和视频通话功能,包括发起通话、接听/拒绝通话、通话记录等。
|
|||
|
|
|
|||
|
|
## 相关文件
|
|||
|
|
- `CallActivity.java` - 通话界面
|
|||
|
|
- `IncomingCallActivity.java` - 来电界面
|
|||
|
|
- `CallHistoryActivity.java` - 通话记录界面
|
|||
|
|
- `CallManager.java` - 通话管理器
|
|||
|
|
- `CallSignalingClient.java` - 信令客户端
|
|||
|
|
- `CallApiService.java` - 通话API接口
|
|||
|
|
- `InitiateCallRequest.java` - 发起通话请求模型
|
|||
|
|
- `InitiateCallResponse.java` - 发起通话响应模型
|
|||
|
|
- `CallRecordResponse.java` - 通话记录响应模型
|
|||
|
|
- `CallHistoryResponse.java` - 通话历史响应模型
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 接口列表
|
|||
|
|
|
|||
|
|
### 1. 发起通话
|
|||
|
|
**接口地址**: `POST /api/front/call/initiate`
|
|||
|
|
|
|||
|
|
**请求参数** (InitiateCallRequest):
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"calleeId": "integer", // 被叫用户ID(必填)
|
|||
|
|
"callType": "string" // 通话类型: voice/video(必填)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**返回数据** (InitiateCallResponse):
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"callId": "string", // 通话ID
|
|||
|
|
"callType": "string", // 通话类型
|
|||
|
|
"calleeId": "integer", // 被叫用户ID
|
|||
|
|
"calleeName": "string", // 被叫用户昵称
|
|||
|
|
"calleeAvatar": "string", // 被叫用户头像
|
|||
|
|
"status": "string", // 通话状态: calling
|
|||
|
|
"signalingUrl": "string" // 信令服务器URL
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. 接听通话
|
|||
|
|
**接口地址**: `POST /api/front/call/accept/{callId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
callId: string // 通话ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "接听成功",
|
|||
|
|
"data": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. 拒绝通话
|
|||
|
|
**接口地址**: `POST /api/front/call/reject/{callId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
callId: string // 通话ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "已拒绝",
|
|||
|
|
"data": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. 取消通话
|
|||
|
|
**接口地址**: `POST /api/front/call/cancel/{callId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
callId: string // 通话ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "已取消",
|
|||
|
|
"data": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. 结束通话
|
|||
|
|
**接口地址**: `POST /api/front/call/end/{callId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
callId: string // 通话ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**:
|
|||
|
|
```
|
|||
|
|
endReason: string // 结束原因(可选)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "通话已结束",
|
|||
|
|
"data": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 6. 获取通话记录
|
|||
|
|
**接口地址**: `GET /api/front/call/history`
|
|||
|
|
|
|||
|
|
**请求参数**:
|
|||
|
|
```
|
|||
|
|
page: integer // 页码,默认1
|
|||
|
|
limit: integer // 每页数量,默认20
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**返回数据** (CallHistoryResponse):
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"records": [
|
|||
|
|
{
|
|||
|
|
"id": "long", // 记录ID
|
|||
|
|
"callId": "string", // 通话ID
|
|||
|
|
"callType": "string", // 通话类型: voice/video
|
|||
|
|
"callDirection": "string", // 通话方向: outgoing/incoming
|
|||
|
|
"otherUserId": "integer", // 对方用户ID
|
|||
|
|
"otherUserName": "string", // 对方用户昵称
|
|||
|
|
"otherUserAvatar": "string",// 对方用户头像
|
|||
|
|
"status": "string", // 状态: completed/missed/rejected/cancelled
|
|||
|
|
"duration": "integer", // 通话时长(秒)
|
|||
|
|
"startTime": "long", // 开始时间戳
|
|||
|
|
"endTime": "long" // 结束时间戳
|
|||
|
|
}
|
|||
|
|
],
|
|||
|
|
"total": "integer",
|
|||
|
|
"page": "integer",
|
|||
|
|
"limit": "integer"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 7. 删除通话记录
|
|||
|
|
**接口地址**: `DELETE /api/front/call/record/{recordId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
recordId: long // 记录ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "删除成功",
|
|||
|
|
"data": true
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 8. 获取未接来电数量
|
|||
|
|
**接口地址**: `GET /api/front/call/missed/count`
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": 5 // 未接来电数量
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 9. 获取通话状态
|
|||
|
|
**接口地址**: `GET /api/front/call/status`
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"inCall": "boolean", // 是否正在通话中
|
|||
|
|
"callId": "string", // 当前通话ID
|
|||
|
|
"callType": "string", // 通话类型
|
|||
|
|
"otherUserId": "integer", // 对方用户ID
|
|||
|
|
"startTime": "long" // 通话开始时间
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 10. 获取通话详情
|
|||
|
|
**接口地址**: `GET /api/front/call/detail/{callId}`
|
|||
|
|
|
|||
|
|
**路径参数**:
|
|||
|
|
```
|
|||
|
|
callId: string // 通话ID
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**请求参数**: 无
|
|||
|
|
|
|||
|
|
**返回数据** (CallRecordResponse):
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"code": 200,
|
|||
|
|
"msg": "success",
|
|||
|
|
"data": {
|
|||
|
|
"id": "long",
|
|||
|
|
"callId": "string",
|
|||
|
|
"callType": "string",
|
|||
|
|
"callDirection": "string",
|
|||
|
|
"callerId": "integer",
|
|||
|
|
"callerName": "string",
|
|||
|
|
"callerAvatar": "string",
|
|||
|
|
"calleeId": "integer",
|
|||
|
|
"calleeName": "string",
|
|||
|
|
"calleeAvatar": "string",
|
|||
|
|
"status": "string",
|
|||
|
|
"duration": "integer",
|
|||
|
|
"startTime": "long",
|
|||
|
|
"endTime": "long",
|
|||
|
|
"endReason": "string"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 功能说明
|
|||
|
|
|
|||
|
|
### 通话类型
|
|||
|
|
|
|||
|
|
| 类型 | 值 | 说明 |
|
|||
|
|
|------|-----|------|
|
|||
|
|
| 语音通话 | voice | 仅语音通话 |
|
|||
|
|
| 视频通话 | video | 视频+语音通话 |
|
|||
|
|
|
|||
|
|
### 通话状态
|
|||
|
|
|
|||
|
|
| 状态 | 值 | 说明 |
|
|||
|
|
|------|-----|------|
|
|||
|
|
| 呼叫中 | calling | 正在呼叫对方 |
|
|||
|
|
| 振铃中 | ringing | 对方正在振铃 |
|
|||
|
|
| 通话中 | connected | 通话已接通 |
|
|||
|
|
| 已完成 | completed | 通话正常结束 |
|
|||
|
|
| 未接听 | missed | 对方未接听 |
|
|||
|
|
| 已拒绝 | rejected | 对方拒绝接听 |
|
|||
|
|
| 已取消 | cancelled | 主叫方取消 |
|
|||
|
|
| 已结束 | ended | 通话结束 |
|
|||
|
|
|
|||
|
|
### 通话方向
|
|||
|
|
|
|||
|
|
| 方向 | 值 | 说明 |
|
|||
|
|
|------|-----|------|
|
|||
|
|
| 呼出 | outgoing | 我发起的通话 |
|
|||
|
|
| 呼入 | incoming | 对方发起的通话 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 通话流程
|
|||
|
|
|
|||
|
|
### 发起通话流程
|
|||
|
|
```
|
|||
|
|
1. 用户A点击通话按钮
|
|||
|
|
↓
|
|||
|
|
2. 调用发起通话接口
|
|||
|
|
↓
|
|||
|
|
3. 获取callId和信令服务器地址
|
|||
|
|
↓
|
|||
|
|
4. 连接信令服务器
|
|||
|
|
↓
|
|||
|
|
5. 等待对方响应
|
|||
|
|
↓
|
|||
|
|
6. 对方接听 → 建立P2P连接 → 开始通话
|
|||
|
|
或
|
|||
|
|
对方拒绝/超时 → 结束流程
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 接听通话流程
|
|||
|
|
```
|
|||
|
|
1. 用户B收到来电通知(WebSocket推送)
|
|||
|
|
↓
|
|||
|
|
2. 显示来电界面
|
|||
|
|
↓
|
|||
|
|
3. 用户点击接听按钮
|
|||
|
|
↓
|
|||
|
|
4. 调用接听通话接口
|
|||
|
|
↓
|
|||
|
|
5. 连接信令服务器
|
|||
|
|
↓
|
|||
|
|
6. 建立P2P连接
|
|||
|
|
↓
|
|||
|
|
7. 开始通话
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 结束通话流程
|
|||
|
|
```
|
|||
|
|
1. 用户点击挂断按钮
|
|||
|
|
↓
|
|||
|
|
2. 调用结束通话接口
|
|||
|
|
↓
|
|||
|
|
3. 断开P2P连接
|
|||
|
|
↓
|
|||
|
|
4. 断开信令连接
|
|||
|
|
↓
|
|||
|
|
5. 保存通话记录
|
|||
|
|
↓
|
|||
|
|
6. 返回上一页面
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## WebSocket推送
|
|||
|
|
|
|||
|
|
通话相关的实时通知通过WebSocket推送:
|
|||
|
|
|
|||
|
|
### 来电通知
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "incoming_call",
|
|||
|
|
"callId": "call123",
|
|||
|
|
"callType": "video",
|
|||
|
|
"callerId": 123,
|
|||
|
|
"callerName": "张三",
|
|||
|
|
"callerAvatar": "https://..."
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 通话状态变化
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "call_status",
|
|||
|
|
"callId": "call123",
|
|||
|
|
"status": "connected",
|
|||
|
|
"timestamp": 1703001234567
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 对方挂断
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "call_ended",
|
|||
|
|
"callId": "call123",
|
|||
|
|
"endReason": "normal",
|
|||
|
|
"duration": 120
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 使用示例
|
|||
|
|
|
|||
|
|
### 1. 发起语音通话
|
|||
|
|
```java
|
|||
|
|
private void initiateVoiceCall(int calleeId) {
|
|||
|
|
CallApiService callApiService = // 获取CallApiService实例
|
|||
|
|
|
|||
|
|
InitiateCallRequest request = new InitiateCallRequest(calleeId, "voice");
|
|||
|
|
Call<CallApiResponse<InitiateCallResponse>> call =
|
|||
|
|
callApiService.initiateCall(request);
|
|||
|
|
|
|||
|
|
call.enqueue(new Callback<CallApiResponse<InitiateCallResponse>>() {
|
|||
|
|
@Override
|
|||
|
|
public void onResponse(Call<CallApiResponse<InitiateCallResponse>> call,
|
|||
|
|
Response<CallApiResponse<InitiateCallResponse>> response) {
|
|||
|
|
if (response.isSuccessful() && response.body() != null) {
|
|||
|
|
CallApiResponse<InitiateCallResponse> apiResponse = response.body();
|
|||
|
|
if (apiResponse.getCode() == 200) {
|
|||
|
|
InitiateCallResponse data = apiResponse.getData();
|
|||
|
|
String callId = data.getCallId();
|
|||
|
|
String signalingUrl = data.getSignalingUrl();
|
|||
|
|
|
|||
|
|
// 跳转到通话界面
|
|||
|
|
Intent intent = new Intent(this, CallActivity.class);
|
|||
|
|
intent.putExtra("callId", callId);
|
|||
|
|
intent.putExtra("callType", "voice");
|
|||
|
|
intent.putExtra("signalingUrl", signalingUrl);
|
|||
|
|
startActivity(intent);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void onFailure(Call<CallApiResponse<InitiateCallResponse>> call, Throwable t) {
|
|||
|
|
Toast.makeText(this, "发起通话失败", Toast.LENGTH_SHORT).show();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. 接听通话
|
|||
|
|
```java
|
|||
|
|
private void acceptCall(String callId) {
|
|||
|
|
CallApiService callApiService = // 获取CallApiService实例
|
|||
|
|
|
|||
|
|
Call<CallApiResponse<Boolean>> call = callApiService.acceptCall(callId);
|
|||
|
|
|
|||
|
|
call.enqueue(new Callback<CallApiResponse<Boolean>>() {
|
|||
|
|
@Override
|
|||
|
|
public void onResponse(Call<CallApiResponse<Boolean>> call,
|
|||
|
|
Response<CallApiResponse<Boolean>> response) {
|
|||
|
|
if (response.isSuccessful() && response.body() != null) {
|
|||
|
|
CallApiResponse<Boolean> apiResponse = response.body();
|
|||
|
|
if (apiResponse.getCode() == 200) {
|
|||
|
|
// 跳转到通话界面
|
|||
|
|
Intent intent = new Intent(this, CallActivity.class);
|
|||
|
|
intent.putExtra("callId", callId);
|
|||
|
|
startActivity(intent);
|
|||
|
|
finish();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void onFailure(Call<CallApiResponse<Boolean>> call, Throwable t) {
|
|||
|
|
Toast.makeText(this, "接听失败", Toast.LENGTH_SHORT).show();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 获取通话记录
|
|||
|
|
```java
|
|||
|
|
private void loadCallHistory(int page) {
|
|||
|
|
CallApiService callApiService = // 获取CallApiService实例
|
|||
|
|
|
|||
|
|
Call<CallApiResponse<CallHistoryResponse>> call =
|
|||
|
|
callApiService.getCallHistory(page, 20);
|
|||
|
|
|
|||
|
|
call.enqueue(new Callback<CallApiResponse<CallHistoryResponse>>() {
|
|||
|
|
@Override
|
|||
|
|
public void onResponse(Call<CallApiResponse<CallHistoryResponse>> call,
|
|||
|
|
Response<CallApiResponse<CallHistoryResponse>> response) {
|
|||
|
|
if (response.isSuccessful() && response.body() != null) {
|
|||
|
|
CallApiResponse<CallHistoryResponse> apiResponse = response.body();
|
|||
|
|
if (apiResponse.getCode() == 200) {
|
|||
|
|
CallHistoryResponse data = apiResponse.getData();
|
|||
|
|
List<CallRecordResponse> records = data.getRecords();
|
|||
|
|
|
|||
|
|
// 更新UI显示通话记录
|
|||
|
|
updateCallHistoryList(records);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@Override
|
|||
|
|
public void onFailure(Call<CallApiResponse<CallHistoryResponse>> call, Throwable t) {
|
|||
|
|
Toast.makeText(this, "加载失败", Toast.LENGTH_SHORT).show();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 技术实现
|
|||
|
|
|
|||
|
|
### WebRTC集成
|
|||
|
|
通话功能基于WebRTC技术实现:
|
|||
|
|
- 使用信令服务器交换SDP和ICE候选
|
|||
|
|
- 建立P2P连接进行音视频传输
|
|||
|
|
- 支持NAT穿透
|
|||
|
|
|
|||
|
|
### 权限要求
|
|||
|
|
- 语音通话:`RECORD_AUDIO`
|
|||
|
|
- 视频通话:`RECORD_AUDIO` + `CAMERA`
|
|||
|
|
- 网络访问:`INTERNET`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 注意事项
|
|||
|
|
1. 所有接口都需要登录认证
|
|||
|
|
2. 通话前需要检查对方是否在线
|
|||
|
|
3. 需要申请音频和摄像头权限
|
|||
|
|
4. 通话过程中保持屏幕常亮
|
|||
|
|
5. 通话时禁用锁屏
|
|||
|
|
6. 建议使用耳机以获得更好的通话质量
|
|||
|
|
7. 通话记录保存30天
|
|||
|
|
8. 未接来电会显示红点提示
|
|||
|
|
9. 通话过程中网络断开需要自动重连
|
|||
|
|
10. 需要集成WebRTC库实现音视频通话
|