Compare commits
No commits in common. "e7649df1af5c7238b033a69dad5db97cc8ea8aaf" and "2d88a5534813b8295c826f020cd0505d90999add" have entirely different histories.
e7649df1af
...
2d88a55348
2637
Android接口参数详细文档.md
2637
Android接口参数详细文档.md
File diff suppressed because it is too large
Load Diff
|
|
@ -1,340 +0,0 @@
|
|||
# Android应用接口真实调用情况分析报告
|
||||
|
||||
## 📊 分析概述
|
||||
|
||||
本报告详细分析了Android应用中对后端接口的**真实调用情况**,区分了"已定义但未使用"和"已实际调用"的接口。
|
||||
|
||||
**分析时间**: 2024年12月30日
|
||||
**分析范围**: android-app 和 java-backend 全部代码
|
||||
**分析方法**: 代码静态分析 + 接口定义对比
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已真实调用的接口(共 45+ 个)
|
||||
|
||||
### 1. 用户认证模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/login` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/register` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/sendCode` | LoginActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/logout` | SettingsPageActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/user` | ProfileActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 2. 直播间模块 (85% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/live/public/rooms` | MainActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/rooms` | MainActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/live/public/rooms/{id}` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/live/rooms/{id}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/follow` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/room/{id}/start` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/room/{id}/stop` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/live/online/broadcast/{roomId}` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/live/public/rooms/{roomId}/messages` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/live/rooms/{roomId}/gift` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `GET /api/front/live/public/rooms/{roomId}/viewers/count` - 已定义但未使用
|
||||
- ❌ `GET /api/front/live/rooms/{roomId}/viewers` - 已定义但未使用
|
||||
|
||||
### 3. 礼物打赏模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/gift/list` | RoomDetailActivity.java:1190 | ✅ 真实调用 |
|
||||
| `GET /api/front/gift/balance` | RoomDetailActivity.java:1588 | ✅ 真实调用 |
|
||||
| `POST /api/front/gift/send` | RoomDetailActivity.java:1636 | ✅ 真实调用 |
|
||||
| `GET /api/front/gift/recharge/options` | RoomDetailActivity.java:1387 | ✅ 真实调用 |
|
||||
| `POST /api/front/gift/recharge/create` | RoomDetailActivity.java:1460 | ✅ 真实调用 |
|
||||
| `POST /api/front/pay/payment` | RoomDetailActivity.java:1537 | ✅ 真实调用 |
|
||||
|
||||
### 4. 私聊会话模块 (90% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/conversations` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/conversations/search` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/with/{otherUserId}` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/{id}/read` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/conversations/{id}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/conversations/{id}/messages` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/conversations/{id}/messages` | ChatActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `DELETE /api/front/conversations/messages/{id}` - 已定义但未使用(删除单条消息功能未实现)
|
||||
|
||||
### 5. 好友管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/friends` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/friends/{friendId}` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/block/{friendId}` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/unblock/{friendId}` | BlockedListActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/friends/blocked` | BlockedListActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/users/search` | AddFriendActivity.java:62 | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/request` | AddFriendActivity.java:142 | ✅ 真实调用 |
|
||||
| `GET /api/front/friends/requests` | FriendRequestsActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/friends/requests/{requestId}/handle` | FriendRequestsActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 6. 关注功能模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/follow/follow` | UserProfileReadOnlyActivity.java:141 | ✅ 真实调用 |
|
||||
| `POST /api/front/follow/unfollow` | UserProfileReadOnlyActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/status/{userId}` | UserProfileReadOnlyActivity.java:105 | ✅ 真实调用 |
|
||||
| `POST /api/front/follow/status/batch` | FishPondActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/following` | FollowingListActivity.java:62 | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/followers` | FansListActivity.java:62 | ✅ 真实调用 |
|
||||
| `GET /api/front/follow/stats` | ProfileActivity.java:684 | ✅ 真实调用 |
|
||||
|
||||
### 7. 作品管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/works/publish` | PublishWorkActivity.java:766 | ✅ 真实调用 |
|
||||
| `POST /api/front/works/update` | EditWorkActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/delete/{worksId}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/detail/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/search` | SearchActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/user/{userId}` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/like/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/unlike/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/collect/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/works/uncollect/{worksId}` | WorkDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/my/liked` | ProfileActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/works/my/collected` | ProfileActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 8. 文件上传模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/user/upload/image` | PublishWorkActivity.java:642 | ✅ 真实调用 |
|
||||
| `POST /api/front/upload/work/video` | PublishWorkActivity.java:691 | ✅ 真实调用 |
|
||||
|
||||
### 9. 在线状态模块 (80% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/online/status/{userId}` | ChatActivity.java | ✅ 真实调用 |
|
||||
| `POST /api/front/online/status/batch` | MyFriendsActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/room/{roomId}/count` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/room/{roomId}/users` | RoomDetailActivity.java | ✅ 真实调用 |
|
||||
|
||||
**未调用接口**:
|
||||
- ❌ `GET /api/front/online/stats` - 已定义但未使用(连接统计功能未实现)
|
||||
|
||||
### 10. 离线消息模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/online/offline/count/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/online/offline/messages/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/online/offline/messages/{userId}` | MessagesActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 11. 搜索功能模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/search/live-rooms` | SearchActivity.java:156 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/hot` | SearchActivity.java:257 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/suggestions` | SearchActivity.java:286 | ✅ 真实调用 |
|
||||
| `GET /api/front/search/history` | SearchActivity.java | ✅ 真实调用 |
|
||||
| `DELETE /api/front/search/history` | SearchActivity.java | ✅ 真实调用 |
|
||||
|
||||
### 12. 观看历史模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `POST /api/front/watch/history` | RoomDetailActivity.java:744 | ✅ 真实调用 |
|
||||
|
||||
### 13. 分类管理模块 (100% 调用)
|
||||
|
||||
| 接口 | 调用位置 | 状态 |
|
||||
|------|---------|------|
|
||||
| `GET /api/front/category/live-room` | MainActivity.java | ✅ 真实调用 |
|
||||
| `GET /api/front/category/work` | PublishWorkActivity.java | ✅ 真实调用 |
|
||||
|
||||
---
|
||||
|
||||
## ❌ 已定义但未真实调用的接口(共 8 个)
|
||||
|
||||
### 1. 直播间模块
|
||||
|
||||
```java
|
||||
// ApiService.java 中已定义,但在代码中未找到调用
|
||||
❌ GET /api/front/live/public/rooms/{roomId}/viewers/count
|
||||
- 功能: 获取观众数量
|
||||
- 原因: 使用 WebSocket 实时推送在线人数,不需要轮询
|
||||
|
||||
❌ GET /api/front/live/rooms/{roomId}/viewers
|
||||
- 功能: 获取观众列表
|
||||
- 原因: 功能未实现,UI中没有显示观众列表的入口
|
||||
```
|
||||
|
||||
### 2. 私聊模块
|
||||
|
||||
```java
|
||||
❌ DELETE /api/front/conversations/messages/{id}
|
||||
- 功能: 删除单条消息
|
||||
- 原因: UI中只实现了删除整个会话,未实现删除单条消息
|
||||
```
|
||||
|
||||
### 3. 在线状态模块
|
||||
|
||||
```java
|
||||
❌ GET /api/front/online/stats
|
||||
- 功能: 获取连接统计信息
|
||||
- 原因: 管理功能,前端应用不需要
|
||||
```
|
||||
|
||||
### 4. 消息表情回应模块(整个模块未使用)
|
||||
|
||||
```java
|
||||
❌ POST /api/front/messages/reactions/add
|
||||
❌ DELETE /api/front/messages/reactions/remove
|
||||
❌ GET /api/front/messages/{messageId}/reactions
|
||||
❌ GET /api/front/messages/{messageId}/reactions/users
|
||||
- 功能: 消息表情回应(类似微信的点赞、爱心等)
|
||||
- 原因: 功能未实现,UI设计中没有这个功能
|
||||
```
|
||||
|
||||
### 5. 搜索模块
|
||||
|
||||
```java
|
||||
❌ GET /api/front/search/users
|
||||
- 功能: 全局搜索用户
|
||||
- 原因: 使用了 /api/front/users/search 替代
|
||||
|
||||
❌ GET /api/front/search/works
|
||||
- 功能: 全局搜索作品
|
||||
- 原因: 使用了 /api/front/works/search 替代
|
||||
|
||||
❌ GET /api/front/search/all
|
||||
- 功能: 综合搜索
|
||||
- 原因: 功能未实现,当前只支持分类搜索
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 统计数据
|
||||
|
||||
### 整体调用率
|
||||
|
||||
| 模块 | 已定义接口数 | 真实调用数 | 调用率 |
|
||||
|------|------------|-----------|--------|
|
||||
| 用户认证 | 5 | 5 | 100% |
|
||||
| 直播间 | 12 | 10 | 83% |
|
||||
| 礼物打赏 | 6 | 6 | 100% |
|
||||
| 私聊会话 | 8 | 7 | 88% |
|
||||
| 好友管理 | 9 | 9 | 100% |
|
||||
| 关注功能 | 7 | 7 | 100% |
|
||||
| 作品管理 | 12 | 12 | 100% |
|
||||
| 文件上传 | 2 | 2 | 100% |
|
||||
| 在线状态 | 5 | 4 | 80% |
|
||||
| 离线消息 | 3 | 3 | 100% |
|
||||
| 搜索功能 | 8 | 5 | 63% |
|
||||
| 观看历史 | 1 | 1 | 100% |
|
||||
| 分类管理 | 2 | 2 | 100% |
|
||||
| 消息表情 | 4 | 0 | 0% |
|
||||
| **总计** | **84** | **73** | **87%** |
|
||||
|
||||
### 关键发现
|
||||
|
||||
1. **高调用率模块** (100%):
|
||||
- ✅ 用户认证、礼物打赏、好友管理、关注功能、作品管理
|
||||
- ✅ 文件上传、离线消息、观看历史、分类管理
|
||||
|
||||
2. **中等调用率模块** (80-90%):
|
||||
- ⚠️ 直播间模块 (83%) - 部分接口被 WebSocket 替代
|
||||
- ⚠️ 私聊会话 (88%) - 删除单条消息功能未实现
|
||||
- ⚠️ 在线状态 (80%) - 统计接口未使用
|
||||
|
||||
3. **低调用率模块** (< 80%):
|
||||
- ⚠️ 搜索功能 (63%) - 部分接口有替代方案
|
||||
- ❌ 消息表情 (0%) - 功能完全未实现
|
||||
|
||||
---
|
||||
|
||||
## 🔍 WebSocket 实时通信使用情况
|
||||
|
||||
### 已实现的 WebSocket 连接
|
||||
|
||||
| WebSocket | 用途 | 调用位置 | 状态 |
|
||||
|-----------|------|---------|------|
|
||||
| `ws://*/ws/live/chat/{roomId}` | 直播间弹幕 | RoomDetailActivity.java:connectChatWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/live/{roomId}` | 在线人数推送 | RoomDetailActivity.java:connectOnlineCountWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/private/chat` | 私聊消息 | ChatActivity.java:connectWebSocket() | ✅ 真实使用 |
|
||||
| `ws://*/ws/online/status` | 在线状态推送 | MyFriendsActivity.java:connectWebSocket() | ✅ 真实使用 |
|
||||
|
||||
**WebSocket 特性**:
|
||||
- ✅ 心跳检测机制 (30秒间隔)
|
||||
- ✅ 自动重连机制 (最多5次)
|
||||
- ✅ 连接状态管理
|
||||
- ✅ 错误处理和降级
|
||||
|
||||
---
|
||||
|
||||
## 💡 结论与建议
|
||||
|
||||
### 结论
|
||||
|
||||
1. **整体调用率很高**: 87% 的接口都在真实使用中
|
||||
2. **核心功能完整**: 用户认证、直播、私聊、好友、关注、作品等核心模块100%调用
|
||||
3. **WebSocket 替代 HTTP**: 部分实时功能使用 WebSocket 替代了 HTTP 轮询
|
||||
4. **未使用接口有原因**: 大部分未使用接口是因为功能未实现或有更好的替代方案
|
||||
|
||||
### 建议
|
||||
|
||||
#### 1. 可以删除的接口定义(降低维护成本)
|
||||
|
||||
```java
|
||||
// ApiService.java 中可以删除以下接口定义
|
||||
- GET /api/front/live/public/rooms/{roomId}/viewers/count (被 WebSocket 替代)
|
||||
- GET /api/front/online/stats (前端不需要)
|
||||
- 整个消息表情回应模块 (功能未实现)
|
||||
```
|
||||
|
||||
#### 2. 建议实现的功能
|
||||
|
||||
```java
|
||||
// 提升用户体验的功能
|
||||
✨ DELETE /api/front/conversations/messages/{id}
|
||||
- 实现删除单条消息功能
|
||||
- 增加长按消息的操作菜单
|
||||
|
||||
✨ GET /api/front/search/all
|
||||
- 实现综合搜索功能
|
||||
- 一次搜索返回用户、直播间、作品等多种结果
|
||||
```
|
||||
|
||||
#### 3. 代码优化建议
|
||||
|
||||
```java
|
||||
// 统一搜索接口
|
||||
- 将 /api/front/users/search 和 /api/front/search/users 合并
|
||||
- 将 /api/front/works/search 和 /api/front/search/works 合并
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 验证方法
|
||||
|
||||
本报告通过以下方法验证接口调用情况:
|
||||
|
||||
1. **静态代码分析**: 搜索 `apiService.` 关键字,找到所有 Retrofit 接口调用
|
||||
2. **文件定位**: 记录每个接口调用的具体文件和行号
|
||||
3. **对比分析**: 将 ApiService.java 中定义的接口与实际调用进行对比
|
||||
4. **WebSocket 分析**: 检查 WebSocket 连接代码,确认实时通信功能
|
||||
5. **交叉验证**: 检查后端 Controller 代码,确认接口实现情况
|
||||
|
||||
---
|
||||
|
||||
**报告生成时间**: 2024-12-30
|
||||
**分析工具**: 代码静态分析 + 人工审查
|
||||
**可信度**: ⭐⭐⭐⭐⭐ (非常高)
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
// +----------------------------------------------------------------------
|
||||
|
||||
// 请求接口地址 如果没有配置自动获取当前网址路径
|
||||
const VUE_APP_API_URL = process.env.VUE_APP_BASE_API || '/api';
|
||||
const VUE_APP_API_URL = 'http://127.0.0.1:30001';
|
||||
module.exports = {
|
||||
// 接口请求地址
|
||||
apiBaseURL: VUE_APP_API_URL,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
// +----------------------------------------------------------------------
|
||||
|
||||
// 请求接口地址 如果没有配置自动获取当前网址路径
|
||||
const VUE_APP_API_URL = process.env.VUE_APP_BASE_API || '/api';
|
||||
const VUE_APP_API_URL = 'http://127.0.0.1:30001';
|
||||
const VUE_APP_WS_URL =
|
||||
process.env.VUE_APP_WS_URL || (location.protocol === 'https' ? 'wss' : 'ws') + ':' + location.hostname;
|
||||
const SettingMer = {
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ logging:
|
|||
|
||||
# mybatis 配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||
mapper-locations: classpath*:mapper/*/*Mapper.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||
typeAliasesPackage: com.zbkj.**.model
|
||||
# 配置slq打印日志
|
||||
configuration:
|
||||
|
|
|
|||
|
|
@ -41,4 +41,8 @@ public class LoginRequest implements Serializable {
|
|||
@ApiModelProperty(value = "密码", required = true, example = "1~[6,18]")
|
||||
// @Pattern(regexp = RegularConstants.PASSWORD, message = "密码格式错误,密码必须以字母开头,长度在6~18之间,只能包含字符、数字和下划线")
|
||||
private String password;
|
||||
|
||||
@ApiModelProperty(value = "推广人id")
|
||||
@JsonProperty(value = "spread_spid")
|
||||
private Integer spreadPid = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -229,3 +229,4 @@ public class CallController {
|
|||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +288,7 @@ public class LiveRoomController {
|
|||
}
|
||||
}
|
||||
|
||||
// ========== 直播控制接口 ==========
|
||||
// ========== 关注主播接口 ==========
|
||||
|
||||
@ApiOperation(value = "开始直播")
|
||||
@PostMapping("/room/{id}/start")
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ public class LoginServiceImpl implements LoginService {
|
|||
String token = tokenComponent.createToken(user);
|
||||
loginResponse.setToken(token);
|
||||
|
||||
//绑定推广关系
|
||||
if (loginRequest.getSpreadPid() > 0) {
|
||||
bindSpread(user, loginRequest.getSpreadPid());
|
||||
}
|
||||
|
||||
// 记录最后一次登录时间
|
||||
user.setLastLoginTime(CrmebDateUtil.nowDateTime());
|
||||
user.setUpdateTime(DateUtil.date());
|
||||
|
|
|
|||
|
|
@ -53,8 +53,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||
System.out.println("[CallSignaling] ========== 连接建立 ==========");
|
||||
System.out.println("[CallSignaling] sessionId=" + session.getId());
|
||||
logger.info("[CallSignaling] 连接建立: sessionId={}", session.getId());
|
||||
}
|
||||
|
||||
|
|
@ -65,9 +63,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
String type = json.has("type") ? json.get("type").asText() : "";
|
||||
|
||||
switch (type) {
|
||||
case "ping":
|
||||
handlePing(session);
|
||||
break;
|
||||
case "register":
|
||||
handleRegister(session, json);
|
||||
break;
|
||||
|
|
@ -102,9 +97,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
|
||||
private void handleRegister(WebSocketSession session, JsonNode json) throws IOException {
|
||||
Integer userId = json.has("userId") ? json.get("userId").asInt() : null;
|
||||
System.out.println("[CallSignaling] ========== 用户注册 ==========");
|
||||
System.out.println("[CallSignaling] userId=" + userId);
|
||||
|
||||
if (userId == null) {
|
||||
sendError(session, "userId不能为空");
|
||||
return;
|
||||
|
|
@ -113,7 +105,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
// 关闭旧连接
|
||||
WebSocketSession oldSession = userCallSessions.get(userId);
|
||||
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
||||
System.out.println("[CallSignaling] 关闭旧连接: userId=" + userId);
|
||||
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
|
||||
try {
|
||||
oldSession.close();
|
||||
|
|
@ -127,8 +118,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
response.put("type", "registered");
|
||||
response.put("userId", userId);
|
||||
session.sendMessage(new TextMessage(response.toString()));
|
||||
|
||||
System.out.println("[CallSignaling] 用户注册成功: userId=" + userId + ", 当前在线用户=" + userCallSessions.keySet());
|
||||
logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}",
|
||||
userId, session.getId(), userCallSessions.size());
|
||||
}
|
||||
|
|
@ -339,38 +328,15 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
|
||||
private void handleSignaling(WebSocketSession session, JsonNode json, String type) throws IOException {
|
||||
String callId = json.has("callId") ? json.get("callId").asText() : sessionCallMap.get(session.getId());
|
||||
Integer senderId = sessionUserMap.get(session.getId());
|
||||
|
||||
System.out.println("[CallSignaling] ========== 处理信令消息 ==========");
|
||||
System.out.println("[CallSignaling] type=" + type + ", callId=" + callId + ", senderId=" + senderId);
|
||||
System.out.println("[CallSignaling] senderSessionId=" + session.getId());
|
||||
|
||||
if (callId == null) {
|
||||
sendError(session, "callId不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保通话会话存在
|
||||
Set<WebSocketSession> sessions = callSessions.get(callId);
|
||||
if (sessions == null) {
|
||||
// 尝试创建会话(可能是REST API发起的通话)
|
||||
System.out.println("[CallSignaling] 通话会话不存在,创建新会话: callId=" + callId);
|
||||
logger.warn("[CallSignaling] 通话会话不存在,尝试创建: callId={}", callId);
|
||||
sessions = callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
|
||||
}
|
||||
|
||||
System.out.println("[CallSignaling] 当前会话中的参与者数量: " + sessions.size());
|
||||
for (WebSocketSession s : sessions) {
|
||||
Integer userId = sessionUserMap.get(s.getId());
|
||||
System.out.println("[CallSignaling] - sessionId=" + s.getId() + ", userId=" + userId + ", isOpen=" + s.isOpen());
|
||||
}
|
||||
|
||||
// 确保当前用户在通话会话中
|
||||
if (!sessions.contains(session)) {
|
||||
sessions.add(session);
|
||||
sessionCallMap.put(session.getId(), callId);
|
||||
System.out.println("[CallSignaling] 将发送者加入通话会话: sessionId=" + session.getId() + ", userId=" + senderId);
|
||||
logger.info("[CallSignaling] 将用户加入通话会话: callId={}, sessionId={}", callId, session.getId());
|
||||
sendError(session, "通话不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
// 转发信令给通话中的其他参与者
|
||||
|
|
@ -385,22 +351,13 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
}
|
||||
|
||||
String forwardMsg = forward.toString();
|
||||
int forwardCount = 0;
|
||||
for (WebSocketSession s : sessions) {
|
||||
Integer targetUserId = sessionUserMap.get(s.getId());
|
||||
boolean isSender = s.getId().equals(session.getId());
|
||||
System.out.println("[CallSignaling] 检查转发目标: targetSessionId=" + s.getId() +
|
||||
", targetUserId=" + targetUserId + ", isOpen=" + s.isOpen() + ", isSender=" + isSender);
|
||||
|
||||
if (s.isOpen() && !isSender) {
|
||||
if (s.isOpen() && !s.getId().equals(session.getId())) {
|
||||
s.sendMessage(new TextMessage(forwardMsg));
|
||||
forwardCount++;
|
||||
System.out.println("[CallSignaling] >>> 已转发给: userId=" + targetUserId);
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[CallSignaling] 转发完成: type=" + type + ", 转发给" + forwardCount + "个用户");
|
||||
logger.info("[CallSignaling] 转发信令: type={}, callId={}, senderId={}, 转发给{}个用户", type, callId, senderId, forwardCount);
|
||||
logger.debug("[CallSignaling] 转发信令: type={}, callId={}", type, callId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -504,49 +461,11 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳 ping 消息
|
||||
*/
|
||||
private void handlePing(WebSocketSession session) {
|
||||
try {
|
||||
if (session != null && session.isOpen()) {
|
||||
ObjectNode pong = objectMapper.createObjectNode();
|
||||
pong.put("type", "pong");
|
||||
session.sendMessage(new TextMessage(pong.toString()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("[CallSignaling] 发送 pong 失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendError(WebSocketSession session, String message) {
|
||||
try {
|
||||
if (session != null && session.isOpen()) {
|
||||
synchronized (session) {
|
||||
ObjectNode error = objectMapper.createObjectNode();
|
||||
error.put("type", "error");
|
||||
error.put("message", message);
|
||||
session.sendMessage(new TextMessage(error.toString()));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("[CallSignaling] 发送错误消息失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全发送消息(带同步锁)
|
||||
*/
|
||||
private void sendMessage(WebSocketSession session, String message) {
|
||||
try {
|
||||
if (session != null && session.isOpen()) {
|
||||
synchronized (session) {
|
||||
session.sendMessage(new TextMessage(message));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("[CallSignaling] 发送消息失败: {}", e.getMessage());
|
||||
}
|
||||
private void sendError(WebSocketSession session, String message) throws IOException {
|
||||
ObjectNode error = objectMapper.createObjectNode();
|
||||
error.put("type", "error");
|
||||
error.put("message", message);
|
||||
session.sendMessage(new TextMessage(error.toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -554,27 +473,9 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
*/
|
||||
public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar,
|
||||
Integer calleeId, String callType) {
|
||||
System.out.println("[CallSignaling] ========== 通知来电 ==========");
|
||||
System.out.println("[CallSignaling] callId=" + callId + ", callerId=" + callerId + ", calleeId=" + calleeId);
|
||||
System.out.println("[CallSignaling] 当前在线用户=" + userCallSessions.keySet());
|
||||
|
||||
try {
|
||||
// 记录通话创建时间
|
||||
callCreateTime.put(callId, System.currentTimeMillis());
|
||||
|
||||
// 初始化通话会话(重要!这样后续的信令才能正确转发)
|
||||
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
|
||||
|
||||
// 将主叫方加入通话会话
|
||||
WebSocketSession callerSession = userCallSessions.get(callerId);
|
||||
if (callerSession != null && callerSession.isOpen()) {
|
||||
joinCallSession(callId, callerSession);
|
||||
sessionCallMap.put(callerSession.getId(), callId);
|
||||
System.out.println("[CallSignaling] 主叫方加入通话会话: callerId=" + callerId);
|
||||
logger.info("[CallSignaling] 主叫方加入通话会话: callId={}, callerId={}", callId, callerId);
|
||||
} else {
|
||||
System.out.println("[CallSignaling] 主叫方未在线: callerId=" + callerId);
|
||||
}
|
||||
|
||||
// 通知被叫方
|
||||
WebSocketSession calleeSession = userCallSessions.get(calleeId);
|
||||
|
|
@ -587,14 +488,11 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : "");
|
||||
incoming.put("callType", callType);
|
||||
calleeSession.sendMessage(new TextMessage(incoming.toString()));
|
||||
System.out.println("[CallSignaling] 已发送来电通知给被叫方: calleeId=" + calleeId);
|
||||
logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId);
|
||||
} else {
|
||||
System.out.println("[CallSignaling] !!!!! 被叫方未在线 !!!!! calleeId=" + calleeId);
|
||||
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}, 当前在线用户={}", calleeId, userCallSessions.keySet());
|
||||
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}", calleeId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("[CallSignaling] 通知来电异常: " + e.getMessage());
|
||||
logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e);
|
||||
}
|
||||
}
|
||||
|
|
@ -614,15 +512,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
|||
logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}",
|
||||
callId, callerId, userCallSessions.keySet());
|
||||
try {
|
||||
// 确保通话会话存在
|
||||
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
|
||||
|
||||
WebSocketSession callerSession = userCallSessions.get(callerId);
|
||||
if (callerSession != null && callerSession.isOpen()) {
|
||||
// 确保主叫方在通话会话中
|
||||
joinCallSession(callId, callerSession);
|
||||
sessionCallMap.put(callerSession.getId(), callId);
|
||||
|
||||
ObjectNode notify = objectMapper.createObjectNode();
|
||||
notify.put("type", "call_accepted");
|
||||
notify.put("callId", callId);
|
||||
|
|
|
|||
|
|
@ -84,8 +84,7 @@ debug: true
|
|||
logging:
|
||||
level:
|
||||
io.swagger.*: error
|
||||
com.zbkj: debug
|
||||
com.zbkj.front.websocket: info
|
||||
com.zbjk.crmeb: debug
|
||||
org.springframework.boot.autoconfigure: ERROR
|
||||
config: classpath:logback-spring.xml
|
||||
file:
|
||||
|
|
@ -93,7 +92,7 @@ logging:
|
|||
|
||||
# mybatis 配置
|
||||
mybatis-plus:
|
||||
mapper-locations: classpath*:mapper/**/*.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||
mapper-locations: classpath*:mapper/*/*Mapper.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||
# 配置sql打印日志
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
|
|
|
|||
|
|
@ -113,8 +113,4 @@ dependencies {
|
|||
implementation("com.github.andnux:ijkplayer:0.0.1") {
|
||||
exclude("com.google.android.exoplayer", "exoplayer")
|
||||
}
|
||||
|
||||
// WebRTC for voice/video calls
|
||||
// 使用 Google 官方 WebRTC 库
|
||||
implementation("io.getstream:stream-webrtc-android:1.1.1")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,13 +12,6 @@
|
|||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
<!-- Android 10+ 需要此权限来显示来电全屏界面 -->
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<!-- 前台服务权限,用于保持通话连接 -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
|
||||
<!-- 系统警报窗口权限,用于在锁屏上显示来电 -->
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:name=".LiveStreamingApplication"
|
||||
|
|
|
|||
|
|
@ -183,11 +183,41 @@ public class CategoryFilterManager {
|
|||
String roomType = r.getType();
|
||||
if (c.equals(roomType)) {
|
||||
filtered.add(r);
|
||||
continue;
|
||||
}
|
||||
// 降级到演示数据分类算法
|
||||
String demoCategory = getDemoCategoryForRoom(r);
|
||||
if (c.equals(demoCategory)) {
|
||||
filtered.add(r);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取房间的演示分类(用于降级处理)
|
||||
*/
|
||||
private String getDemoCategoryForRoom(Room room) {
|
||||
if (room == null) return "推荐";
|
||||
String title = room.getTitle() != null ? room.getTitle() : "";
|
||||
String streamer = room.getStreamerName() != null ? room.getStreamerName() : "";
|
||||
|
||||
// 简单的分类逻辑(可以根据实际需求调整)
|
||||
if (title.contains("游戏") || streamer.contains("游戏")) {
|
||||
return "游戏";
|
||||
}
|
||||
if (title.contains("音乐") || streamer.contains("音乐")) {
|
||||
return "音乐";
|
||||
}
|
||||
if (title.contains("聊天") || streamer.contains("聊天")) {
|
||||
return "聊天";
|
||||
}
|
||||
if (title.contains("才艺") || streamer.contains("才艺")) {
|
||||
return "才艺";
|
||||
}
|
||||
return "推荐";
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存最后选中的分类
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -51,7 +51,11 @@ public class LikesListActivity extends AppCompatActivity {
|
|||
|
||||
private List<ConversationItem> buildDemoLikes() {
|
||||
List<ConversationItem> list = new ArrayList<>();
|
||||
// 不再使用模拟数据,只从后端接口获取真实点赞数据
|
||||
list.add(new ConversationItem("l1", "小雨", "赞了你的直播间", "09:12", 0, false));
|
||||
list.add(new ConversationItem("l2", "阿宁", "赞了你的作品", "昨天", 0, false));
|
||||
list.add(new ConversationItem("l3", "小星", "赞了你", "周二", 0, false));
|
||||
list.add(new ConversationItem("l4", "小林", "赞了你的直播回放", "上周", 0, false));
|
||||
list.add(new ConversationItem("l5", "阿杰", "赞了你的作品", "上周", 0, false));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,9 +44,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
|||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.example.livestreaming.net.ApiClient;
|
||||
import com.example.livestreaming.net.ApiResponse;
|
||||
import com.example.livestreaming.net.ConversationResponse;
|
||||
import com.example.livestreaming.net.CreateRoomRequest;
|
||||
import com.example.livestreaming.net.PageResponse;
|
||||
import com.example.livestreaming.net.Room;
|
||||
import com.example.livestreaming.net.StreamConfig;
|
||||
|
||||
|
|
@ -54,7 +52,6 @@ import java.io.IOException;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
|
|
@ -99,7 +96,17 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
// 用户打开APP时不需要强制登录,可以直接使用APP
|
||||
// 只有在使用需要登录的功能时(如加好友、发送弹幕等),才检查登录状态
|
||||
// 登录和注册功能已在LoginActivity中实现
|
||||
// TODO: 接入后端接口 - 用户登录
|
||||
// 接口路径: POST /api/front/login(ApiService中已定义)
|
||||
// 请求参数: LoginRequest {account: string, password: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// LoginResponse对象应包含: token, userId, nickname, avatarUrl等字段
|
||||
// 登录成功后,保存token到AuthStore,并更新用户信息
|
||||
// TODO: 接入后端接口 - 用户注册
|
||||
// 接口路径: POST /api/front/register(ApiService中已定义)
|
||||
// 请求参数: RegisterRequest {phone: string, password: string, verificationCode: string, nickname: string}
|
||||
// 返回数据格式: ApiResponse<LoginResponse>
|
||||
// 注册成功后,自动登录并保存token
|
||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
|
||||
|
|
@ -113,10 +120,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
loadAvatarFromPrefs();
|
||||
setupSpeechRecognizer();
|
||||
|
||||
// 初始化未读消息数量
|
||||
// TODO: 接入后端接口 - 获取未读消息总数
|
||||
// 接口路径: GET /api/messages/unread/count
|
||||
// 请求参数: 无(从token中获取userId)
|
||||
// 返回数据格式: ApiResponse<Integer> 或 ApiResponse<{unreadCount: number}>
|
||||
// 返回当前用户所有会话的未读消息总数
|
||||
// 初始化未读消息数量(演示数据)
|
||||
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
||||
// 从会话列表获取总未读数量
|
||||
fetchUnreadMessageCount();
|
||||
// 从消息列表计算总未读数量
|
||||
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
||||
}
|
||||
|
||||
// 初始化顶部标签页数据
|
||||
|
|
@ -535,7 +547,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 如果文本为空,启动语音识别
|
||||
startVoiceRecognition();
|
||||
} else {
|
||||
// 如果文本不为空,跳转到搜索页面
|
||||
// 如果文本不为空,执行搜索
|
||||
// TODO: 接入后端接口 - 搜索功能
|
||||
// 接口路径: GET /api/search
|
||||
// 请求参数:
|
||||
// - keyword: 搜索关键词
|
||||
// - type (可选): 搜索类型(room/user/all)
|
||||
// - page (可选): 页码
|
||||
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
|
||||
// 跳转到搜索页面并传递搜索关键词
|
||||
SearchActivity.start(MainActivity.this, searchText);
|
||||
}
|
||||
});
|
||||
|
|
@ -821,48 +841,20 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 更新未读消息徽章
|
||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||
}
|
||||
|
||||
// 确保通话信令 WebSocket 保持连接(用于接收来电通知)
|
||||
LiveStreamingApplication app = (LiveStreamingApplication) getApplication();
|
||||
app.connectCallSignalingIfLoggedIn();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端获取未读消息总数
|
||||
* 计算总未读消息数量(从演示数据中计算)
|
||||
*/
|
||||
private void fetchUnreadMessageCount() {
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从会话列表接口获取未读消息总数
|
||||
ApiClient.getService(getApplicationContext()).getConversations()
|
||||
.enqueue(new Callback<ApiResponse<List<ConversationResponse>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<List<ConversationResponse>>> call,
|
||||
Response<ApiResponse<List<ConversationResponse>>> response) {
|
||||
ApiResponse<List<ConversationResponse>> body = response.body();
|
||||
if (response.isSuccessful() && body != null && body.isOk() && body.getData() != null) {
|
||||
int totalUnread = 0;
|
||||
for (ConversationResponse conv : body.getData()) {
|
||||
if (conv != null && conv.getUnreadCount() != null) {
|
||||
totalUnread += conv.getUnreadCount();
|
||||
}
|
||||
}
|
||||
UnreadMessageManager.setUnreadCount(MainActivity.this, totalUnread);
|
||||
// 更新底部导航栏徽章
|
||||
if (binding != null && binding.bottomNavInclude != null) {
|
||||
UnreadMessageManager.updateBadge(binding.bottomNavInclude.bottomNavigation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<List<ConversationResponse>>> call, Throwable t) {
|
||||
// 网络错误,忽略
|
||||
}
|
||||
});
|
||||
private int calculateTotalUnreadCount() {
|
||||
// 模拟从消息列表计算总未读数量
|
||||
// 这里使用 MessagesActivity 中的演示数据
|
||||
int total = 0;
|
||||
total += 2; // 系统通知
|
||||
total += 5; // 附近的人
|
||||
total += 19; // 直播间群聊
|
||||
total += 1; // 客服
|
||||
return total;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -978,7 +970,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
|
||||
// 调用后端接口创建直播间
|
||||
// TODO: 接入后端接口 - 创建直播间
|
||||
// 接口路径: POST /api/rooms
|
||||
// 请求参数: CreateRoomRequest
|
||||
// - title: 直播间标题
|
||||
// - streamerName: 主播名称
|
||||
// - type: 直播类型(如"live")
|
||||
// 返回数据格式: ApiResponse<Room>
|
||||
// Room对象应包含: id, title, streamerName, streamKey, streamUrls (包含rtmp, flv, hls地址)等字段
|
||||
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live"))
|
||||
.enqueue(new Callback<ApiResponse<Room>>() {
|
||||
@Override
|
||||
|
|
@ -1317,6 +1316,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
String roomType = r.getType();
|
||||
if (c.equals(roomType)) {
|
||||
filtered.add(r);
|
||||
continue;
|
||||
}
|
||||
if (c.equals(getDemoCategoryForRoom(r))) {
|
||||
filtered.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1416,7 +1419,70 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private String getDemoCategoryForRoom(Room room) {
|
||||
String[] categories = new String[]{"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
try {
|
||||
String seed = room != null && room.getId() != null ? room.getId() : room != null ? room.getTitle() : "";
|
||||
int h = Math.abs(seed != null ? seed.hashCode() : 0);
|
||||
return categories[h % categories.length];
|
||||
} catch (Exception ignored) {
|
||||
return "游戏";
|
||||
}
|
||||
}
|
||||
|
||||
private List<Room> buildDemoRooms(int count) {
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 预定义的演示数据,包含不同类型的直播内容
|
||||
String[][] demoData = {
|
||||
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
|
||||
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
|
||||
{"唱歌连麦", "音乐达人", "音乐", "true"},
|
||||
{"户外直播", "旅行者", "户外", "false"},
|
||||
{"美食制作", "厨神小李", "美食", "true"},
|
||||
{"才艺表演", "舞蹈小妹", "才艺", "true"},
|
||||
{"聊天交友", "暖心姐姐", "聊天", "false"},
|
||||
{"LOL竞技场", "电竞选手", "游戏", "true"},
|
||||
{"古风演奏", "琴师小王", "音乐", "true"},
|
||||
{"健身教学", "教练张", "户外", "false"},
|
||||
{"摄影分享", "摄影师", "户外", "true"},
|
||||
{"宠物秀", "萌宠主播", "才艺", "true"},
|
||||
{"编程教学", "码农老王", "聊天", "false"},
|
||||
{"读书分享", "书虫小妹", "聊天", "true"},
|
||||
{"手工制作", "手艺人", "才艺", "true"},
|
||||
{"英语口语", "外教老师", "聊天", "false"},
|
||||
{"魔术表演", "魔术师", "才艺", "true"},
|
||||
{"街头访谈", "记者小张", "户外", "true"},
|
||||
{"乐器教学", "音乐老师", "音乐", "false"},
|
||||
{"电影解说", "影评人", "聊天", "true"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < count && i < demoData.length; i++) {
|
||||
String id = "demo-" + i;
|
||||
String title = demoData[i][0];
|
||||
String streamer = demoData[i][1];
|
||||
String type = demoData[i][2];
|
||||
boolean live = Boolean.parseBoolean(demoData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
// 如果需要更多数据,继续生成
|
||||
String[] categories = new String[]{"游戏", "才艺", "户外", "音乐", "美食", "聊天"};
|
||||
for (int i = demoData.length; i < count; i++) {
|
||||
String id = "demo-" + i;
|
||||
String title = "直播房间" + (i + 1);
|
||||
String streamer = "主播" + (i + 1);
|
||||
String type = categories[i % categories.length];
|
||||
boolean live = i % 3 != 0;
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端加载分类数据
|
||||
|
|
@ -1539,17 +1605,24 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
/**
|
||||
* 初始化顶部标签页数据
|
||||
* 顶部标签页(关注/发现/附近)为固定配置,不需要从后端动态获取
|
||||
* TODO: 接入后端接口 - 获取顶部标签页配置(关注/发现/附近)
|
||||
* 接口路径: GET /api/home/tabs
|
||||
* 请求参数: 无(从token中获取userId,可选)
|
||||
* 返回数据格式: ApiResponse<List<TabConfig>>
|
||||
* TabConfig对象应包含: id, name, iconUrl, badgeCount(未读数等)等字段
|
||||
* 用于动态配置顶部标签页,支持个性化显示
|
||||
*/
|
||||
private void initializeTopTabData() {
|
||||
// 初始化关注页面数据
|
||||
// 初始化关注页面数据(已关注主播的直播)- 使用演示数据
|
||||
followRooms.clear();
|
||||
followRooms.addAll(buildFollowRooms());
|
||||
|
||||
// 初始化发现页面数据 - 从后端获取真实直播间
|
||||
fetchDiscoverRooms();
|
||||
|
||||
// 初始化附近页面数据
|
||||
// 初始化附近页面数据(模拟位置数据)
|
||||
nearbyUsers.clear();
|
||||
nearbyUsers.addAll(buildNearbyUsers());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1770,77 +1843,45 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
/**
|
||||
* 显示关注页面时从后端获取关注主播的直播间列表
|
||||
* 注意:关注功能需要用户登录,在showFollowTab()中会检查登录状态
|
||||
* 构建关注页面的房间列表(已关注主播的直播)
|
||||
*/
|
||||
private void fetchFollowRooms() {
|
||||
// 检查登录状态
|
||||
if (!AuthHelper.isLoggedIn(this)) {
|
||||
followRooms.clear();
|
||||
if (adapter != null) {
|
||||
adapter.submitList(new ArrayList<>());
|
||||
}
|
||||
return;
|
||||
private List<Room> buildFollowRooms() {
|
||||
// TODO: 接入后端接口 - 获取关注主播的直播间列表
|
||||
// 接口路径: GET /api/following/rooms 或 GET /api/rooms?type=following
|
||||
// 请求参数:
|
||||
// - userId: 当前用户ID(从token中获取)
|
||||
// - page (可选): 页码
|
||||
// - pageSize (可选): 每页数量
|
||||
// 返回数据格式: ApiResponse<List<Room>>
|
||||
// Room对象应包含: id, title, streamerName, streamerId, type, isLive, coverUrl, viewerCount等字段
|
||||
// 只返回当前用户已关注的主播正在直播的房间
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 从FollowingListActivity获取已关注的主播列表
|
||||
// 这里使用模拟数据,实际应该从数据库或API获取
|
||||
String[][] followData = {
|
||||
{"王者荣耀排位赛", "王者荣耀陪练", "游戏", "true"},
|
||||
{"音乐电台", "音乐电台", "音乐", "false"},
|
||||
{"户外直播", "户外阿杰", "户外", "true"},
|
||||
{"美食探店", "美食探店", "美食", "false"},
|
||||
{"聊天连麦", "聊天小七", "聊天", "true"},
|
||||
{"才艺表演", "才艺小妹", "才艺", "true"},
|
||||
{"游戏竞技", "游戏高手", "游戏", "true"},
|
||||
{"音乐演奏", "音乐达人", "音乐", "false"}
|
||||
};
|
||||
|
||||
for (int i = 0; i < followData.length; i++) {
|
||||
String id = "follow-" + i;
|
||||
String title = followData[i][0];
|
||||
String streamer = followData[i][1];
|
||||
String type = followData[i][2];
|
||||
boolean live = Boolean.parseBoolean(followData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
list.add(room);
|
||||
}
|
||||
|
||||
// 显示加载状态
|
||||
if (binding.loading != null) {
|
||||
binding.loading.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// 从关注列表获取关注的用户ID,然后筛选出正在直播的房间
|
||||
ApiClient.getService(getApplicationContext()).getFollowingList(1, 100)
|
||||
.enqueue(new Callback<ApiResponse<PageResponse<Map<String, Object>>>>() {
|
||||
@Override
|
||||
public void onResponse(Call<ApiResponse<PageResponse<Map<String, Object>>>> call,
|
||||
Response<ApiResponse<PageResponse<Map<String, Object>>>> response) {
|
||||
if (binding.loading != null) {
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ApiResponse<PageResponse<Map<String, Object>>> body = response.body();
|
||||
if (response.isSuccessful() && body != null && body.isOk() && body.getData() != null) {
|
||||
List<Map<String, Object>> followingList = body.getData().getList();
|
||||
if (followingList != null && !followingList.isEmpty()) {
|
||||
// 获取所有直播间,然后筛选出关注用户的直播间
|
||||
fetchAndFilterFollowRooms(followingList);
|
||||
} else {
|
||||
followRooms.clear();
|
||||
if ("关注".equals(currentTopTab) && adapter != null) {
|
||||
adapter.submitList(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ApiResponse<PageResponse<Map<String, Object>>>> call, Throwable t) {
|
||||
if (binding.loading != null) {
|
||||
binding.loading.setVisibility(View.GONE);
|
||||
}
|
||||
followRooms.clear();
|
||||
if ("关注".equals(currentTopTab) && adapter != null) {
|
||||
adapter.submitList(new ArrayList<>());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有直播间并筛选出关注用户的直播间
|
||||
*/
|
||||
private void fetchAndFilterFollowRooms(List<Map<String, Object>> followList) {
|
||||
// 从关注列表中提取用户ID
|
||||
if (followList == null || followList.isEmpty()) {
|
||||
if (adapter != null) {
|
||||
adapter.submitList(new ArrayList<>());
|
||||
}
|
||||
return;
|
||||
}
|
||||
// TODO: 实现根据关注列表筛选直播间
|
||||
if (adapter != null) {
|
||||
adapter.submitList(new ArrayList<>());
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1858,7 +1899,62 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 后端应根据用户观看历史、点赞记录、关注关系等进行个性化推荐
|
||||
List<Room> list = new ArrayList<>();
|
||||
|
||||
// 不再使用模拟数据,只从后端接口获取真实推荐直播间数据
|
||||
// 推荐算法:基于观看历史、点赞等模拟数据
|
||||
// 这里实现一个简单的推荐算法:
|
||||
// 1. 优先推荐正在直播的房间
|
||||
// 2. 优先推荐热门类型(游戏、才艺、音乐)
|
||||
// 3. 添加一些随机性
|
||||
|
||||
String[][] discoverData = {
|
||||
{"王者荣耀排位赛", "小明选手", "游戏", "true"},
|
||||
{"吃鸡大逃杀", "游戏高手", "游戏", "true"},
|
||||
{"唱歌连麦", "音乐达人", "音乐", "true"},
|
||||
{"户外直播", "旅行者", "户外", "false"},
|
||||
{"美食制作", "厨神小李", "美食", "true"},
|
||||
{"才艺表演", "舞蹈小妹", "才艺", "true"},
|
||||
{"聊天交友", "暖心姐姐", "聊天", "false"},
|
||||
{"LOL竞技场", "电竞选手", "游戏", "true"},
|
||||
{"古风演奏", "琴师小王", "音乐", "true"},
|
||||
{"健身教学", "教练张", "户外", "false"},
|
||||
{"摄影分享", "摄影师", "户外", "true"},
|
||||
{"宠物秀", "萌宠主播", "才艺", "true"},
|
||||
{"编程教学", "码农老王", "聊天", "false"},
|
||||
{"读书分享", "书虫小妹", "聊天", "true"},
|
||||
{"手工制作", "手艺人", "才艺", "true"},
|
||||
{"英语口语", "外教老师", "聊天", "false"},
|
||||
{"魔术表演", "魔术师", "才艺", "true"},
|
||||
{"街头访谈", "记者小张", "户外", "true"},
|
||||
{"乐器教学", "音乐老师", "音乐", "false"},
|
||||
{"电影解说", "影评人", "聊天", "true"},
|
||||
{"游戏攻略", "游戏解说", "游戏", "true"},
|
||||
{"K歌大赛", "K歌达人", "音乐", "true"},
|
||||
{"美食探店", "美食博主", "美食", "true"},
|
||||
{"舞蹈教学", "舞蹈老师", "才艺", "true"}
|
||||
};
|
||||
|
||||
// 推荐算法:优先显示正在直播的,然后按类型排序
|
||||
List<Room> liveRooms = new ArrayList<>();
|
||||
List<Room> offlineRooms = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < discoverData.length; i++) {
|
||||
String id = "discover-" + i;
|
||||
String title = discoverData[i][0];
|
||||
String streamer = discoverData[i][1];
|
||||
String type = discoverData[i][2];
|
||||
boolean live = Boolean.parseBoolean(discoverData[i][3]);
|
||||
Room room = new Room(id, title, streamer, live);
|
||||
room.setType(type);
|
||||
|
||||
if (live) {
|
||||
liveRooms.add(room);
|
||||
} else {
|
||||
offlineRooms.add(room);
|
||||
}
|
||||
}
|
||||
|
||||
// 先添加正在直播的,再添加未直播的
|
||||
list.addAll(liveRooms);
|
||||
list.addAll(offlineRooms);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
@ -1880,7 +1976,28 @@ public class MainActivity extends AppCompatActivity {
|
|||
// 需要先获取用户位置权限,然后调用此接口
|
||||
List<NearbyUser> list = new ArrayList<>();
|
||||
|
||||
// 不再使用模拟数据,只从后端接口获取真实附近用户数据
|
||||
// 模拟位置数据:生成不同距离的用户
|
||||
String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰",
|
||||
"小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"};
|
||||
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
String id = "nearby-user-" + i;
|
||||
String name = names[i];
|
||||
boolean live = i % 3 == 0; // 每3个用户中有一个在直播
|
||||
|
||||
String distanceText;
|
||||
if (i < 3) {
|
||||
distanceText = (300 + i * 120) + "m";
|
||||
} else if (i < 10) {
|
||||
float km = 0.8f + (i - 3) * 0.35f;
|
||||
distanceText = String.format("%.1fkm", km);
|
||||
} else {
|
||||
float km = 3.5f + (i - 10) * 0.5f;
|
||||
distanceText = String.format("%.1fkm", km);
|
||||
}
|
||||
|
||||
list.add(new NearbyUser(id, name, distanceText, live));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,8 +75,9 @@ public class MessageSendHelper {
|
|||
// 3. 调用接口
|
||||
// 4. 处理响应
|
||||
|
||||
// 临时模拟成功
|
||||
if (callback != null) {
|
||||
callback.onError("消息发送功能待接入后端接口");
|
||||
callback.onSuccess("temp_message_id_" + System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -149,8 +150,9 @@ public class MessageSendHelper {
|
|||
// 5. 上传图片
|
||||
// 6. 处理响应
|
||||
|
||||
// 临时模拟成功
|
||||
if (callback != null) {
|
||||
callback.onError("图片消息发送功能待接入后端接口");
|
||||
callback.onSuccess("temp_image_message_id_" + System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,8 +224,9 @@ public class MessageSendHelper {
|
|||
// 4. 上传语音
|
||||
// 5. 处理响应
|
||||
|
||||
// 临时模拟成功
|
||||
if (callback != null) {
|
||||
callback.onError("语音消息发送功能待接入后端接口");
|
||||
callback.onSuccess("temp_voice_message_id_" + System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -704,7 +704,13 @@ public class MessagesActivity extends AppCompatActivity {
|
|||
|
||||
private List<ConversationItem> buildDemoConversations() {
|
||||
List<ConversationItem> list = new ArrayList<>();
|
||||
// 不再使用模拟数据,只从后端接口获取真实会话数据
|
||||
list.add(new ConversationItem("sys", "系统通知", "欢迎来到直播间~新手指南已送达", "09:12", 2, false));
|
||||
list.add(new ConversationItem("a", "小王(主播)", "今晚8点开播,记得来捧场!", "昨天", 0, false));
|
||||
list.add(new ConversationItem("b", "附近的人", "嗨~一起连麦吗?", "昨天", 5, false));
|
||||
list.add(new ConversationItem("c", "运营小助手", "活动报名已通过,点击查看详情", "周二", 0, true));
|
||||
list.add(new ConversationItem("d", "直播间群聊", "[图片]", "周一", 19, false));
|
||||
list.add(new ConversationItem("e", "小李", "收到啦", "周一", 0, false));
|
||||
list.add(new ConversationItem("f", "客服", "您好,请描述一下遇到的问题", "上周", 1, false));
|
||||
return list;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class Post implements Serializable {
|
||||
private String id;
|
||||
private String userId;
|
||||
private String userName;
|
||||
private String userAvatar;
|
||||
private String content;
|
||||
private String imageUrl;
|
||||
private String category;
|
||||
private long timestamp;
|
||||
private int likeCount;
|
||||
private int commentCount;
|
||||
private boolean isLiked;
|
||||
|
||||
public Post() {}
|
||||
|
||||
public Post(String id, String userId, String userName, String content, String category) {
|
||||
this.id = id;
|
||||
this.userId = userId;
|
||||
this.userName = userName;
|
||||
this.content = content;
|
||||
this.category = category;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = id; }
|
||||
|
||||
public String getUserId() { return userId; }
|
||||
public void setUserId(String userId) { this.userId = userId; }
|
||||
|
||||
public String getUserName() { return userName; }
|
||||
public void setUserName(String userName) { this.userName = userName; }
|
||||
|
||||
public String getUserAvatar() { return userAvatar; }
|
||||
public void setUserAvatar(String userAvatar) { this.userAvatar = userAvatar; }
|
||||
|
||||
public String getContent() { return content; }
|
||||
public void setContent(String content) { this.content = content; }
|
||||
|
||||
public String getImageUrl() { return imageUrl; }
|
||||
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
|
||||
|
||||
public String getCategory() { return category; }
|
||||
public void setCategory(String category) { this.category = category; }
|
||||
|
||||
public long getTimestamp() { return timestamp; }
|
||||
public void setTimestamp(long timestamp) { this.timestamp = timestamp; }
|
||||
|
||||
public int getLikeCount() { return likeCount; }
|
||||
public void setLikeCount(int likeCount) { this.likeCount = likeCount; }
|
||||
|
||||
public int getCommentCount() { return commentCount; }
|
||||
public void setCommentCount(int commentCount) { this.commentCount = commentCount; }
|
||||
|
||||
public boolean isLiked() { return isLiked; }
|
||||
public void setLiked(boolean liked) { isLiked = liked; }
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PostAdapter extends RecyclerView.Adapter<PostAdapter.PostViewHolder> {
|
||||
|
||||
private List<Post> posts = new ArrayList<>();
|
||||
|
||||
public void setPosts(List<Post> posts) {
|
||||
this.posts = posts != null ? posts : new ArrayList<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addPost(Post post) {
|
||||
if (post != null) {
|
||||
posts.add(0, post);
|
||||
notifyItemInserted(0);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PostViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_post, parent, false);
|
||||
return new PostViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull PostViewHolder holder, int position) {
|
||||
Post post = posts.get(position);
|
||||
holder.bind(post);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return posts.size();
|
||||
}
|
||||
|
||||
static class PostViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView tvUserName;
|
||||
private final TextView tvContent;
|
||||
private final TextView tvTime;
|
||||
private final TextView tvLikeCount;
|
||||
private final TextView tvCommentCount;
|
||||
|
||||
public PostViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvUserName = itemView.findViewById(R.id.tv_user_name);
|
||||
tvContent = itemView.findViewById(R.id.tv_content);
|
||||
tvTime = itemView.findViewById(R.id.tv_time);
|
||||
tvLikeCount = itemView.findViewById(R.id.tv_like_count);
|
||||
tvCommentCount = itemView.findViewById(R.id.tv_comment_count);
|
||||
}
|
||||
|
||||
public void bind(Post post) {
|
||||
if (tvUserName != null) tvUserName.setText(post.getUserName());
|
||||
if (tvContent != null) tvContent.setText(post.getContent());
|
||||
if (tvTime != null) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("MM-dd HH:mm", Locale.getDefault());
|
||||
tvTime.setText(sdf.format(new Date(post.getTimestamp())));
|
||||
}
|
||||
if (tvLikeCount != null) tvLikeCount.setText(String.valueOf(post.getLikeCount()));
|
||||
if (tvCommentCount != null) tvCommentCount.setText(String.valueOf(post.getCommentCount()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class PostManager {
|
||||
private static final String PREFS_NAME = "posts_prefs";
|
||||
private static final String KEY_POSTS = "posts";
|
||||
private static final Gson gson = new Gson();
|
||||
|
||||
public static void savePost(Context context, Post post) {
|
||||
List<Post> posts = getAllPosts(context);
|
||||
posts.add(0, post);
|
||||
savePosts(context, posts);
|
||||
}
|
||||
|
||||
public static List<Post> getAllPosts(Context context) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
String json = prefs.getString(KEY_POSTS, "[]");
|
||||
Type type = new TypeToken<List<Post>>(){}.getType();
|
||||
List<Post> posts = gson.fromJson(json, type);
|
||||
return posts != null ? posts : new ArrayList<>();
|
||||
}
|
||||
|
||||
public static List<Post> getPostsByCategory(Context context, String category) {
|
||||
return getAllPosts(context).stream()
|
||||
.filter(p -> category.equals(p.getCategory()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static void savePosts(Context context, List<Post> posts) {
|
||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
prefs.edit().putString(KEY_POSTS, gson.toJson(posts)).apply();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class PublishPostHelper {
|
||||
|
||||
public interface PublishCallback {
|
||||
void onSuccess(Post post);
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public static void showPublishDialog(Activity activity, String category, PublishCallback callback) {
|
||||
EditText input = new EditText(activity);
|
||||
input.setHint("输入内容...");
|
||||
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle("发布动态")
|
||||
.setView(input)
|
||||
.setPositiveButton("发布", (dialog, which) -> {
|
||||
String content = input.getText().toString().trim();
|
||||
if (content.isEmpty()) {
|
||||
Toast.makeText(activity, "内容不能为空", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
Post post = new Post();
|
||||
post.setId(String.valueOf(System.currentTimeMillis()));
|
||||
post.setContent(content);
|
||||
post.setCategory(category);
|
||||
post.setUserName("用户");
|
||||
post.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
PostManager.savePost(activity, post);
|
||||
if (callback != null) {
|
||||
callback.onSuccess(post);
|
||||
}
|
||||
})
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
package com.example.livestreaming;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.net.Room;
|
||||
|
||||
public class RoomAdapter extends ListAdapter<Room, RoomAdapter.RoomViewHolder> {
|
||||
|
||||
private OnRoomClickListener listener;
|
||||
|
||||
public interface OnRoomClickListener {
|
||||
void onRoomClick(Room room);
|
||||
}
|
||||
|
||||
public RoomAdapter() {
|
||||
super(new DiffUtil.ItemCallback<Room>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
|
||||
return oldItem.getId() != null && oldItem.getId().equals(newItem.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setOnRoomClickListener(OnRoomClickListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RoomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_room, parent, false);
|
||||
return new RoomViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RoomViewHolder holder, int position) {
|
||||
Room room = getItem(position);
|
||||
holder.bind(room, listener);
|
||||
}
|
||||
|
||||
static class RoomViewHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView tvTitle;
|
||||
private final TextView tvStreamer;
|
||||
private final TextView tvLikeCount;
|
||||
private final ImageView ivCover;
|
||||
private final View liveIndicator;
|
||||
|
||||
public RoomViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvTitle = itemView.findViewById(R.id.roomTitle);
|
||||
tvStreamer = itemView.findViewById(R.id.streamerName);
|
||||
tvLikeCount = itemView.findViewById(R.id.likeCount);
|
||||
ivCover = itemView.findViewById(R.id.coverImage);
|
||||
liveIndicator = itemView.findViewById(R.id.liveBadge);
|
||||
}
|
||||
|
||||
public void bind(Room room, OnRoomClickListener listener) {
|
||||
if (tvTitle != null) tvTitle.setText(room.getTitle());
|
||||
if (tvStreamer != null) tvStreamer.setText(room.getStreamerName());
|
||||
if (tvLikeCount != null) tvLikeCount.setText(String.valueOf(room.getViewerCount()));
|
||||
|
||||
if (liveIndicator != null) {
|
||||
liveIndicator.setVisibility(room.isLive() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
if (ivCover != null && room.getCoverImage() != null) {
|
||||
Glide.with(itemView.getContext())
|
||||
.load(room.getCoverImage())
|
||||
.placeholder(android.R.drawable.ic_menu_gallery)
|
||||
.into(ivCover);
|
||||
}
|
||||
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (listener != null) {
|
||||
listener.onRoomClick(room);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,28 +100,12 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
// WebSocket - 弹幕
|
||||
private WebSocket chatWebSocket;
|
||||
private OkHttpClient chatWsClient;
|
||||
private static final String WS_CHAT_BASE_URL = "ws://192.168.1.164:8081/ws/live/chat/";
|
||||
|
||||
// WebSocket - 在线人数
|
||||
private WebSocket onlineCountWebSocket;
|
||||
private OkHttpClient onlineCountWsClient;
|
||||
|
||||
// 动态获取WebSocket URL
|
||||
private String getWsChatBaseUrl() {
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
||||
baseUrl = "http://192.168.1.164:8081/";
|
||||
}
|
||||
// 将 http:// 转换为 ws://
|
||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/chat/";
|
||||
}
|
||||
|
||||
private String getWsOnlineBaseUrl() {
|
||||
String baseUrl = ApiClient.getCurrentBaseUrl(this);
|
||||
if (baseUrl == null || baseUrl.isEmpty()) {
|
||||
baseUrl = "http://192.168.1.164:8081/";
|
||||
}
|
||||
return baseUrl.replace("http://", "ws://").replace("https://", "wss://") + "ws/live/";
|
||||
}
|
||||
private static final String WS_ONLINE_BASE_URL = "ws://192.168.1.164:8081/ws/live/";
|
||||
|
||||
// WebSocket 心跳检测 - 弹幕
|
||||
private Runnable chatHeartbeatRunnable;
|
||||
|
|
@ -281,7 +265,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||
.build();
|
||||
Request request = new Request.Builder()
|
||||
.url(getWsChatBaseUrl() + roomId)
|
||||
.url(WS_CHAT_BASE_URL + roomId)
|
||||
.build();
|
||||
|
||||
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
||||
|
|
@ -388,7 +372,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
|
||||
userIdStr :
|
||||
"guest_" + System.currentTimeMillis();
|
||||
String wsUrl = getWsOnlineBaseUrl() + roomId + "?clientId=" + clientId;
|
||||
String wsUrl = WS_ONLINE_BASE_URL + roomId + "?clientId=" + clientId;
|
||||
|
||||
onlineCountWsClient = new OkHttpClient.Builder()
|
||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||
|
|
@ -1066,19 +1050,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
if (ijkSurface == null) return;
|
||||
|
||||
IjkMediaPlayer p = new IjkMediaPlayer();
|
||||
// 优化缓冲设置,减少卡顿
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1); // 开启缓冲
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000); // 100ms分析时长
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240); // 10KB探测大小
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5); // 允许丢帧
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000); // 3秒缓存
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50); // 最小缓冲帧数
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 0); // 关闭无限缓冲
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1); // 断线重连
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_streamed", 1);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect_delay_max", 5); // 最大重连延迟5秒
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 300);
|
||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
|
||||
|
||||
p.setOnPreparedListener(mp -> {
|
||||
binding.offlineLayout.setVisibility(View.GONE);
|
||||
|
|
@ -1087,36 +1066,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
||||
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
|
||||
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||
// 5秒后尝试重新连接
|
||||
handler.postDelayed(() -> {
|
||||
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
|
||||
fetchRoom(); // 重新获取房间信息并播放
|
||||
}
|
||||
}, 5000);
|
||||
return true;
|
||||
}
|
||||
ijkFallbackTried = true;
|
||||
startHls(ijkFallbackHlsUrl, null);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 添加缓冲监听
|
||||
p.setOnInfoListener((mp, what, extra) -> {
|
||||
if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_START) {
|
||||
android.util.Log.d("IjkPlayer", "开始缓冲...");
|
||||
// 可以显示加载指示器
|
||||
} else if (what == IMediaPlayer.MEDIA_INFO_BUFFERING_END) {
|
||||
android.util.Log.d("IjkPlayer", "缓冲结束");
|
||||
// 隐藏加载指示器
|
||||
} else if (what == IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
|
||||
android.util.Log.d("IjkPlayer", "视频开始渲染");
|
||||
binding.offlineLayout.setVisibility(View.GONE);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
ijkPlayer = p;
|
||||
try {
|
||||
|
|
@ -1124,7 +1081,6 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
p.setDataSource(url);
|
||||
p.prepareAsync();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
|
||||
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
||||
startHls(ijkFallbackHlsUrl, null);
|
||||
} else {
|
||||
|
|
@ -1276,7 +1232,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
*/
|
||||
private void setDefaultGifts() {
|
||||
availableGifts = new ArrayList<>();
|
||||
// 不再使用模拟数据,只从后端接口获取真实礼物数据
|
||||
availableGifts.add(new Gift("1", "玫瑰", 10, R.drawable.ic_gift_rose, 1));
|
||||
availableGifts.add(new Gift("2", "爱心", 20, R.drawable.ic_gift_heart, 1));
|
||||
availableGifts.add(new Gift("3", "蛋糕", 50, R.drawable.ic_gift_cake, 2));
|
||||
availableGifts.add(new Gift("4", "星星", 100, R.drawable.ic_gift_star, 2));
|
||||
availableGifts.add(new Gift("5", "钻石", 200, R.drawable.ic_gift_diamond, 3));
|
||||
availableGifts.add(new Gift("6", "皇冠", 500, R.drawable.ic_gift_crown, 4));
|
||||
availableGifts.add(new Gift("7", "跑车", 1000, R.drawable.ic_gift_car, 5));
|
||||
availableGifts.add(new Gift("8", "火箭", 2000, R.drawable.ic_gift_rocket, 5));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1543,7 +1506,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
*/
|
||||
private void showPaymentMethodDialog(String orderId, RechargeOption selectedOption,
|
||||
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
String[] paymentMethods = {"支付宝支付", "微信支付"};
|
||||
String[] paymentMethods = {"支付宝支付", "微信支付", "余额支付(模拟)"};
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle("选择支付方式")
|
||||
|
|
@ -1560,6 +1523,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
payType = "weixin";
|
||||
payChannel = "weixinAppAndroid";
|
||||
break;
|
||||
case 2: // 余额支付(模拟)
|
||||
// 模拟充值成功
|
||||
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
|
@ -1601,9 +1568,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
"\n请集成支付SDK完成实际支付",
|
||||
Toast.LENGTH_LONG).show();
|
||||
|
||||
// TODO: 集成支付SDK后,在支付成功回调中更新余额
|
||||
// 支付成功后应该调用后端接口查询订单状态并更新余额
|
||||
rechargeDialog.dismiss();
|
||||
// 暂时模拟支付成功
|
||||
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||
} else {
|
||||
Toast.makeText(RoomDetailActivity.this,
|
||||
"支付失败: " + apiResponse.getMessage(),
|
||||
|
|
@ -1625,6 +1591,28 @@ public class RoomDetailActivity extends AppCompatActivity {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟充值成功
|
||||
*/
|
||||
private void simulateRechargeSuccess(RechargeOption selectedOption,
|
||||
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||
userCoinBalance += selectedOption.getCoinAmount();
|
||||
|
||||
// 更新礼物弹窗中的余额显示
|
||||
if (giftDialog != null && giftDialog.isShowing()) {
|
||||
View giftView = giftDialog.findViewById(R.id.coinBalance);
|
||||
if (giftView instanceof android.widget.TextView) {
|
||||
((android.widget.TextView) giftView).setText(String.valueOf(userCoinBalance));
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(this,
|
||||
String.format("充值成功!获得 %d 金币", selectedOption.getCoinAmount()),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
rechargeDialog.dismiss();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从后端加载用户金币余额
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -367,18 +367,47 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private List<Room> buildFollowDemoRooms(int count) {
|
||||
// 不再使用模拟数据,只从后端接口获取真实关注主播的直播间数据
|
||||
return new ArrayList<>();
|
||||
List<Room> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String id = "follow-" + i;
|
||||
String title = "关注主播直播间 " + (i + 1);
|
||||
String streamer = "已关注主播" + (i + 1);
|
||||
boolean live = i % 4 != 0;
|
||||
list.add(new Room(id, title, streamer, live));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<NearbyUser> buildNearbyDemoUsers(int count) {
|
||||
// 不再使用模拟数据,只从后端接口获取真实附近用户数据
|
||||
return new ArrayList<>();
|
||||
List<NearbyUser> list = new ArrayList<>();
|
||||
String[] names = {"小王", "小李", "安安", "小陈", "小美", "老张", "小七", "阿杰",
|
||||
"小雨", "阿宁", "小星", "小林", "小杨", "小刘", "小赵", "小孙", "小周", "小吴"};
|
||||
for (int i = 0; i < count; i++) {
|
||||
String id = "user-" + i;
|
||||
String name = i < names.length ? names[i] : "用户" + (i + 1);
|
||||
boolean live = false; // 不再显示直播状态
|
||||
String distanceText;
|
||||
if (i < 3) {
|
||||
distanceText = (300 + i * 120) + "m";
|
||||
} else {
|
||||
float km = 0.8f + (i - 3) * 0.35f;
|
||||
distanceText = String.format("%.1fkm", km);
|
||||
}
|
||||
list.add(new NearbyUser(id, name, distanceText, live));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Room> buildDiscoverDemoRooms(int count) {
|
||||
// 不再使用模拟数据,只从后端接口获取真实推荐直播间数据
|
||||
return new ArrayList<>();
|
||||
List<Room> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String id = "discover-" + i;
|
||||
String title = "推荐直播间 " + (i + 1);
|
||||
String streamer = "推荐主播" + (i + 1);
|
||||
boolean live = i % 4 != 0;
|
||||
list.add(new Room(id, title, streamer, live));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private void showParkBadges() {
|
||||
|
|
@ -424,8 +453,17 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private List<BadgeItem> buildDemoBadges() {
|
||||
// 不再使用模拟数据,只从后端接口获取真实勋章数据
|
||||
return new ArrayList<>();
|
||||
List<BadgeItem> list = new ArrayList<>();
|
||||
list.add(new BadgeItem("b-1", "新人报道", "首次完善个人资料", R.drawable.ic_person_24, true, false));
|
||||
list.add(new BadgeItem("b-2", "热度新星", "累计获得100次点赞", R.drawable.ic_heart_24, false, false));
|
||||
list.add(new BadgeItem("b-3", "连续签到", "连续签到7天", R.drawable.ic_grid_24, false, true));
|
||||
list.add(new BadgeItem("b-4", "分享达人", "分享主页3次", R.drawable.ic_copy_24, false, false));
|
||||
list.add(new BadgeItem("b-5", "探索者", "进入发现页10次", R.drawable.ic_globe_24, true, false));
|
||||
list.add(new BadgeItem("b-6", "公园守护", "完成公园任务5次", R.drawable.ic_tree_24, false, true));
|
||||
list.add(new BadgeItem("b-7", "话题参与", "发布话题内容1次", R.drawable.ic_palette_24, false, false));
|
||||
list.add(new BadgeItem("b-8", "社交达人", "添加好友5人", R.drawable.ic_people_24, false, true));
|
||||
list.add(new BadgeItem("b-9", "开播尝鲜", "创建直播间1次", R.drawable.ic_mic_24, false, false));
|
||||
return list;
|
||||
}
|
||||
|
||||
private void showMore() {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,14 @@ public class WatchHistoryActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private List<Room> buildDemoHistory(int count) {
|
||||
// 不再使用模拟数据,只从后端接口获取真实观看历史数据
|
||||
return new ArrayList<>();
|
||||
List<Room> list = new ArrayList<>();
|
||||
for (int i = 0; i < count; i++) {
|
||||
String id = "history-" + i;
|
||||
String title = "看过的直播间 " + (i + 1);
|
||||
String streamer = "主播" + (i + 1);
|
||||
boolean live = i % 5 != 0;
|
||||
list.add(new Room(id, title, streamer, live));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,27 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.example.livestreaming.R;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通话界面 - 集成 WebRTC
|
||||
* 通话界面
|
||||
*/
|
||||
public class CallActivity extends AppCompatActivity implements
|
||||
CallManager.CallStateListener,
|
||||
WebRTCClient.WebRTCListener {
|
||||
public class CallActivity extends AppCompatActivity implements CallManager.CallStateListener {
|
||||
|
||||
private static final String TAG = "CallActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 100;
|
||||
|
||||
// UI 组件
|
||||
private ImageView ivBackgroundAvatar;
|
||||
private ImageView ivAvatar;
|
||||
private TextView tvUserName;
|
||||
|
|
@ -60,20 +36,11 @@ public class CallActivity extends AppCompatActivity implements
|
|||
private ImageButton btnSwitchCamera;
|
||||
private LinearLayout layoutCallControls;
|
||||
private LinearLayout layoutVideoToggle;
|
||||
private FrameLayout layoutLocalVideo;
|
||||
private FrameLayout layoutRemoteVideo;
|
||||
|
||||
// WebRTC 视频渲染器
|
||||
private SurfaceViewRenderer localRenderer;
|
||||
private SurfaceViewRenderer remoteRenderer;
|
||||
|
||||
// 管理器
|
||||
private CallManager callManager;
|
||||
private WebRTCClient webRTCClient;
|
||||
private AudioManager audioManager;
|
||||
private Handler handler;
|
||||
|
||||
// 通话信息
|
||||
private String callId;
|
||||
private String callType;
|
||||
private boolean isCaller;
|
||||
|
|
@ -81,18 +48,12 @@ public class CallActivity extends AppCompatActivity implements
|
|||
private String otherUserName;
|
||||
private String otherUserAvatar;
|
||||
|
||||
// 状态
|
||||
private boolean isMuted = false;
|
||||
private boolean isSpeakerOn = false;
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean isConnected = false;
|
||||
private boolean isWebRTCInitialized = false;
|
||||
private long callStartTime = 0;
|
||||
|
||||
// ICE Candidate 缓存(在远程 SDP 设置前收到的)
|
||||
private List<IceCandidate> pendingIceCandidates = new ArrayList<>();
|
||||
private boolean remoteDescriptionSet = false;
|
||||
|
||||
private Runnable durationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
|
@ -109,8 +70,6 @@ public class CallActivity extends AppCompatActivity implements
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Log.d(TAG, "========== CallActivity onCreate ==========");
|
||||
Toast.makeText(this, "通话界面已打开", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 保持屏幕常亮,显示在锁屏上方
|
||||
getWindow().addFlags(
|
||||
|
|
@ -123,20 +82,9 @@ public class CallActivity extends AppCompatActivity implements
|
|||
|
||||
initViews();
|
||||
initData();
|
||||
|
||||
Log.d(TAG, "callId=" + callId + ", callType=" + callType + ", isCaller=" + isCaller);
|
||||
Toast.makeText(this, "通话类型: " + callType + ", 主叫: " + isCaller, Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 检查权限
|
||||
if (checkPermissions()) {
|
||||
Log.d(TAG, "权限已授予,初始化通话");
|
||||
Toast.makeText(this, "权限OK,初始化WebRTC", Toast.LENGTH_SHORT).show();
|
||||
initCallAndWebRTC();
|
||||
} else {
|
||||
Log.d(TAG, "请求权限");
|
||||
Toast.makeText(this, "请求权限中...", Toast.LENGTH_SHORT).show();
|
||||
requestPermissions();
|
||||
}
|
||||
initCallManager();
|
||||
setupListeners();
|
||||
updateUI();
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
|
|
@ -154,62 +102,9 @@ public class CallActivity extends AppCompatActivity implements
|
|||
btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
|
||||
layoutCallControls = findViewById(R.id.layoutCallControls);
|
||||
layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
|
||||
layoutLocalVideo = findViewById(R.id.layoutLocalVideo);
|
||||
layoutRemoteVideo = findViewById(R.id.layoutRemoteVideo);
|
||||
|
||||
handler = new Handler(Looper.getMainLooper());
|
||||
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
||||
|
||||
// 设置音频模式为通话模式(重要!WebRTC音频需要此设置)
|
||||
setupAudioForCall();
|
||||
|
||||
setupListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置音频为通话模式
|
||||
*/
|
||||
private void setupAudioForCall() {
|
||||
Log.d(TAG, "========== 设置音频模式 ==========");
|
||||
try {
|
||||
// 保存原始音频模式
|
||||
int originalMode = audioManager.getMode();
|
||||
Log.d(TAG, "原始音频模式: " + originalMode);
|
||||
|
||||
// 设置为通话模式
|
||||
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
|
||||
Log.d(TAG, "设置音频模式为 MODE_IN_COMMUNICATION");
|
||||
|
||||
// 请求音频焦点
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
android.media.AudioAttributes playbackAttributes = new android.media.AudioAttributes.Builder()
|
||||
.setUsage(android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION)
|
||||
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
|
||||
.build();
|
||||
android.media.AudioFocusRequest focusRequest = new android.media.AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
|
||||
.setAudioAttributes(playbackAttributes)
|
||||
.build();
|
||||
audioManager.requestAudioFocus(focusRequest);
|
||||
} else {
|
||||
audioManager.requestAudioFocus(null, AudioManager.STREAM_VOICE_CALL, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
|
||||
}
|
||||
Log.d(TAG, "已请求音频焦点");
|
||||
|
||||
// 默认使用听筒(语音通话)或扬声器(视频通话)
|
||||
if ("video".equals(callType)) {
|
||||
audioManager.setSpeakerphoneOn(true);
|
||||
isSpeakerOn = true;
|
||||
Log.d(TAG, "视频通话,默认开启扬声器");
|
||||
} else {
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
isSpeakerOn = false;
|
||||
Log.d(TAG, "语音通话,默认使用听筒");
|
||||
}
|
||||
|
||||
Log.d(TAG, "音频设置完成");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "设置音频模式失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initData() {
|
||||
|
|
@ -223,175 +118,36 @@ public class CallActivity extends AppCompatActivity implements
|
|||
// 如果是被叫方接听,直接进入通话状态
|
||||
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
|
||||
if (alreadyConnected) {
|
||||
Log.d(TAG, "被叫方接听,直接进入通话状态");
|
||||
android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态");
|
||||
isConnected = true;
|
||||
callStartTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
private boolean checkPermissions() {
|
||||
boolean audioPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
boolean cameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
|
||||
if ("video".equals(callType)) {
|
||||
return audioPermission && cameraPermission;
|
||||
}
|
||||
return audioPermission;
|
||||
}
|
||||
|
||||
private void requestPermissions() {
|
||||
List<String> permissions = new ArrayList<>();
|
||||
permissions.add(Manifest.permission.RECORD_AUDIO);
|
||||
if ("video".equals(callType)) {
|
||||
permissions.add(Manifest.permission.CAMERA);
|
||||
}
|
||||
ActivityCompat.requestPermissions(this, permissions.toArray(new String[0]), PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == PERMISSION_REQUEST_CODE) {
|
||||
boolean allGranted = true;
|
||||
for (int result : grantResults) {
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
allGranted = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allGranted) {
|
||||
initCallAndWebRTC();
|
||||
} else {
|
||||
Toast.makeText(this, "需要麦克风和摄像头权限才能进行通话", Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initCallAndWebRTC() {
|
||||
initCallManager();
|
||||
initWebRTC();
|
||||
}
|
||||
|
||||
private void initCallManager() {
|
||||
callManager = CallManager.getInstance(this);
|
||||
callManager.setStateListener(this);
|
||||
|
||||
// 确保WebSocket已连接
|
||||
// 确保WebSocket已连接(主叫方需要接收接听/拒绝通知)
|
||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
try {
|
||||
int uid = (int) Double.parseDouble(userId);
|
||||
if (uid > 0) {
|
||||
Log.d(TAG, "确保WebSocket连接,userId: " + uid);
|
||||
android.util.Log.d("CallActivity", "确保WebSocket连接,userId: " + uid);
|
||||
callManager.connect(uid);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "解析用户ID失败", e);
|
||||
android.util.Log.e("CallActivity", "解析用户ID失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initWebRTC() {
|
||||
Log.d(TAG, "========== 初始化 WebRTC ==========");
|
||||
Toast.makeText(this, "开始初始化WebRTC...", Toast.LENGTH_SHORT).show();
|
||||
|
||||
boolean isVideoCall = "video".equals(callType);
|
||||
Log.d(TAG, "isVideoCall=" + isVideoCall + ", callType=" + callType);
|
||||
|
||||
try {
|
||||
// 创建 WebRTC 客户端
|
||||
Toast.makeText(this, "创建WebRTC客户端...", Toast.LENGTH_SHORT).show();
|
||||
webRTCClient = new WebRTCClient(this);
|
||||
webRTCClient.setListener(this);
|
||||
|
||||
Toast.makeText(this, "调用initialize...", Toast.LENGTH_SHORT).show();
|
||||
webRTCClient.initialize(isVideoCall);
|
||||
|
||||
Log.d(TAG, "WebRTC 客户端创建成功");
|
||||
Toast.makeText(this, "initialize完成!", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 如果是视频通话,设置视频渲染器
|
||||
if (isVideoCall) {
|
||||
try {
|
||||
Toast.makeText(this, "设置视频渲染器...", Toast.LENGTH_SHORT).show();
|
||||
setupVideoRenderers();
|
||||
Toast.makeText(this, "视频渲染器设置成功", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "视频渲染器设置失败", e);
|
||||
Toast.makeText(this, "视频渲染器失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建本地媒体流
|
||||
Log.d(TAG, "创建本地媒体流...");
|
||||
try {
|
||||
Toast.makeText(this, "创建本地媒体流...", Toast.LENGTH_SHORT).show();
|
||||
webRTCClient.createLocalStream();
|
||||
Toast.makeText(this, "本地媒体流创建成功", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "本地媒体流创建失败", e);
|
||||
Toast.makeText(this, "媒体流失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
// 创建 PeerConnection
|
||||
Log.d(TAG, "创建 PeerConnection...");
|
||||
try {
|
||||
webRTCClient.createPeerConnection();
|
||||
Toast.makeText(this, "PeerConnection创建成功", Toast.LENGTH_SHORT).show();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "PeerConnection创建失败", e);
|
||||
Toast.makeText(this, "PeerConnection失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
isWebRTCInitialized = true;
|
||||
Log.d(TAG, "WebRTC 初始化完成");
|
||||
Toast.makeText(this, "WebRTC初始化完成!", Toast.LENGTH_SHORT).show();
|
||||
|
||||
// 主叫方:等待对方接听后再创建 Offer(在 onCallConnected 中创建)
|
||||
// 被叫方:等待收到 Offer 后创建 Answer
|
||||
if (isCaller) {
|
||||
Log.d(TAG, "主叫方,等待对方接听后创建 Offer");
|
||||
Toast.makeText(this, "等待对方接听...", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Log.d(TAG, "被叫方,等待 Offer");
|
||||
Toast.makeText(this, "被叫方,等待Offer...", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "WebRTC 初始化失败", e);
|
||||
Toast.makeText(this, "WebRTC初始化失败: " + e.getMessage(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void setupVideoRenderers() {
|
||||
Log.d(TAG, "设置视频渲染器");
|
||||
|
||||
// 创建本地视频渲染器
|
||||
localRenderer = new SurfaceViewRenderer(this);
|
||||
layoutLocalVideo.addView(localRenderer);
|
||||
webRTCClient.setLocalRenderer(localRenderer);
|
||||
|
||||
// 创建远程视频渲染器
|
||||
remoteRenderer = new SurfaceViewRenderer(this);
|
||||
layoutRemoteVideo.addView(remoteRenderer);
|
||||
webRTCClient.setRemoteRenderer(remoteRenderer);
|
||||
|
||||
// 显示视频布局
|
||||
layoutLocalVideo.setVisibility(View.VISIBLE);
|
||||
layoutRemoteVideo.setVisibility(View.VISIBLE);
|
||||
|
||||
// 隐藏头像(视频通话时)
|
||||
ivBackgroundAvatar.setVisibility(View.GONE);
|
||||
ivAvatar.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
btnMinimize.setOnClickListener(v -> moveTaskToBack(true));
|
||||
btnMinimize.setOnClickListener(v -> {
|
||||
// 最小化通话(后台运行)
|
||||
moveTaskToBack(true);
|
||||
});
|
||||
|
||||
btnMute.setOnClickListener(v -> toggleMute());
|
||||
btnSpeaker.setOnClickListener(v -> toggleSpeaker());
|
||||
|
|
@ -406,7 +162,7 @@ public class CallActivity extends AppCompatActivity implements
|
|||
} else {
|
||||
callManager.rejectCall(callId);
|
||||
}
|
||||
releaseAndFinish();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -427,11 +183,14 @@ public class CallActivity extends AppCompatActivity implements
|
|||
|
||||
// 根据连接状态设置界面
|
||||
if (isConnected) {
|
||||
// 已接通,显示通话中界面
|
||||
tvCallStatus.setVisibility(View.GONE);
|
||||
tvCallDuration.setVisibility(View.VISIBLE);
|
||||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
|
||||
} else {
|
||||
// 未接通,显示等待状态
|
||||
if (isCaller) {
|
||||
tvCallStatus.setText("正在呼叫...");
|
||||
} else {
|
||||
|
|
@ -444,10 +203,7 @@ public class CallActivity extends AppCompatActivity implements
|
|||
isMuted = !isMuted;
|
||||
btnMute.setImageResource(isMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic);
|
||||
btnMute.setBackgroundResource(isMuted ? R.drawable.bg_call_button_active : R.drawable.bg_call_button);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setMuted(isMuted);
|
||||
}
|
||||
// TODO: 实际静音控制
|
||||
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
|
|
@ -461,35 +217,17 @@ public class CallActivity extends AppCompatActivity implements
|
|||
private void toggleVideo() {
|
||||
isVideoEnabled = !isVideoEnabled;
|
||||
btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active);
|
||||
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.setVideoEnabled(isVideoEnabled);
|
||||
}
|
||||
|
||||
if (localRenderer != null) {
|
||||
localRenderer.setVisibility(isVideoEnabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
// TODO: 实际视频控制
|
||||
Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void switchCamera() {
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.switchCamera();
|
||||
}
|
||||
// TODO: 切换前后摄像头
|
||||
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onCallConnected() {
|
||||
Log.d(TAG, "========== 通话已连接 ==========");
|
||||
Log.d(TAG, "isCaller=" + isCaller + ", isWebRTCInitialized=" + isWebRTCInitialized);
|
||||
|
||||
// 如果是主叫方收到 call_accepted,现在创建 Offer
|
||||
if (isCaller && isWebRTCInitialized && webRTCClient != null) {
|
||||
Log.d(TAG, "主叫方收到接听通知,开始创建 Offer");
|
||||
Toast.makeText(this, "对方已接听,建立连接...", Toast.LENGTH_SHORT).show();
|
||||
webRTCClient.createOffer();
|
||||
}
|
||||
|
||||
android.util.Log.d("CallActivity", "onCallConnected() 开始执行");
|
||||
isConnected = true;
|
||||
callStartTime = System.currentTimeMillis();
|
||||
|
||||
|
|
@ -498,22 +236,14 @@ public class CallActivity extends AppCompatActivity implements
|
|||
layoutCallControls.setVisibility(View.VISIBLE);
|
||||
|
||||
handler.post(durationRunnable);
|
||||
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
|
||||
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void releaseAndFinish() {
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.release();
|
||||
webRTCClient = null;
|
||||
}
|
||||
finish();
|
||||
}
|
||||
|
||||
// ==================== CallStateListener 实现 ====================
|
||||
|
||||
// CallStateListener 实现
|
||||
@Override
|
||||
public void onCallStateChanged(String state, String callId) {
|
||||
Log.d(TAG, "onCallStateChanged: " + state);
|
||||
// 状态变化处理
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -523,8 +253,13 @@ public class CallActivity extends AppCompatActivity implements
|
|||
|
||||
@Override
|
||||
public void onCallConnected(String callId) {
|
||||
Log.d(TAG, "onCallConnected: " + callId);
|
||||
runOnUiThread(this::onCallConnected);
|
||||
android.util.Log.d("CallActivity", "========== onCallConnected 被调用 ==========");
|
||||
android.util.Log.d("CallActivity", "callId: " + callId);
|
||||
android.util.Log.d("CallActivity", "this.callId: " + this.callId);
|
||||
runOnUiThread(() -> {
|
||||
android.util.Log.d("CallActivity", "执行 onCallConnected UI更新");
|
||||
onCallConnected();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -551,7 +286,7 @@ public class CallActivity extends AppCompatActivity implements
|
|||
message = "通话已结束";
|
||||
}
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
releaseAndFinish();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -562,163 +297,18 @@ public class CallActivity extends AppCompatActivity implements
|
|||
});
|
||||
}
|
||||
|
||||
// ==================== WebRTCListener 实现 ====================
|
||||
|
||||
@Override
|
||||
public void onLocalStream(MediaStream stream) {
|
||||
Log.d(TAG, "本地媒体流已创建");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoteStream(MediaStream stream) {
|
||||
Log.d(TAG, "收到远程媒体流");
|
||||
runOnUiThread(() -> {
|
||||
if (!isConnected) {
|
||||
onCallConnected();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate candidate) {
|
||||
Log.d(TAG, "========== 发送 ICE Candidate ==========");
|
||||
// 通过信令服务器发送 ICE Candidate
|
||||
if (callManager != null && callId != null) {
|
||||
try {
|
||||
JSONObject candidateJson = new JSONObject();
|
||||
candidateJson.put("sdpMid", candidate.sdpMid);
|
||||
candidateJson.put("sdpMLineIndex", candidate.sdpMLineIndex);
|
||||
candidateJson.put("candidate", candidate.sdp);
|
||||
callManager.sendIceCandidate(callId, candidateJson);
|
||||
Log.d(TAG, "ICE Candidate 已发送");
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "构建 ICE Candidate JSON 失败", e);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "无法发送ICE: callManager=" + callManager + ", callId=" + callId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState state) {
|
||||
Log.d(TAG, "ICE 连接状态变化: " + state);
|
||||
runOnUiThread(() -> {
|
||||
switch (state) {
|
||||
case CONNECTED:
|
||||
case COMPLETED:
|
||||
if (!isConnected) {
|
||||
onCallConnected();
|
||||
}
|
||||
break;
|
||||
case DISCONNECTED:
|
||||
case FAILED:
|
||||
Toast.makeText(this, "连接断开", Toast.LENGTH_SHORT).show();
|
||||
releaseAndFinish();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOfferCreated(SessionDescription sdp) {
|
||||
Log.d(TAG, "========== Offer 已创建 ==========");
|
||||
Log.d(TAG, "callId=" + callId);
|
||||
Log.d(TAG, "callManager=" + (callManager != null ? "存在" : "null"));
|
||||
|
||||
if (callManager != null && callId != null) {
|
||||
callManager.sendOffer(callId, sdp.description);
|
||||
Log.d(TAG, "Offer 已发送");
|
||||
runOnUiThread(() -> Toast.makeText(this, "Offer已发送!", Toast.LENGTH_SHORT).show());
|
||||
} else {
|
||||
Log.e(TAG, "无法发送Offer: callManager=" + callManager + ", callId=" + callId);
|
||||
runOnUiThread(() -> Toast.makeText(this, "发送Offer失败!", Toast.LENGTH_SHORT).show());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnswerCreated(SessionDescription sdp) {
|
||||
Log.d(TAG, "Answer 已创建,发送给对方");
|
||||
if (callManager != null && callId != null) {
|
||||
callManager.sendAnswer(callId, sdp.description);
|
||||
Log.d(TAG, "Answer 已发送");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理收到的 Offer
|
||||
public void handleRemoteOffer(String sdp) {
|
||||
Log.d(TAG, "收到远程 Offer");
|
||||
if (webRTCClient != null) {
|
||||
SessionDescription offer = new SessionDescription(SessionDescription.Type.OFFER, sdp);
|
||||
webRTCClient.setRemoteDescription(offer);
|
||||
remoteDescriptionSet = true;
|
||||
|
||||
// 处理缓存的 ICE Candidates
|
||||
for (IceCandidate candidate : pendingIceCandidates) {
|
||||
webRTCClient.addIceCandidate(candidate);
|
||||
}
|
||||
pendingIceCandidates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理收到的 Answer
|
||||
public void handleRemoteAnswer(String sdp) {
|
||||
Log.d(TAG, "收到远程 Answer");
|
||||
if (webRTCClient != null) {
|
||||
SessionDescription answer = new SessionDescription(SessionDescription.Type.ANSWER, sdp);
|
||||
webRTCClient.setRemoteDescription(answer);
|
||||
remoteDescriptionSet = true;
|
||||
|
||||
// 处理缓存的 ICE Candidates
|
||||
for (IceCandidate candidate : pendingIceCandidates) {
|
||||
webRTCClient.addIceCandidate(candidate);
|
||||
}
|
||||
pendingIceCandidates.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理收到的 ICE Candidate
|
||||
public void handleRemoteIceCandidate(JSONObject candidateJson) {
|
||||
try {
|
||||
String sdpMid = candidateJson.getString("sdpMid");
|
||||
int sdpMLineIndex = candidateJson.getInt("sdpMLineIndex");
|
||||
String sdp = candidateJson.getString("candidate");
|
||||
|
||||
IceCandidate candidate = new IceCandidate(sdpMid, sdpMLineIndex, sdp);
|
||||
|
||||
if (remoteDescriptionSet && webRTCClient != null) {
|
||||
webRTCClient.addIceCandidate(candidate);
|
||||
} else {
|
||||
// 缓存 ICE Candidate,等远程 SDP 设置后再添加
|
||||
pendingIceCandidates.add(candidate);
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "解析 ICE Candidate 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
handler.removeCallbacks(durationRunnable);
|
||||
|
||||
// 恢复音频模式
|
||||
if (audioManager != null) {
|
||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
||||
audioManager.setSpeakerphoneOn(false);
|
||||
Log.d(TAG, "音频模式已恢复");
|
||||
}
|
||||
|
||||
if (callManager != null) {
|
||||
callManager.setStateListener(null);
|
||||
}
|
||||
if (webRTCClient != null) {
|
||||
webRTCClient.release();
|
||||
webRTCClient = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// 禁止返回键退出,需要点击挂断
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -351,86 +351,18 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
// 启动来电界面
|
||||
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
||||
Intent intent = new Intent(context, IncomingCallActivity.class);
|
||||
// 添加多个 flags 确保能从后台启动
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
| Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.putExtra("callId", callId);
|
||||
intent.putExtra("callType", callType);
|
||||
intent.putExtra("callerId", callerId);
|
||||
intent.putExtra("callerName", callerName);
|
||||
intent.putExtra("callerAvatar", callerAvatar);
|
||||
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
Log.d(TAG, "来电界面启动成功");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "启动来电界面失败: " + e.getMessage(), e);
|
||||
// 如果直接启动失败,尝试使用通知方式
|
||||
showIncomingCallNotification(callId, callerId, callerName, callerAvatar, callType);
|
||||
}
|
||||
context.startActivity(intent);
|
||||
|
||||
if (stateListener != null) {
|
||||
stateListener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示来电通知(当无法直接启动Activity时使用)
|
||||
*/
|
||||
private void showIncomingCallNotification(String callId, int callerId, String callerName, String callerAvatar, String callType) {
|
||||
try {
|
||||
android.app.NotificationManager notificationManager =
|
||||
(android.app.NotificationManager) context.getSystemService(android.content.Context.NOTIFICATION_SERVICE);
|
||||
|
||||
// 创建通知渠道 (Android 8.0+)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
android.app.NotificationChannel channel = new android.app.NotificationChannel(
|
||||
"incoming_call",
|
||||
"来电通知",
|
||||
android.app.NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("显示来电通知");
|
||||
channel.enableVibration(true);
|
||||
channel.setLockscreenVisibility(android.app.Notification.VISIBILITY_PUBLIC);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
// 创建点击通知时启动的Intent
|
||||
Intent intent = new Intent(context, IncomingCallActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.putExtra("callId", callId);
|
||||
intent.putExtra("callType", callType);
|
||||
intent.putExtra("callerId", callerId);
|
||||
intent.putExtra("callerName", callerName);
|
||||
intent.putExtra("callerAvatar", callerAvatar);
|
||||
|
||||
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getActivity(
|
||||
context, 0, intent,
|
||||
android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
);
|
||||
|
||||
// 构建通知
|
||||
String callTypeText = "video".equals(callType) ? "视频通话" : "语音通话";
|
||||
androidx.core.app.NotificationCompat.Builder builder =
|
||||
new androidx.core.app.NotificationCompat.Builder(context, "incoming_call")
|
||||
.setSmallIcon(android.R.drawable.ic_menu_call)
|
||||
.setContentTitle(callerName + " 的" + callTypeText)
|
||||
.setContentText("点击接听")
|
||||
.setPriority(androidx.core.app.NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(androidx.core.app.NotificationCompat.CATEGORY_CALL)
|
||||
.setFullScreenIntent(pendingIntent, true)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
.setOngoing(true);
|
||||
|
||||
notificationManager.notify(1001, builder.build());
|
||||
Log.d(TAG, "来电通知已显示");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "显示来电通知失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallAccepted(String callId) {
|
||||
|
|
@ -485,56 +417,17 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
|||
|
||||
@Override
|
||||
public void onOffer(String callId, String sdp) {
|
||||
Log.d(TAG, "收到 Offer");
|
||||
// 通知 CallActivity 处理 Offer
|
||||
if (stateListener instanceof CallActivity) {
|
||||
((CallActivity) stateListener).handleRemoteOffer(sdp);
|
||||
}
|
||||
// WebRTC offer处理 - 后续实现
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnswer(String callId, String sdp) {
|
||||
Log.d(TAG, "收到 Answer");
|
||||
// 通知 CallActivity 处理 Answer
|
||||
if (stateListener instanceof CallActivity) {
|
||||
((CallActivity) stateListener).handleRemoteAnswer(sdp);
|
||||
}
|
||||
// WebRTC answer处理 - 后续实现
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(String callId, JSONObject candidate) {
|
||||
Log.d(TAG, "收到 ICE Candidate");
|
||||
// 通知 CallActivity 处理 ICE Candidate
|
||||
if (stateListener instanceof CallActivity) {
|
||||
((CallActivity) stateListener).handleRemoteIceCandidate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Offer
|
||||
*/
|
||||
public void sendOffer(String callId, String sdp) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendOffer(callId, sdp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Answer
|
||||
*/
|
||||
public void sendAnswer(String callId, String sdp) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendAnswer(callId, sdp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 ICE Candidate
|
||||
*/
|
||||
public void sendIceCandidate(String callId, JSONObject candidate) {
|
||||
if (signalingClient != null) {
|
||||
signalingClient.sendIceCandidate(callId, candidate);
|
||||
}
|
||||
// WebRTC ICE candidate处理 - 后续实现
|
||||
}
|
||||
|
||||
public interface CallCallback {
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ public class CallSignalingClient {
|
|||
private String baseUrl;
|
||||
private int userId;
|
||||
private boolean isConnected = false;
|
||||
private boolean isManualDisconnect = false;
|
||||
private int reconnectAttempts = 0;
|
||||
private static final int MAX_RECONNECT_ATTEMPTS = 5;
|
||||
private static final long RECONNECT_DELAY_MS = 3000;
|
||||
private static final long HEARTBEAT_INTERVAL_MS = 30000; // 30秒心跳
|
||||
private Runnable heartbeatRunnable;
|
||||
|
||||
public interface SignalingListener {
|
||||
void onConnected();
|
||||
|
|
@ -60,26 +54,19 @@ public class CallSignalingClient {
|
|||
}
|
||||
|
||||
public void connect() {
|
||||
isManualDisconnect = false;
|
||||
doConnect();
|
||||
}
|
||||
|
||||
private void doConnect() {
|
||||
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
|
||||
if (!wsUrl.endsWith("/")) wsUrl += "/";
|
||||
wsUrl += "ws/call";
|
||||
|
||||
Log.d(TAG, "Connecting to: " + wsUrl + " (attempt " + (reconnectAttempts + 1) + ")");
|
||||
Log.d(TAG, "Connecting to: " + wsUrl);
|
||||
|
||||
Request request = new Request.Builder().url(wsUrl).build();
|
||||
webSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||
@Override
|
||||
public void onOpen(WebSocket webSocket, Response response) {
|
||||
Log.d(TAG, "WebSocket connected successfully!");
|
||||
Log.d(TAG, "WebSocket connected");
|
||||
isConnected = true;
|
||||
reconnectAttempts = 0; // 重置重连计数
|
||||
register();
|
||||
startHeartbeat();
|
||||
notifyConnected();
|
||||
}
|
||||
|
||||
|
|
@ -99,83 +86,24 @@ public class CallSignalingClient {
|
|||
Log.d(TAG, "WebSocket closed: " + reason);
|
||||
isConnected = false;
|
||||
notifyDisconnected();
|
||||
// 非手动断开时尝试重连
|
||||
if (!isManualDisconnect) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
||||
Log.e(TAG, "WebSocket connection failed: " + t.getMessage(), t);
|
||||
Log.e(TAG, "WebSocket error", t);
|
||||
isConnected = false;
|
||||
notifyError("连接失败: " + t.getMessage());
|
||||
// 尝试重连
|
||||
if (!isManualDisconnect) {
|
||||
scheduleReconnect();
|
||||
}
|
||||
notifyError(t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void scheduleReconnect() {
|
||||
if (isManualDisconnect) {
|
||||
Log.d(TAG, "手动断开,不重连");
|
||||
return;
|
||||
}
|
||||
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
||||
Log.e(TAG, "达到最大重连次数(" + MAX_RECONNECT_ATTEMPTS + "),停止重连");
|
||||
notifyError("WebSocket连接失败,已达最大重试次数");
|
||||
return;
|
||||
}
|
||||
reconnectAttempts++;
|
||||
long delay = RECONNECT_DELAY_MS * reconnectAttempts;
|
||||
Log.d(TAG, "将在 " + delay + "ms 后重连 (第" + reconnectAttempts + "次)");
|
||||
mainHandler.postDelayed(() -> {
|
||||
if (!isConnected && !isManualDisconnect) {
|
||||
doConnect();
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
isManualDisconnect = true;
|
||||
reconnectAttempts = 0;
|
||||
stopHeartbeat();
|
||||
if (webSocket != null) {
|
||||
webSocket.close(1000, "User disconnect");
|
||||
webSocket = null;
|
||||
}
|
||||
isConnected = false;
|
||||
}
|
||||
|
||||
private void startHeartbeat() {
|
||||
stopHeartbeat();
|
||||
heartbeatRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isConnected && webSocket != null) {
|
||||
try {
|
||||
JSONObject ping = new JSONObject();
|
||||
ping.put("type", "ping");
|
||||
webSocket.send(ping.toString());
|
||||
Log.d(TAG, "发送心跳 ping");
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "心跳发送失败", e);
|
||||
}
|
||||
mainHandler.postDelayed(this, HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
};
|
||||
mainHandler.postDelayed(heartbeatRunnable, HEARTBEAT_INTERVAL_MS);
|
||||
}
|
||||
|
||||
private void stopHeartbeat() {
|
||||
if (heartbeatRunnable != null) {
|
||||
mainHandler.removeCallbacks(heartbeatRunnable);
|
||||
heartbeatRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isConnected;
|
||||
|
|
|
|||
|
|
@ -1,729 +0,0 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.webrtc.AudioSource;
|
||||
import org.webrtc.AudioTrack;
|
||||
import org.webrtc.Camera1Enumerator;
|
||||
import org.webrtc.Camera2Enumerator;
|
||||
import org.webrtc.CameraEnumerator;
|
||||
import org.webrtc.DataChannel;
|
||||
import org.webrtc.DefaultVideoDecoderFactory;
|
||||
import org.webrtc.DefaultVideoEncoderFactory;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
import org.webrtc.MediaConstraints;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.webrtc.PeerConnectionFactory;
|
||||
import org.webrtc.RtpReceiver;
|
||||
import org.webrtc.SdpObserver;
|
||||
import org.webrtc.SessionDescription;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoCapturer;
|
||||
import org.webrtc.VideoSource;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WebRTC 客户端 - 处理音视频传输
|
||||
*/
|
||||
public class WebRTCClient {
|
||||
private static final String TAG = "WebRTCClient";
|
||||
|
||||
private Context context;
|
||||
private EglBase eglBase;
|
||||
private PeerConnectionFactory peerConnectionFactory;
|
||||
private PeerConnection peerConnection;
|
||||
|
||||
private AudioSource audioSource;
|
||||
private AudioTrack localAudioTrack;
|
||||
private VideoSource videoSource;
|
||||
private VideoTrack localVideoTrack;
|
||||
private VideoCapturer videoCapturer;
|
||||
private SurfaceTextureHelper surfaceTextureHelper;
|
||||
|
||||
private SurfaceViewRenderer localRenderer;
|
||||
private SurfaceViewRenderer remoteRenderer;
|
||||
|
||||
private WebRTCListener listener;
|
||||
private boolean isVideoCall = false;
|
||||
private boolean isMuted = false;
|
||||
private boolean isVideoEnabled = true;
|
||||
private boolean useFrontCamera = true;
|
||||
|
||||
// ICE 服务器配置 - 可通过 WebRTCConfig 修改
|
||||
private static List<PeerConnection.IceServer> getIceServers() {
|
||||
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||
|
||||
// STUN 服务器(用于获取公网 IP)
|
||||
for (String stunServer : WebRTCConfig.STUN_SERVERS) {
|
||||
iceServers.add(PeerConnection.IceServer.builder(stunServer).createIceServer());
|
||||
}
|
||||
|
||||
// TURN 服务器(用于 NAT 穿透失败时的中继)
|
||||
if (WebRTCConfig.TURN_SERVER_URL != null && !WebRTCConfig.TURN_SERVER_URL.isEmpty()) {
|
||||
// UDP
|
||||
iceServers.add(PeerConnection.IceServer.builder(WebRTCConfig.TURN_SERVER_URL)
|
||||
.setUsername(WebRTCConfig.TURN_USERNAME)
|
||||
.setPassword(WebRTCConfig.TURN_PASSWORD)
|
||||
.createIceServer());
|
||||
|
||||
// TCP(备用)
|
||||
iceServers.add(PeerConnection.IceServer.builder(WebRTCConfig.TURN_SERVER_URL + "?transport=tcp")
|
||||
.setUsername(WebRTCConfig.TURN_USERNAME)
|
||||
.setPassword(WebRTCConfig.TURN_PASSWORD)
|
||||
.createIceServer());
|
||||
}
|
||||
|
||||
return iceServers;
|
||||
}
|
||||
|
||||
public interface WebRTCListener {
|
||||
void onLocalStream(MediaStream stream);
|
||||
void onRemoteStream(MediaStream stream);
|
||||
void onIceCandidate(IceCandidate candidate);
|
||||
void onIceConnectionChange(PeerConnection.IceConnectionState state);
|
||||
void onOfferCreated(SessionDescription sdp);
|
||||
void onAnswerCreated(SessionDescription sdp);
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public WebRTCClient(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public void setListener(WebRTCListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 WebRTC
|
||||
*/
|
||||
public void initialize(boolean isVideoCall) {
|
||||
this.isVideoCall = isVideoCall;
|
||||
Log.d(TAG, "初始化 WebRTC, isVideoCall=" + isVideoCall);
|
||||
|
||||
// 在后台线程初始化,避免阻塞UI
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 初始化 EglBase
|
||||
Log.d(TAG, "创建 EglBase...");
|
||||
eglBase = EglBase.create();
|
||||
Log.d(TAG, "EglBase 创建成功");
|
||||
|
||||
// 初始化 PeerConnectionFactory
|
||||
Log.d(TAG, "初始化 PeerConnectionFactory...");
|
||||
PeerConnectionFactory.InitializationOptions initOptions = PeerConnectionFactory.InitializationOptions.builder(context)
|
||||
.setEnableInternalTracer(false)
|
||||
.createInitializationOptions();
|
||||
PeerConnectionFactory.initialize(initOptions);
|
||||
Log.d(TAG, "PeerConnectionFactory 初始化成功");
|
||||
|
||||
PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
|
||||
|
||||
Log.d(TAG, "创建视频编解码器...");
|
||||
DefaultVideoEncoderFactory encoderFactory = new DefaultVideoEncoderFactory(
|
||||
eglBase.getEglBaseContext(), true, true);
|
||||
DefaultVideoDecoderFactory decoderFactory = new DefaultVideoDecoderFactory(eglBase.getEglBaseContext());
|
||||
Log.d(TAG, "视频编解码器创建成功");
|
||||
|
||||
Log.d(TAG, "创建 PeerConnectionFactory...");
|
||||
peerConnectionFactory = PeerConnectionFactory.builder()
|
||||
.setOptions(options)
|
||||
.setVideoEncoderFactory(encoderFactory)
|
||||
.setVideoDecoderFactory(decoderFactory)
|
||||
.createPeerConnectionFactory();
|
||||
|
||||
Log.d(TAG, "PeerConnectionFactory 创建成功");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "WebRTC 初始化异常", e);
|
||||
if (listener != null) {
|
||||
listener.onError("WebRTC初始化失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
||||
// 等待初始化完成(最多5秒)
|
||||
int waitCount = 0;
|
||||
while (peerConnectionFactory == null && waitCount < 50) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
waitCount++;
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (peerConnectionFactory == null) {
|
||||
Log.e(TAG, "WebRTC 初始化超时");
|
||||
if (listener != null) {
|
||||
listener.onError("WebRTC初始化超时");
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "WebRTC 初始化完成,耗时: " + (waitCount * 100) + "ms");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置本地视频渲染器
|
||||
*/
|
||||
public void setLocalRenderer(SurfaceViewRenderer renderer) {
|
||||
this.localRenderer = renderer;
|
||||
if (localRenderer != null) {
|
||||
localRenderer.init(eglBase.getEglBaseContext(), null);
|
||||
localRenderer.setMirror(true);
|
||||
localRenderer.setEnableHardwareScaler(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置远程视频渲染器
|
||||
*/
|
||||
public void setRemoteRenderer(SurfaceViewRenderer renderer) {
|
||||
this.remoteRenderer = renderer;
|
||||
if (remoteRenderer != null) {
|
||||
remoteRenderer.init(eglBase.getEglBaseContext(), null);
|
||||
remoteRenderer.setMirror(false);
|
||||
remoteRenderer.setEnableHardwareScaler(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建本地媒体流
|
||||
*/
|
||||
public void createLocalStream() {
|
||||
Log.d(TAG, "========== 创建本地媒体流 ==========");
|
||||
Log.d(TAG, "isVideoCall=" + isVideoCall);
|
||||
|
||||
try {
|
||||
// 创建音频轨道
|
||||
MediaConstraints audioConstraints = new MediaConstraints();
|
||||
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googEchoCancellation", "true"));
|
||||
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googAutoGainControl", "true"));
|
||||
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googHighpassFilter", "true"));
|
||||
audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair("googNoiseSuppression", "true"));
|
||||
|
||||
if (peerConnectionFactory != null) {
|
||||
audioSource = peerConnectionFactory.createAudioSource(audioConstraints);
|
||||
if (audioSource != null) {
|
||||
localAudioTrack = peerConnectionFactory.createAudioTrack("ARDAMSa0", audioSource);
|
||||
if (localAudioTrack != null) {
|
||||
localAudioTrack.setEnabled(true);
|
||||
Log.d(TAG, "音频轨道创建成功");
|
||||
} else {
|
||||
Log.w(TAG, "音频轨道创建返回null,继续执行");
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "音频源创建返回null,继续执行");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "peerConnectionFactory为null!");
|
||||
if (listener != null) {
|
||||
listener.onError("PeerConnectionFactory未初始化");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "音频轨道创建失败,继续执行", e);
|
||||
// 不返回,继续尝试创建视频轨道
|
||||
}
|
||||
|
||||
// 如果是视频通话,创建视频轨道
|
||||
if (isVideoCall) {
|
||||
Log.d(TAG, "视频通话,开始创建视频轨道");
|
||||
try {
|
||||
createVideoTrack();
|
||||
Log.d(TAG, "视频轨道创建结果: localVideoTrack=" + (localVideoTrack != null));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "视频轨道创建失败", e);
|
||||
if (listener != null) {
|
||||
listener.onError("视频轨道创建失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "语音通话,跳过视频轨道创建");
|
||||
}
|
||||
|
||||
Log.d(TAG, "本地媒体流创建完成: audioTrack=" + (localAudioTrack != null) + ", videoTrack=" + (localVideoTrack != null));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建视频轨道
|
||||
*/
|
||||
private void createVideoTrack() {
|
||||
Log.d(TAG, "创建视频轨道");
|
||||
|
||||
videoCapturer = createCameraCapturer();
|
||||
if (videoCapturer == null) {
|
||||
Log.e(TAG, "无法创建摄像头捕获器");
|
||||
if (listener != null) {
|
||||
listener.onError("无法访问摄像头");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.getEglBaseContext());
|
||||
videoSource = peerConnectionFactory.createVideoSource(videoCapturer.isScreencast());
|
||||
videoCapturer.initialize(surfaceTextureHelper, context, videoSource.getCapturerObserver());
|
||||
|
||||
// 启动摄像头捕获 (720p, 30fps)
|
||||
videoCapturer.startCapture(1280, 720, 30);
|
||||
|
||||
localVideoTrack = peerConnectionFactory.createVideoTrack("ARDAMSv0", videoSource);
|
||||
localVideoTrack.setEnabled(true);
|
||||
|
||||
// 添加到本地渲染器
|
||||
if (localRenderer != null) {
|
||||
localVideoTrack.addSink(localRenderer);
|
||||
}
|
||||
|
||||
Log.d(TAG, "视频轨道创建成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建摄像头捕获器
|
||||
*/
|
||||
private VideoCapturer createCameraCapturer() {
|
||||
CameraEnumerator enumerator;
|
||||
if (Camera2Enumerator.isSupported(context)) {
|
||||
enumerator = new Camera2Enumerator(context);
|
||||
} else {
|
||||
enumerator = new Camera1Enumerator(true);
|
||||
}
|
||||
|
||||
String[] deviceNames = enumerator.getDeviceNames();
|
||||
|
||||
// 优先使用前置摄像头
|
||||
for (String deviceName : deviceNames) {
|
||||
if (useFrontCamera && enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
Log.d(TAG, "使用前置摄像头: " + deviceName);
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有前置摄像头,使用后置
|
||||
for (String deviceName : deviceNames) {
|
||||
if (!enumerator.isFrontFacing(deviceName)) {
|
||||
VideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer != null) {
|
||||
Log.d(TAG, "使用后置摄像头: " + deviceName);
|
||||
return capturer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 PeerConnection
|
||||
*/
|
||||
public void createPeerConnection() {
|
||||
Log.d(TAG, "创建 PeerConnection");
|
||||
|
||||
PeerConnection.RTCConfiguration config = new PeerConnection.RTCConfiguration(getIceServers());
|
||||
config.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
|
||||
config.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
|
||||
|
||||
peerConnection = peerConnectionFactory.createPeerConnection(config, new PeerConnection.Observer() {
|
||||
@Override
|
||||
public void onSignalingChange(PeerConnection.SignalingState signalingState) {
|
||||
Log.d(TAG, "onSignalingChange: " + signalingState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
|
||||
Log.d(TAG, "========== ICE 连接状态变化 ==========");
|
||||
Log.d(TAG, "ICE状态: " + iceConnectionState);
|
||||
// 打印详细的连接信息
|
||||
if (peerConnection != null) {
|
||||
Log.d(TAG, "信令状态: " + peerConnection.signalingState());
|
||||
Log.d(TAG, "连接状态: " + peerConnection.connectionState());
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onIceConnectionChange(iceConnectionState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceConnectionReceivingChange(boolean b) {
|
||||
Log.d(TAG, "onIceConnectionReceivingChange: " + b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
|
||||
Log.d(TAG, "onIceGatheringChange: " + iceGatheringState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidate(IceCandidate iceCandidate) {
|
||||
Log.d(TAG, "========== 生成 ICE Candidate ==========");
|
||||
Log.d(TAG, "sdpMid: " + iceCandidate.sdpMid);
|
||||
Log.d(TAG, "sdpMLineIndex: " + iceCandidate.sdpMLineIndex);
|
||||
Log.d(TAG, "candidate: " + iceCandidate.sdp);
|
||||
if (listener != null) {
|
||||
listener.onIceCandidate(iceCandidate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
|
||||
Log.d(TAG, "onIceCandidatesRemoved");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "========== 收到远程媒体流 ==========");
|
||||
Log.d(TAG, "streamId: " + mediaStream.getId());
|
||||
Log.d(TAG, "音频轨道数: " + mediaStream.audioTracks.size());
|
||||
Log.d(TAG, "视频轨道数: " + mediaStream.videoTracks.size());
|
||||
|
||||
if (listener != null) {
|
||||
listener.onRemoteStream(mediaStream);
|
||||
}
|
||||
|
||||
// 添加远程音频轨道
|
||||
if (mediaStream.audioTracks.size() > 0) {
|
||||
Log.d(TAG, "启用远程音频轨道");
|
||||
mediaStream.audioTracks.get(0).setEnabled(true);
|
||||
}
|
||||
|
||||
// 添加远程视频到渲染器
|
||||
if (mediaStream.videoTracks.size() > 0 && remoteRenderer != null) {
|
||||
Log.d(TAG, "添加远程视频到渲染器");
|
||||
mediaStream.videoTracks.get(0).addSink(remoteRenderer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveStream(MediaStream mediaStream) {
|
||||
Log.d(TAG, "onRemoveStream: " + mediaStream.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDataChannel(DataChannel dataChannel) {
|
||||
Log.d(TAG, "onDataChannel");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenegotiationNeeded() {
|
||||
Log.d(TAG, "onRenegotiationNeeded");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
|
||||
Log.d(TAG, "========== onAddTrack ==========");
|
||||
if (rtpReceiver.track() != null) {
|
||||
Log.d(TAG, "轨道类型: " + rtpReceiver.track().kind());
|
||||
Log.d(TAG, "轨道ID: " + rtpReceiver.track().id());
|
||||
|
||||
// 处理远程视频轨道
|
||||
if (rtpReceiver.track().kind().equals("video")) {
|
||||
VideoTrack remoteVideoTrack = (VideoTrack) rtpReceiver.track();
|
||||
Log.d(TAG, "收到远程视频轨道,添加到渲染器");
|
||||
if (remoteRenderer != null) {
|
||||
remoteVideoTrack.addSink(remoteRenderer);
|
||||
Log.d(TAG, "远程视频已添加到渲染器");
|
||||
} else {
|
||||
Log.e(TAG, "remoteRenderer 为空,无法显示远程视频");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加本地轨道到 PeerConnection
|
||||
Log.d(TAG, "========== 添加本地轨道 ==========");
|
||||
Log.d(TAG, "localAudioTrack=" + (localAudioTrack != null) + ", localVideoTrack=" + (localVideoTrack != null));
|
||||
|
||||
if (localAudioTrack != null) {
|
||||
peerConnection.addTrack(localAudioTrack);
|
||||
Log.d(TAG, "已添加本地音频轨道");
|
||||
} else {
|
||||
Log.w(TAG, "本地音频轨道为空,无法添加");
|
||||
}
|
||||
if (localVideoTrack != null) {
|
||||
peerConnection.addTrack(localVideoTrack);
|
||||
Log.d(TAG, "已添加本地视频轨道");
|
||||
} else {
|
||||
Log.w(TAG, "本地视频轨道为空,无法添加(如果是视频通话则有问题)");
|
||||
}
|
||||
|
||||
Log.d(TAG, "PeerConnection 创建成功, isVideoCall=" + isVideoCall);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Offer (主叫方调用)
|
||||
*/
|
||||
public void createOffer() {
|
||||
Log.d(TAG, "创建 Offer");
|
||||
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideoCall ? "true" : "false"));
|
||||
|
||||
peerConnection.createOffer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.d(TAG, "Offer 创建成功");
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.d(TAG, "本地 SDP 设置成功");
|
||||
if (listener != null) {
|
||||
listener.onOfferCreated(sessionDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置本地 SDP 失败: " + s);
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "创建 Offer 失败: " + s);
|
||||
if (listener != null) {
|
||||
listener.onError("创建 Offer 失败: " + s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Answer (被叫方调用)
|
||||
*/
|
||||
public void createAnswer() {
|
||||
Log.d(TAG, "创建 Answer");
|
||||
|
||||
MediaConstraints constraints = new MediaConstraints();
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
|
||||
constraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", isVideoCall ? "true" : "false"));
|
||||
|
||||
peerConnection.createAnswer(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {
|
||||
Log.d(TAG, "Answer 创建成功");
|
||||
peerConnection.setLocalDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.d(TAG, "本地 SDP 设置成功");
|
||||
if (listener != null) {
|
||||
listener.onAnswerCreated(sessionDescription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置本地 SDP 失败: " + s);
|
||||
}
|
||||
}, sessionDescription);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {
|
||||
Log.e(TAG, "创建 Answer 失败: " + s);
|
||||
if (listener != null) {
|
||||
listener.onError("创建 Answer 失败: " + s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {}
|
||||
}, constraints);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置远程 SDP
|
||||
*/
|
||||
public void setRemoteDescription(SessionDescription sdp) {
|
||||
Log.d(TAG, "设置远程 SDP, type=" + sdp.type);
|
||||
|
||||
peerConnection.setRemoteDescription(new SdpObserver() {
|
||||
@Override
|
||||
public void onCreateSuccess(SessionDescription sessionDescription) {}
|
||||
|
||||
@Override
|
||||
public void onSetSuccess() {
|
||||
Log.d(TAG, "远程 SDP 设置成功");
|
||||
// 如果是 Offer,需要创建 Answer
|
||||
if (sdp.type == SessionDescription.Type.OFFER) {
|
||||
createAnswer();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateFailure(String s) {}
|
||||
|
||||
@Override
|
||||
public void onSetFailure(String s) {
|
||||
Log.e(TAG, "设置远程 SDP 失败: " + s);
|
||||
if (listener != null) {
|
||||
listener.onError("设置远程 SDP 失败: " + s);
|
||||
}
|
||||
}
|
||||
}, sdp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 ICE Candidate
|
||||
*/
|
||||
public void addIceCandidate(IceCandidate candidate) {
|
||||
Log.d(TAG, "========== 添加远程 ICE Candidate ==========");
|
||||
Log.d(TAG, "sdpMid: " + candidate.sdpMid);
|
||||
Log.d(TAG, "sdpMLineIndex: " + candidate.sdpMLineIndex);
|
||||
Log.d(TAG, "candidate: " + candidate.sdp);
|
||||
if (peerConnection != null) {
|
||||
boolean result = peerConnection.addIceCandidate(candidate);
|
||||
Log.d(TAG, "添加结果: " + result);
|
||||
} else {
|
||||
Log.e(TAG, "peerConnection 为 null,无法添加 ICE Candidate");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换静音
|
||||
*/
|
||||
public void setMuted(boolean muted) {
|
||||
this.isMuted = muted;
|
||||
if (localAudioTrack != null) {
|
||||
localAudioTrack.setEnabled(!muted);
|
||||
}
|
||||
Log.d(TAG, "静音状态: " + muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换视频
|
||||
*/
|
||||
public void setVideoEnabled(boolean enabled) {
|
||||
this.isVideoEnabled = enabled;
|
||||
if (localVideoTrack != null) {
|
||||
localVideoTrack.setEnabled(enabled);
|
||||
}
|
||||
Log.d(TAG, "视频状态: " + enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换摄像头
|
||||
*/
|
||||
public void switchCamera() {
|
||||
if (videoCapturer instanceof org.webrtc.CameraVideoCapturer) {
|
||||
((org.webrtc.CameraVideoCapturer) videoCapturer).switchCamera(null);
|
||||
useFrontCamera = !useFrontCamera;
|
||||
if (localRenderer != null) {
|
||||
localRenderer.setMirror(useFrontCamera);
|
||||
}
|
||||
Log.d(TAG, "切换摄像头, 使用前置: " + useFrontCamera);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
public void release() {
|
||||
Log.d(TAG, "释放 WebRTC 资源");
|
||||
|
||||
if (videoCapturer != null) {
|
||||
try {
|
||||
videoCapturer.stopCapture();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "停止摄像头捕获失败", e);
|
||||
}
|
||||
videoCapturer.dispose();
|
||||
videoCapturer = null;
|
||||
}
|
||||
|
||||
if (surfaceTextureHelper != null) {
|
||||
surfaceTextureHelper.dispose();
|
||||
surfaceTextureHelper = null;
|
||||
}
|
||||
|
||||
if (localVideoTrack != null) {
|
||||
localVideoTrack.dispose();
|
||||
localVideoTrack = null;
|
||||
}
|
||||
|
||||
if (videoSource != null) {
|
||||
videoSource.dispose();
|
||||
videoSource = null;
|
||||
}
|
||||
|
||||
if (localAudioTrack != null) {
|
||||
localAudioTrack.dispose();
|
||||
localAudioTrack = null;
|
||||
}
|
||||
|
||||
if (audioSource != null) {
|
||||
audioSource.dispose();
|
||||
audioSource = null;
|
||||
}
|
||||
|
||||
if (peerConnection != null) {
|
||||
peerConnection.close();
|
||||
peerConnection = null;
|
||||
}
|
||||
|
||||
if (peerConnectionFactory != null) {
|
||||
peerConnectionFactory.dispose();
|
||||
peerConnectionFactory = null;
|
||||
}
|
||||
|
||||
if (localRenderer != null) {
|
||||
localRenderer.release();
|
||||
}
|
||||
|
||||
if (remoteRenderer != null) {
|
||||
remoteRenderer.release();
|
||||
}
|
||||
|
||||
if (eglBase != null) {
|
||||
eglBase.release();
|
||||
eglBase = null;
|
||||
}
|
||||
|
||||
Log.d(TAG, "WebRTC 资源释放完成");
|
||||
}
|
||||
|
||||
public boolean isMuted() {
|
||||
return isMuted;
|
||||
}
|
||||
|
||||
public boolean isVideoEnabled() {
|
||||
return isVideoEnabled;
|
||||
}
|
||||
|
||||
public EglBase getEglBase() {
|
||||
return eglBase;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package com.example.livestreaming.call;
|
||||
|
||||
/**
|
||||
* WebRTC 配置
|
||||
* 部署时修改这里的服务器地址
|
||||
*/
|
||||
public class WebRTCConfig {
|
||||
|
||||
// ============ STUN 服务器 ============
|
||||
// STUN 用于获取设备的公网IP,帮助建立P2P连接
|
||||
public static final String[] STUN_SERVERS = {
|
||||
"stun:stun.l.google.com:19302", // Google STUN(国内可能不稳定)
|
||||
"stun:stun.qq.com:3478", // 腾讯 STUN(国内推荐)
|
||||
"stun:stun.miwifi.com:3478" // 小米 STUN(国内推荐)
|
||||
};
|
||||
|
||||
// ============ TURN 服务器 ============
|
||||
// TURN 用于在P2P连接失败时进行中继转发
|
||||
|
||||
// 你的服务器TURN地址
|
||||
public static final String TURN_SERVER_URL = "turn:1.15.149.240:3478";
|
||||
|
||||
// TURN 服务器用户名
|
||||
public static final String TURN_USERNAME = "turnuser";
|
||||
|
||||
// TURN 服务器密码
|
||||
public static final String TURN_PASSWORD = "TurnPass123456";
|
||||
|
||||
// ============ 使用说明 ============
|
||||
/*
|
||||
* 局域网测试:
|
||||
* - 不需要修改,当前配置即可使用
|
||||
*
|
||||
* 部署到公网服务器:
|
||||
* 1. 在服务器安装 coturn (TURN服务器)
|
||||
* 2. 修改上面的配置:
|
||||
* TURN_SERVER_URL = "turn:你的服务器IP:3478"
|
||||
* TURN_USERNAME = "你设置的用户名"
|
||||
* TURN_PASSWORD = "你设置的密码"
|
||||
*
|
||||
* 宝塔安装 coturn 步骤:
|
||||
* 1. SSH执行: yum install -y coturn (CentOS) 或 apt install -y coturn (Ubuntu)
|
||||
* 2. 编辑 /etc/turnserver.conf:
|
||||
* listening-port=3478
|
||||
* external-ip=你的公网IP
|
||||
* realm=你的公网IP
|
||||
* lt-cred-mech
|
||||
* user=用户名:密码
|
||||
* 3. 宝塔放行端口: 3478(TCP/UDP), 49152-65535(UDP)
|
||||
* 4. 启动: systemctl start coturn
|
||||
*/
|
||||
}
|
||||
|
|
@ -1,32 +1,39 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class ConversationResponse {
|
||||
private Integer id;
|
||||
private Integer targetUserId;
|
||||
private String targetUserName;
|
||||
private String targetUserAvatar;
|
||||
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
|
||||
@SerializedName("title")
|
||||
private String title;
|
||||
|
||||
@SerializedName("lastMessage")
|
||||
private String lastMessage;
|
||||
private String lastMessageTime;
|
||||
|
||||
@SerializedName("timeText")
|
||||
private String timeText;
|
||||
|
||||
@SerializedName("unreadCount")
|
||||
private Integer unreadCount;
|
||||
|
||||
public Integer getId() { return id; }
|
||||
public void setId(Integer id) { this.id = id; }
|
||||
@SerializedName("muted")
|
||||
private Boolean muted;
|
||||
|
||||
public Integer getTargetUserId() { return targetUserId; }
|
||||
public void setTargetUserId(Integer targetUserId) { this.targetUserId = targetUserId; }
|
||||
@SerializedName("avatarUrl")
|
||||
private String avatarUrl;
|
||||
|
||||
public String getTargetUserName() { return targetUserName; }
|
||||
public void setTargetUserName(String targetUserName) { this.targetUserName = targetUserName; }
|
||||
|
||||
public String getTargetUserAvatar() { return targetUserAvatar; }
|
||||
public void setTargetUserAvatar(String targetUserAvatar) { this.targetUserAvatar = targetUserAvatar; }
|
||||
@SerializedName("otherUserId")
|
||||
private Integer otherUserId;
|
||||
|
||||
public String getId() { return id; }
|
||||
public String getTitle() { return title; }
|
||||
public String getLastMessage() { return lastMessage; }
|
||||
public void setLastMessage(String lastMessage) { this.lastMessage = lastMessage; }
|
||||
|
||||
public String getLastMessageTime() { return lastMessageTime; }
|
||||
public void setLastMessageTime(String lastMessageTime) { this.lastMessageTime = lastMessageTime; }
|
||||
|
||||
public String getTimeText() { return timeText; }
|
||||
public Integer getUnreadCount() { return unreadCount; }
|
||||
public void setUnreadCount(Integer unreadCount) { this.unreadCount = unreadCount; }
|
||||
public Boolean getMuted() { return muted; }
|
||||
public String getAvatarUrl() { return avatarUrl; }
|
||||
public Integer getOtherUserId() { return otherUserId; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,32 @@
|
|||
package com.example.livestreaming.net;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.List;
|
||||
|
||||
public class PageResponse<T> {
|
||||
|
||||
@SerializedName("list")
|
||||
private List<T> list;
|
||||
|
||||
@SerializedName("total")
|
||||
private Long total;
|
||||
|
||||
@SerializedName("page")
|
||||
private Integer page;
|
||||
|
||||
@SerializedName("limit")
|
||||
private Integer limit;
|
||||
private Integer total;
|
||||
|
||||
@SerializedName("totalPage")
|
||||
private Integer totalPage;
|
||||
|
||||
public List<T> getList() { return list; }
|
||||
public void setList(List<T> list) { this.list = list; }
|
||||
|
||||
public Long getTotal() { return total; }
|
||||
public Integer getPage() { return page; }
|
||||
public void setPage(Integer page) { this.page = page; }
|
||||
|
||||
public Integer getLimit() { return limit; }
|
||||
public void setLimit(Integer limit) { this.limit = limit; }
|
||||
|
||||
public Integer getTotal() { return total; }
|
||||
public void setTotal(Integer total) { this.total = total; }
|
||||
|
||||
public Integer getTotalPage() { return totalPage; }
|
||||
public void setTotalPage(Integer totalPage) { this.totalPage = totalPage; }
|
||||
|
||||
public boolean hasMore() {
|
||||
return page != null && totalPage != null && page < totalPage;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#8B4513" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFFEF3" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FFD700" />
|
||||
</shape>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#FFF8E1" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FFCC00"
|
||||
android:dashWidth="4dp"
|
||||
android:dashGap="2dp" />
|
||||
</shape>
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
android:layout_height="match_parent"
|
||||
android:background="#1A1A2E">
|
||||
|
||||
<!-- 背景模糊头像(语音通话时显示) -->
|
||||
<!-- 背景模糊头像 -->
|
||||
<ImageView
|
||||
android:id="@+id/ivBackgroundAvatar"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -13,22 +13,21 @@
|
|||
android:scaleType="centerCrop"
|
||||
android:alpha="0.3" />
|
||||
|
||||
<!-- 视频通话时的远程视频容器 -->
|
||||
<FrameLayout
|
||||
android:id="@+id/layoutRemoteVideo"
|
||||
<!-- 视频通话时的远程视频 -->
|
||||
<SurfaceView
|
||||
android:id="@+id/remoteVideoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 视频通话时的本地视频容器(小窗口) -->
|
||||
<FrameLayout
|
||||
android:id="@+id/layoutLocalVideo"
|
||||
<!-- 视频通话时的本地视频(小窗口) -->
|
||||
<SurfaceView
|
||||
android:id="@+id/localVideoView"
|
||||
android:layout_width="120dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_marginTop="80dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="#333333"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="许下你的愿望"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/editWish"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="写下你的愿望..."
|
||||
android:minLines="3"
|
||||
android:gravity="top" />
|
||||
</LinearLayout>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:src="@android:drawable/btn_star_big_on" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="愿望已挂上许愿树"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="愿你心想事成"
|
||||
android:textColor="#666"
|
||||
android:layout_marginTop="8dp" />
|
||||
</LinearLayout>
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@mipmap/ic_launcher" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_user_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#999"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_like_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@android:drawable/btn_star"
|
||||
android:drawablePadding="4dp"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_comment_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:drawableStart="@android:drawable/ic_menu_edit"
|
||||
android:drawablePadding="4dp"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
@ -1,82 +1,119 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="4dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="2dp">
|
||||
android:layout_margin="6dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<FrameLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/coverImage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:scaleType="centerCrop" />
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/bg_cover_placeholder"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintDimensionRatio="H,4:3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/liveBadge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/holo_red_light"
|
||||
android:paddingHorizontal="6dp"
|
||||
android:paddingVertical="2dp"
|
||||
android:text="直播中"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:text="LIVE"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="10sp"
|
||||
android:visibility="gone" />
|
||||
android:textSize="11sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="@id/coverImage"
|
||||
app:layout_constraintTop_toTopOf="@id/coverImage" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/infoContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="#80000000"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="10dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/coverImage">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/roomTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
android:maxLines="2"
|
||||
android:text="王者荣耀陪练"
|
||||
android:textColor="#111111"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<ImageView
|
||||
android:id="@+id/streamerAvatar"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_avatar_circle"
|
||||
android:padding="3dp"
|
||||
android:src="@drawable/ic_account_circle_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/streamerName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="虚拟主播"
|
||||
android:textColor="#666666"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@id/likeIcon"
|
||||
app:layout_constraintStart_toEndOf="@id/streamerAvatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/streamerName"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textColor="#CCC"
|
||||
android:textSize="12sp" />
|
||||
<ImageView
|
||||
android:id="@+id/likeIcon"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@drawable/ic_heart_24"
|
||||
app:layout_constraintEnd_toStartOf="@id/likeCount"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/likeIcon"
|
||||
android:layout_width="14dp"
|
||||
android:layout_height="14dp"
|
||||
android:src="@android:drawable/btn_star_big_on"
|
||||
android:visibility="gone" />
|
||||
<TextView
|
||||
android:id="@+id/likeCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="184"
|
||||
android:textColor="#888888"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/likeCount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:textColor="#CCC"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
|
||||
# 使用 Android Studio 自带的 JDK 17
|
||||
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
|
||||
|
||||
systemProp.gradle.wrapperUser=myuser
|
||||
systemProp.gradle.wrapperPassword=mypassword
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal EnableDelayedExpansion
|
||||
|
||||
REM ============================================
|
||||
REM 直播系统一键部署脚本 (Windows版)
|
||||
REM 服务器地址: 1.15.149.240
|
||||
REM ============================================
|
||||
|
||||
set SERVER_IP=1.15.149.240
|
||||
set SERVER_USER=root
|
||||
set DEPLOY_PATH=/opt/zhibo
|
||||
|
||||
echo ==========================================
|
||||
echo 直播系统部署脚本 (Windows版)
|
||||
echo 目标服务器: %SERVER_IP%
|
||||
echo ==========================================
|
||||
echo.
|
||||
|
||||
REM 检查scp和ssh命令
|
||||
where scp >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 未找到scp命令
|
||||
echo 请安装OpenSSH客户端或使用Git Bash运行deploy-to-server.sh
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/6] 测试SSH连接...
|
||||
ssh -o ConnectTimeout=10 %SERVER_USER%@%SERVER_IP% "echo SSH连接成功"
|
||||
if %errorlevel% neq 0 (
|
||||
echo 错误: 无法连接到服务器
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [2/6] 创建服务器目录结构...
|
||||
ssh %SERVER_USER%@%SERVER_IP% "mkdir -p /opt/zhibo/admin-api /opt/zhibo/front-api /opt/zhibo/admin-web /opt/zhibo/logs /opt/zhibo/scripts"
|
||||
|
||||
echo.
|
||||
echo [3/6] 上传后端JAR包...
|
||||
echo - 上传 Admin API...
|
||||
scp Zhibo/zhibo-h/crmeb-admin/target/Crmeb-admin.jar %SERVER_USER%@%SERVER_IP%:/opt/zhibo/admin-api/
|
||||
|
||||
echo - 上传 Front API...
|
||||
scp Zhibo/zhibo-h/crmeb-front/target/Crmeb-front.jar %SERVER_USER%@%SERVER_IP%:/opt/zhibo/front-api/
|
||||
|
||||
echo.
|
||||
echo [4/6] 上传前端管理界面...
|
||||
scp -r Zhibo/admin/dist/* %SERVER_USER%@%SERVER_IP%:/opt/zhibo/admin-web/
|
||||
|
||||
echo.
|
||||
echo [5/6] 创建服务启动脚本...
|
||||
ssh %SERVER_USER%@%SERVER_IP% "cat > /opt/zhibo/scripts/start-admin-api.sh" < server-scripts/start-admin-api.sh
|
||||
ssh %SERVER_USER%@%SERVER_IP% "cat > /opt/zhibo/scripts/start-front-api.sh" < server-scripts/start-front-api.sh
|
||||
ssh %SERVER_USER%@%SERVER_IP% "chmod +x /opt/zhibo/scripts/*.sh"
|
||||
|
||||
echo.
|
||||
echo [6/6] 上传Nginx配置...
|
||||
scp server-scripts/zhibo.nginx.conf %SERVER_USER%@%SERVER_IP%:/etc/nginx/conf.d/zhibo.conf
|
||||
ssh %SERVER_USER%@%SERVER_IP% "nginx -t && systemctl reload nginx"
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 部署完成!
|
||||
echo ==========================================
|
||||
echo.
|
||||
echo 请在服务器上执行以下命令启动服务:
|
||||
echo ssh %SERVER_USER%@%SERVER_IP%
|
||||
echo cd /opt/zhibo/scripts
|
||||
echo ./start-all.sh
|
||||
echo.
|
||||
echo 服务访问地址:
|
||||
echo - 管理后台: http://%SERVER_IP%
|
||||
echo - Admin API: http://%SERVER_IP%:30001
|
||||
echo - Front API: http://%SERVER_IP%:8081
|
||||
echo.
|
||||
pause
|
||||
|
|
@ -1,269 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ============================================
|
||||
# 直播系统一键部署脚本
|
||||
# 服务器地址: 1.15.149.240
|
||||
# ============================================
|
||||
|
||||
set -e
|
||||
|
||||
# 配置变量
|
||||
SERVER_IP="1.15.149.240"
|
||||
SERVER_USER="root"
|
||||
DEPLOY_PATH="/opt/zhibo"
|
||||
ADMIN_API_PORT=30001
|
||||
FRONT_API_PORT=8081
|
||||
|
||||
echo "=========================================="
|
||||
echo " 直播系统部署脚本"
|
||||
echo " 目标服务器: $SERVER_IP"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查SSH连接
|
||||
echo ""
|
||||
echo "[1/6] 检查SSH连接..."
|
||||
ssh -o ConnectTimeout=10 ${SERVER_USER}@${SERVER_IP} "echo 'SSH连接成功'" || {
|
||||
echo "错误: 无法连接到服务器 ${SERVER_IP}"
|
||||
echo "请确保:"
|
||||
echo " 1. 服务器IP正确"
|
||||
echo " 2. SSH密钥已配置或准备好密码"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 在服务器上创建目录结构
|
||||
echo ""
|
||||
echo "[2/6] 创建服务器目录结构..."
|
||||
ssh ${SERVER_USER}@${SERVER_IP} << 'ENDSSH'
|
||||
mkdir -p /opt/zhibo/admin-api
|
||||
mkdir -p /opt/zhibo/front-api
|
||||
mkdir -p /opt/zhibo/admin-web
|
||||
mkdir -p /opt/zhibo/logs
|
||||
mkdir -p /opt/zhibo/scripts
|
||||
echo "目录创建完成"
|
||||
ENDSSH
|
||||
|
||||
# 上传后端JAR包
|
||||
echo ""
|
||||
echo "[3/6] 上传后端JAR包..."
|
||||
echo " - 上传 Admin API (Crmeb-admin.jar)..."
|
||||
scp Zhibo/zhibo-h/crmeb-admin/target/Crmeb-admin.jar ${SERVER_USER}@${SERVER_IP}:/opt/zhibo/admin-api/
|
||||
|
||||
echo " - 上传 Front API (Crmeb-front.jar)..."
|
||||
scp Zhibo/zhibo-h/crmeb-front/target/Crmeb-front.jar ${SERVER_USER}@${SERVER_IP}:/opt/zhibo/front-api/
|
||||
|
||||
# 上传前端文件
|
||||
echo ""
|
||||
echo "[4/6] 上传前端管理界面..."
|
||||
scp -r Zhibo/admin/dist/* ${SERVER_USER}@${SERVER_IP}:/opt/zhibo/admin-web/
|
||||
|
||||
# 创建启动脚本
|
||||
echo ""
|
||||
echo "[5/6] 创建服务启动脚本..."
|
||||
ssh ${SERVER_USER}@${SERVER_IP} << 'ENDSSH'
|
||||
# 创建Admin API启动脚本
|
||||
cat > /opt/zhibo/scripts/start-admin-api.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
APP_NAME="Crmeb-admin"
|
||||
APP_PATH="/opt/zhibo/admin-api"
|
||||
LOG_PATH="/opt/zhibo/logs"
|
||||
JAR_FILE="${APP_PATH}/${APP_NAME}.jar"
|
||||
|
||||
# 停止旧进程
|
||||
pid=$(ps -ef | grep "${APP_NAME}.jar" | grep -v grep | awk '{print $2}')
|
||||
if [ -n "$pid" ]; then
|
||||
echo "停止旧进程: $pid"
|
||||
kill -9 $pid
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 启动新进程
|
||||
if [ -f "$JAR_FILE" ]; then
|
||||
echo "启动 ${APP_NAME}..."
|
||||
nohup java -Xms512m -Xmx1024m -jar $JAR_FILE \
|
||||
--spring.redis.host=127.0.0.1 \
|
||||
> ${LOG_PATH}/admin-api.log 2>&1 &
|
||||
echo "Admin API 启动成功,端口: 30001"
|
||||
else
|
||||
echo "错误: JAR文件不存在: $JAR_FILE"
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
# 创建Front API启动脚本
|
||||
cat > /opt/zhibo/scripts/start-front-api.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
APP_NAME="Crmeb-front"
|
||||
APP_PATH="/opt/zhibo/front-api"
|
||||
LOG_PATH="/opt/zhibo/logs"
|
||||
JAR_FILE="${APP_PATH}/${APP_NAME}.jar"
|
||||
|
||||
# 停止旧进程
|
||||
pid=$(ps -ef | grep "${APP_NAME}.jar" | grep -v grep | awk '{print $2}')
|
||||
if [ -n "$pid" ]; then
|
||||
echo "停止旧进程: $pid"
|
||||
kill -9 $pid
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# 启动新进程
|
||||
if [ -f "$JAR_FILE" ]; then
|
||||
echo "启动 ${APP_NAME}..."
|
||||
nohup java -Xms512m -Xmx1024m -jar $JAR_FILE \
|
||||
--spring.redis.host=127.0.0.1 \
|
||||
> ${LOG_PATH}/front-api.log 2>&1 &
|
||||
echo "Front API 启动成功,端口: 8081"
|
||||
else
|
||||
echo "错误: JAR文件不存在: $JAR_FILE"
|
||||
exit 1
|
||||
fi
|
||||
EOF
|
||||
|
||||
# 创建停止脚本
|
||||
cat > /opt/zhibo/scripts/stop-all.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "停止所有服务..."
|
||||
pkill -f "Crmeb-admin.jar" 2>/dev/null && echo "Admin API 已停止" || echo "Admin API 未运行"
|
||||
pkill -f "Crmeb-front.jar" 2>/dev/null && echo "Front API 已停止" || echo "Front API 未运行"
|
||||
echo "完成"
|
||||
EOF
|
||||
|
||||
# 创建一键启动脚本
|
||||
cat > /opt/zhibo/scripts/start-all.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "=========================================="
|
||||
echo " 启动所有直播系统服务"
|
||||
echo "=========================================="
|
||||
cd /opt/zhibo/scripts
|
||||
./start-admin-api.sh
|
||||
sleep 5
|
||||
./start-front-api.sh
|
||||
echo ""
|
||||
echo "所有服务启动完成!"
|
||||
echo " - Admin API: http://localhost:30001"
|
||||
echo " - Front API: http://localhost:8081"
|
||||
EOF
|
||||
|
||||
# 创建状态检查脚本
|
||||
cat > /opt/zhibo/scripts/status.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "=========================================="
|
||||
echo " 服务状态检查"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Admin API (端口 30001):"
|
||||
if pgrep -f "Crmeb-admin.jar" > /dev/null; then
|
||||
echo " 状态: 运行中"
|
||||
pid=$(pgrep -f "Crmeb-admin.jar")
|
||||
echo " PID: $pid"
|
||||
else
|
||||
echo " 状态: 未运行"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Front API (端口 8081):"
|
||||
if pgrep -f "Crmeb-front.jar" > /dev/null; then
|
||||
echo " 状态: 运行中"
|
||||
pid=$(pgrep -f "Crmeb-front.jar")
|
||||
echo " PID: $pid"
|
||||
else
|
||||
echo " 状态: 未运行"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "端口监听状态:"
|
||||
netstat -tlnp 2>/dev/null | grep -E "30001|8081" || ss -tlnp | grep -E "30001|8081"
|
||||
EOF
|
||||
|
||||
chmod +x /opt/zhibo/scripts/*.sh
|
||||
echo "启动脚本创建完成"
|
||||
ENDSSH
|
||||
|
||||
# 创建Nginx配置
|
||||
echo ""
|
||||
echo "[6/6] 配置Nginx..."
|
||||
ssh ${SERVER_USER}@${SERVER_IP} << 'ENDSSH'
|
||||
# 检查Nginx是否安装
|
||||
if ! command -v nginx &> /dev/null; then
|
||||
echo "Nginx未安装,正在安装..."
|
||||
apt-get update && apt-get install -y nginx || yum install -y nginx
|
||||
fi
|
||||
|
||||
# 创建Nginx配置
|
||||
cat > /etc/nginx/conf.d/zhibo.conf << 'EOF'
|
||||
# 直播系统 - 管理后台前端
|
||||
server {
|
||||
listen 80;
|
||||
server_name admin.zhibo.local; # 可以改成你的域名
|
||||
|
||||
root /opt/zhibo/admin-web;
|
||||
index index.html;
|
||||
|
||||
# 前端静态文件
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 代理Admin API
|
||||
location /api/admin/ {
|
||||
proxy_pass http://127.0.0.1:30001/api/admin/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
# 代理公共API
|
||||
location /api/public/ {
|
||||
proxy_pass http://127.0.0.1:30001/api/public/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
# 直播系统 - 前端API (供APP调用)
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
# 代理Front API
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 测试Nginx配置
|
||||
nginx -t && systemctl reload nginx
|
||||
echo "Nginx配置完成"
|
||||
ENDSSH
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 部署完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "接下来请在服务器上执行以下命令启动服务:"
|
||||
echo ""
|
||||
echo " ssh ${SERVER_USER}@${SERVER_IP}"
|
||||
echo " cd /opt/zhibo/scripts"
|
||||
echo " ./start-all.sh"
|
||||
echo ""
|
||||
echo "服务访问地址:"
|
||||
echo " - 管理后台: http://${SERVER_IP}"
|
||||
echo " - Admin API: http://${SERVER_IP}:30001"
|
||||
echo " - Front API: http://${SERVER_IP}:8081 (或通过Nginx 8080端口)"
|
||||
echo ""
|
||||
echo "常用命令:"
|
||||
echo " - 启动所有服务: /opt/zhibo/scripts/start-all.sh"
|
||||
echo " - 停止所有服务: /opt/zhibo/scripts/stop-all.sh"
|
||||
echo " - 查看服务状态: /opt/zhibo/scripts/status.sh"
|
||||
echo " - 查看日志: tail -f /opt/zhibo/logs/admin-api.log"
|
||||
echo " tail -f /opt/zhibo/logs/front-api.log"
|
||||
echo ""
|
||||
Binary file not shown.
|
|
@ -1,25 +0,0 @@
|
|||
# ==========================================
|
||||
# 直播服务 Docker 环境配置
|
||||
# 复制此文件为 .env 并修改配置
|
||||
# ==========================================
|
||||
|
||||
# ========== 服务器公网地址 ==========
|
||||
# 你的服务器公网 IP
|
||||
PUBLIC_SRS_HOST=1.15.149.240
|
||||
|
||||
# ========== 端口配置 ==========
|
||||
# API 服务端口
|
||||
API_EXPOSE_PORT=25001
|
||||
|
||||
# SRS RTMP 端口(推流用)
|
||||
SRS_RTMP_EXPOSE_PORT=25002
|
||||
|
||||
# SRS HTTP 端口(拉流用)
|
||||
SRS_HTTP_EXPOSE_PORT=25003
|
||||
|
||||
# SRS API 端口
|
||||
SRS_API_EXPOSE_PORT=1985
|
||||
|
||||
# ========== 公网端口(如果使用端口映射,修改这里)==========
|
||||
PUBLIC_SRS_RTMP_PORT=25002
|
||||
PUBLIC_SRS_HTTP_PORT=25003
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
REM ==========================================
|
||||
REM 直播服务 Docker 部署脚本 (Windows)
|
||||
REM ==========================================
|
||||
|
||||
echo ==========================================
|
||||
echo 直播服务 Docker 部署
|
||||
echo ==========================================
|
||||
|
||||
REM 检查 .env 文件
|
||||
if not exist ".env" (
|
||||
echo ❌ 错误: 未找到 .env 文件
|
||||
echo 请复制 .env.example 为 .env 并修改配置
|
||||
echo copy .env.example .env
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 🔨 构建镜像...
|
||||
docker-compose build
|
||||
|
||||
echo.
|
||||
echo 🚀 启动服务...
|
||||
docker-compose up -d
|
||||
|
||||
echo.
|
||||
echo ✅ 部署完成!
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 服务状态
|
||||
echo ==========================================
|
||||
docker-compose ps
|
||||
|
||||
echo.
|
||||
echo ==========================================
|
||||
echo 常用命令
|
||||
echo ==========================================
|
||||
echo 查看日志: docker-compose logs -f
|
||||
echo 停止服务: docker-compose down
|
||||
echo 重启服务: docker-compose restart
|
||||
echo 查看状态: docker-compose ps
|
||||
echo.
|
||||
|
||||
pause
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
#!/bin/bash
|
||||
# ==========================================
|
||||
# 直播服务 Docker 部署脚本
|
||||
# ==========================================
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " 直播服务 Docker 部署"
|
||||
echo "=========================================="
|
||||
|
||||
# 检查 .env 文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "❌ 错误: 未找到 .env 文件"
|
||||
echo "请复制 .env.example 为 .env 并修改配置"
|
||||
echo " cp .env.example .env"
|
||||
echo " nano .env"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 加载环境变量
|
||||
source .env
|
||||
|
||||
echo ""
|
||||
echo "📋 当前配置:"
|
||||
echo " - 公网地址: ${PUBLIC_SRS_HOST:-未设置}"
|
||||
echo " - API 端口: ${API_EXPOSE_PORT:-25001}"
|
||||
echo " - RTMP 端口: ${SRS_RTMP_EXPOSE_PORT:-25002}"
|
||||
echo " - HTTP 端口: ${SRS_HTTP_EXPOSE_PORT:-25003}"
|
||||
echo ""
|
||||
|
||||
# 确认部署
|
||||
read -p "是否继续部署? (y/n): " confirm
|
||||
if [ "$confirm" != "y" ]; then
|
||||
echo "已取消部署"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🔨 构建镜像..."
|
||||
docker-compose build
|
||||
|
||||
echo ""
|
||||
echo "🚀 启动服务..."
|
||||
docker-compose up -d
|
||||
|
||||
echo ""
|
||||
echo "✅ 部署完成!"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 服务状态"
|
||||
echo "=========================================="
|
||||
docker-compose ps
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 访问地址"
|
||||
echo "=========================================="
|
||||
echo " API 服务: http://${PUBLIC_SRS_HOST:-localhost}:${API_EXPOSE_PORT:-25001}"
|
||||
echo " RTMP 推流: rtmp://${PUBLIC_SRS_HOST:-localhost}:${SRS_RTMP_EXPOSE_PORT:-25002}/live/[streamKey]"
|
||||
echo " HTTP 拉流: http://${PUBLIC_SRS_HOST:-localhost}:${SRS_HTTP_EXPOSE_PORT:-25003}/live/[streamKey].flv"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " 常用命令"
|
||||
echo "=========================================="
|
||||
echo " 查看日志: docker-compose logs -f"
|
||||
echo " 停止服务: docker-compose down"
|
||||
echo " 重启服务: docker-compose restart"
|
||||
echo " 查看状态: docker-compose ps"
|
||||
echo ""
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
# 直播系统部署指南
|
||||
|
||||
## 服务器信息
|
||||
- **IP地址**: 1.15.149.240
|
||||
- **部署目录**: /opt/zhibo
|
||||
|
||||
## 需要部署的服务
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| Admin API | 30001 | 管理后台API |
|
||||
| Front API | 8081 | 前端/APP API |
|
||||
| Admin Web | 80 | 管理后台界面 |
|
||||
|
||||
---
|
||||
|
||||
## 第一步:上传文件到服务器
|
||||
|
||||
### 方法1:使用SCP命令(推荐)
|
||||
|
||||
打开PowerShell或CMD,在项目根目录执行:
|
||||
|
||||
```powershell
|
||||
# 1. 创建服务器目录
|
||||
ssh root@1.15.149.240 "mkdir -p /opt/zhibo/{admin-api,front-api,admin-web,logs,scripts}"
|
||||
|
||||
# 2. 上传Admin API JAR包
|
||||
scp Zhibo/zhibo-h/crmeb-admin/target/Crmeb-admin.jar root@1.15.149.240:/opt/zhibo/admin-api/
|
||||
|
||||
# 3. 上传Front API JAR包
|
||||
scp Zhibo/zhibo-h/crmeb-front/target/Crmeb-front.jar root@1.15.149.240:/opt/zhibo/front-api/
|
||||
|
||||
# 4. 上传前端文件
|
||||
scp -r Zhibo/admin/dist/* root@1.15.149.240:/opt/zhibo/admin-web/
|
||||
```
|
||||
|
||||
### 方法2:使用SFTP工具
|
||||
|
||||
使用FileZilla、WinSCP等工具连接服务器,手动上传:
|
||||
- `Zhibo/zhibo-h/crmeb-admin/target/Crmeb-admin.jar` → `/opt/zhibo/admin-api/`
|
||||
- `Zhibo/zhibo-h/crmeb-front/target/Crmeb-front.jar` → `/opt/zhibo/front-api/`
|
||||
- `Zhibo/admin/dist/` 目录下所有文件 → `/opt/zhibo/admin-web/`
|
||||
|
||||
---
|
||||
|
||||
## 第二步:SSH登录服务器配置
|
||||
|
||||
```bash
|
||||
ssh root@1.15.149.240
|
||||
```
|
||||
|
||||
### 2.1 创建启动脚本
|
||||
|
||||
```bash
|
||||
# 创建Admin API启动脚本
|
||||
cat > /opt/zhibo/scripts/start-admin-api.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
APP_NAME="Crmeb-admin"
|
||||
JAR_FILE="/opt/zhibo/admin-api/${APP_NAME}.jar"
|
||||
LOG_FILE="/opt/zhibo/logs/admin-api.log"
|
||||
|
||||
# 停止旧进程
|
||||
pid=$(pgrep -f "${APP_NAME}.jar")
|
||||
[ -n "$pid" ] && kill -9 $pid && echo "停止旧进程: $pid" && sleep 2
|
||||
|
||||
# 启动
|
||||
nohup java -Xms512m -Xmx1024m -jar $JAR_FILE \
|
||||
--spring.redis.host=127.0.0.1 \
|
||||
> $LOG_FILE 2>&1 &
|
||||
echo "Admin API 启动成功,端口: 30001"
|
||||
EOF
|
||||
|
||||
# 创建Front API启动脚本
|
||||
cat > /opt/zhibo/scripts/start-front-api.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
APP_NAME="Crmeb-front"
|
||||
JAR_FILE="/opt/zhibo/front-api/${APP_NAME}.jar"
|
||||
LOG_FILE="/opt/zhibo/logs/front-api.log"
|
||||
|
||||
# 停止旧进程
|
||||
pid=$(pgrep -f "${APP_NAME}.jar")
|
||||
[ -n "$pid" ] && kill -9 $pid && echo "停止旧进程: $pid" && sleep 2
|
||||
|
||||
# 启动
|
||||
nohup java -Xms512m -Xmx1024m -jar $JAR_FILE \
|
||||
--spring.redis.host=127.0.0.1 \
|
||||
> $LOG_FILE 2>&1 &
|
||||
echo "Front API 启动成功,端口: 8081"
|
||||
EOF
|
||||
|
||||
# 创建一键启动脚本
|
||||
cat > /opt/zhibo/scripts/start-all.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
echo "启动所有服务..."
|
||||
/opt/zhibo/scripts/start-admin-api.sh
|
||||
sleep 5
|
||||
/opt/zhibo/scripts/start-front-api.sh
|
||||
echo "完成!"
|
||||
EOF
|
||||
|
||||
# 创建停止脚本
|
||||
cat > /opt/zhibo/scripts/stop-all.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
pkill -f "Crmeb-admin.jar" && echo "Admin API 已停止"
|
||||
pkill -f "Crmeb-front.jar" && echo "Front API 已停止"
|
||||
EOF
|
||||
|
||||
# 赋予执行权限
|
||||
chmod +x /opt/zhibo/scripts/*.sh
|
||||
```
|
||||
|
||||
### 2.2 配置Nginx
|
||||
|
||||
```bash
|
||||
# 创建Nginx配置
|
||||
cat > /etc/nginx/conf.d/zhibo.conf << 'EOF'
|
||||
# 管理后台
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /opt/zhibo/admin-web;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Admin API代理
|
||||
location /api/admin/ {
|
||||
proxy_pass http://127.0.0.1:30001/api/admin/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
location /api/public/ {
|
||||
proxy_pass http://127.0.0.1:30001/api/public/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
|
||||
# Front API (供APP调用)
|
||||
server {
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 测试并重载Nginx
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第三步:启动服务
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
/opt/zhibo/scripts/start-all.sh
|
||||
|
||||
# 查看日志
|
||||
tail -f /opt/zhibo/logs/admin-api.log
|
||||
tail -f /opt/zhibo/logs/front-api.log
|
||||
|
||||
# 检查端口
|
||||
netstat -tlnp | grep -E "30001|8081"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第四步:验证部署
|
||||
|
||||
### 检查服务状态
|
||||
```bash
|
||||
# 检查进程
|
||||
ps aux | grep -E "Crmeb-admin|Crmeb-front"
|
||||
|
||||
# 检查端口
|
||||
ss -tlnp | grep -E "30001|8081|80|8080"
|
||||
```
|
||||
|
||||
### 测试接口
|
||||
```bash
|
||||
# 测试Admin API
|
||||
curl http://localhost:30001/api/public/version
|
||||
|
||||
# 测试Front API
|
||||
curl http://localhost:8081/api/front/index
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 访问地址
|
||||
|
||||
| 服务 | 地址 |
|
||||
|------|------|
|
||||
| 管理后台 | http://1.15.149.240 |
|
||||
| Admin API | http://1.15.149.240:30001 |
|
||||
| Front API | http://1.15.149.240:8081 |
|
||||
| Front API (Nginx) | http://1.15.149.240:8080 |
|
||||
|
||||
---
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
# 启动所有服务
|
||||
/opt/zhibo/scripts/start-all.sh
|
||||
|
||||
# 停止所有服务
|
||||
/opt/zhibo/scripts/stop-all.sh
|
||||
|
||||
# 单独启动Admin API
|
||||
/opt/zhibo/scripts/start-admin-api.sh
|
||||
|
||||
# 单独启动Front API
|
||||
/opt/zhibo/scripts/start-front-api.sh
|
||||
|
||||
# 查看日志
|
||||
tail -100f /opt/zhibo/logs/admin-api.log
|
||||
tail -100f /opt/zhibo/logs/front-api.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## APP配置
|
||||
|
||||
部署完成后,需要修改Android APP的API地址:
|
||||
|
||||
文件:`android-app/app/src/main/java/com/example/livestreaming/net/ApiConfig.java`
|
||||
|
||||
```java
|
||||
public static final String BASE_URL = "http://1.15.149.240:8081/";
|
||||
```
|
||||
|
||||
然后重新编译APK。
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 服务启动失败
|
||||
```bash
|
||||
# 查看详细日志
|
||||
cat /opt/zhibo/logs/admin-api.log
|
||||
cat /opt/zhibo/logs/front-api.log
|
||||
|
||||
# 检查Java版本
|
||||
java -version # 需要JDK 1.8
|
||||
|
||||
# 检查Redis是否运行
|
||||
redis-cli ping
|
||||
```
|
||||
|
||||
### 端口被占用
|
||||
```bash
|
||||
# 查看端口占用
|
||||
lsof -i :30001
|
||||
lsof -i :8081
|
||||
|
||||
# 杀掉占用进程
|
||||
kill -9 <PID>
|
||||
```
|
||||
Loading…
Reference in New Issue
Block a user