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 = {
|
module.exports = {
|
||||||
// 接口请求地址
|
// 接口请求地址
|
||||||
apiBaseURL: VUE_APP_API_URL,
|
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 =
|
const VUE_APP_WS_URL =
|
||||||
process.env.VUE_APP_WS_URL || (location.protocol === 'https' ? 'wss' : 'ws') + ':' + location.hostname;
|
process.env.VUE_APP_WS_URL || (location.protocol === 'https' ? 'wss' : 'ws') + ':' + location.hostname;
|
||||||
const SettingMer = {
|
const SettingMer = {
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@ logging:
|
||||||
|
|
||||||
# mybatis 配置
|
# mybatis 配置
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
mapper-locations: classpath*:mapper/**/*.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
mapper-locations: classpath*:mapper/*/*Mapper.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||||
typeAliasesPackage: com.zbkj.**.model
|
typeAliasesPackage: com.zbkj.**.model
|
||||||
# 配置slq打印日志
|
# 配置slq打印日志
|
||||||
configuration:
|
configuration:
|
||||||
|
|
|
||||||
|
|
@ -41,4 +41,8 @@ public class LoginRequest implements Serializable {
|
||||||
@ApiModelProperty(value = "密码", required = true, example = "1~[6,18]")
|
@ApiModelProperty(value = "密码", required = true, example = "1~[6,18]")
|
||||||
// @Pattern(regexp = RegularConstants.PASSWORD, message = "密码格式错误,密码必须以字母开头,长度在6~18之间,只能包含字符、数字和下划线")
|
// @Pattern(regexp = RegularConstants.PASSWORD, message = "密码格式错误,密码必须以字母开头,长度在6~18之间,只能包含字符、数字和下划线")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "推广人id")
|
||||||
|
@JsonProperty(value = "spread_spid")
|
||||||
|
private Integer spreadPid = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,3 +229,4 @@ public class CallController {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -288,7 +288,7 @@ public class LiveRoomController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 直播控制接口 ==========
|
// ========== 关注主播接口 ==========
|
||||||
|
|
||||||
@ApiOperation(value = "开始直播")
|
@ApiOperation(value = "开始直播")
|
||||||
@PostMapping("/room/{id}/start")
|
@PostMapping("/room/{id}/start")
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ public class LoginServiceImpl implements LoginService {
|
||||||
String token = tokenComponent.createToken(user);
|
String token = tokenComponent.createToken(user);
|
||||||
loginResponse.setToken(token);
|
loginResponse.setToken(token);
|
||||||
|
|
||||||
|
//绑定推广关系
|
||||||
|
if (loginRequest.getSpreadPid() > 0) {
|
||||||
|
bindSpread(user, loginRequest.getSpreadPid());
|
||||||
|
}
|
||||||
|
|
||||||
// 记录最后一次登录时间
|
// 记录最后一次登录时间
|
||||||
user.setLastLoginTime(CrmebDateUtil.nowDateTime());
|
user.setLastLoginTime(CrmebDateUtil.nowDateTime());
|
||||||
user.setUpdateTime(DateUtil.date());
|
user.setUpdateTime(DateUtil.date());
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
System.out.println("[CallSignaling] ========== 连接建立 ==========");
|
|
||||||
System.out.println("[CallSignaling] sessionId=" + session.getId());
|
|
||||||
logger.info("[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() : "";
|
String type = json.has("type") ? json.get("type").asText() : "";
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ping":
|
|
||||||
handlePing(session);
|
|
||||||
break;
|
|
||||||
case "register":
|
case "register":
|
||||||
handleRegister(session, json);
|
handleRegister(session, json);
|
||||||
break;
|
break;
|
||||||
|
|
@ -102,9 +97,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
private void handleRegister(WebSocketSession session, JsonNode json) throws IOException {
|
private void handleRegister(WebSocketSession session, JsonNode json) throws IOException {
|
||||||
Integer userId = json.has("userId") ? json.get("userId").asInt() : null;
|
Integer userId = json.has("userId") ? json.get("userId").asInt() : null;
|
||||||
System.out.println("[CallSignaling] ========== 用户注册 ==========");
|
|
||||||
System.out.println("[CallSignaling] userId=" + userId);
|
|
||||||
|
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
sendError(session, "userId不能为空");
|
sendError(session, "userId不能为空");
|
||||||
return;
|
return;
|
||||||
|
|
@ -113,7 +105,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
// 关闭旧连接
|
// 关闭旧连接
|
||||||
WebSocketSession oldSession = userCallSessions.get(userId);
|
WebSocketSession oldSession = userCallSessions.get(userId);
|
||||||
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
if (oldSession != null && oldSession.isOpen() && !oldSession.getId().equals(session.getId())) {
|
||||||
System.out.println("[CallSignaling] 关闭旧连接: userId=" + userId);
|
|
||||||
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
|
logger.info("[CallSignaling] 关闭旧连接: userId={}, oldSessionId={}", userId, oldSession.getId());
|
||||||
try {
|
try {
|
||||||
oldSession.close();
|
oldSession.close();
|
||||||
|
|
@ -127,8 +118,6 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
response.put("type", "registered");
|
response.put("type", "registered");
|
||||||
response.put("userId", userId);
|
response.put("userId", userId);
|
||||||
session.sendMessage(new TextMessage(response.toString()));
|
session.sendMessage(new TextMessage(response.toString()));
|
||||||
|
|
||||||
System.out.println("[CallSignaling] 用户注册成功: userId=" + userId + ", 当前在线用户=" + userCallSessions.keySet());
|
|
||||||
logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}",
|
logger.info("[CallSignaling] 用户注册成功: userId={}, sessionId={}, 当前在线用户数={}",
|
||||||
userId, session.getId(), userCallSessions.size());
|
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 {
|
private void handleSignaling(WebSocketSession session, JsonNode json, String type) throws IOException {
|
||||||
String callId = json.has("callId") ? json.get("callId").asText() : sessionCallMap.get(session.getId());
|
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) {
|
if (callId == null) {
|
||||||
sendError(session, "callId不能为空");
|
sendError(session, "callId不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保通话会话存在
|
|
||||||
Set<WebSocketSession> sessions = callSessions.get(callId);
|
Set<WebSocketSession> sessions = callSessions.get(callId);
|
||||||
if (sessions == null) {
|
if (sessions == null) {
|
||||||
// 尝试创建会话(可能是REST API发起的通话)
|
sendError(session, "通话不存在");
|
||||||
System.out.println("[CallSignaling] 通话会话不存在,创建新会话: callId=" + callId);
|
return;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转发信令给通话中的其他参与者
|
// 转发信令给通话中的其他参与者
|
||||||
|
|
@ -385,22 +351,13 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
String forwardMsg = forward.toString();
|
String forwardMsg = forward.toString();
|
||||||
int forwardCount = 0;
|
|
||||||
for (WebSocketSession s : sessions) {
|
for (WebSocketSession s : sessions) {
|
||||||
Integer targetUserId = sessionUserMap.get(s.getId());
|
if (s.isOpen() && !s.getId().equals(session.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) {
|
|
||||||
s.sendMessage(new TextMessage(forwardMsg));
|
s.sendMessage(new TextMessage(forwardMsg));
|
||||||
forwardCount++;
|
|
||||||
System.out.println("[CallSignaling] >>> 已转发给: userId=" + targetUserId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("[CallSignaling] 转发完成: type=" + type + ", 转发给" + forwardCount + "个用户");
|
logger.debug("[CallSignaling] 转发信令: type={}, callId={}", type, callId);
|
||||||
logger.info("[CallSignaling] 转发信令: type={}, callId={}, senderId={}, 转发给{}个用户", type, callId, senderId, forwardCount);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -504,78 +461,22 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status);
|
return "calling".equals(status) || "ringing".equals(status) || "connected".equals(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void sendError(WebSocketSession session, String message) throws IOException {
|
||||||
* 处理心跳 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();
|
ObjectNode error = objectMapper.createObjectNode();
|
||||||
error.put("type", "error");
|
error.put("type", "error");
|
||||||
error.put("message", message);
|
error.put("message", message);
|
||||||
session.sendMessage(new TextMessage(error.toString()));
|
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知被叫方有来电(供REST API调用)
|
* 通知被叫方有来电(供REST API调用)
|
||||||
*/
|
*/
|
||||||
public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar,
|
public void notifyIncomingCall(String callId, Integer callerId, String callerName, String callerAvatar,
|
||||||
Integer calleeId, String callType) {
|
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 {
|
try {
|
||||||
// 记录通话创建时间
|
// 记录通话创建时间
|
||||||
callCreateTime.put(callId, System.currentTimeMillis());
|
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);
|
WebSocketSession calleeSession = userCallSessions.get(calleeId);
|
||||||
if (calleeSession != null && calleeSession.isOpen()) {
|
if (calleeSession != null && calleeSession.isOpen()) {
|
||||||
|
|
@ -587,14 +488,11 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : "");
|
incoming.put("callerAvatar", callerAvatar != null ? callerAvatar : "");
|
||||||
incoming.put("callType", callType);
|
incoming.put("callType", callType);
|
||||||
calleeSession.sendMessage(new TextMessage(incoming.toString()));
|
calleeSession.sendMessage(new TextMessage(incoming.toString()));
|
||||||
System.out.println("[CallSignaling] 已发送来电通知给被叫方: calleeId=" + calleeId);
|
|
||||||
logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId);
|
logger.info("[CallSignaling] 通知来电: callId={}, callee={}", callId, calleeId);
|
||||||
} else {
|
} else {
|
||||||
System.out.println("[CallSignaling] !!!!! 被叫方未在线 !!!!! calleeId=" + calleeId);
|
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}", calleeId);
|
||||||
logger.warn("[CallSignaling] 被叫方未在线: calleeId={}, 当前在线用户={}", calleeId, userCallSessions.keySet());
|
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
System.out.println("[CallSignaling] 通知来电异常: " + e.getMessage());
|
|
||||||
logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e);
|
logger.error("[CallSignaling] 通知来电异常: callId={}", callId, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -614,15 +512,8 @@ public class CallSignalingHandler extends TextWebSocketHandler {
|
||||||
logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}",
|
logger.info("[CallSignaling] REST API调用notifyCallAccepted: callId={}, callerId={}, 当前在线用户={}",
|
||||||
callId, callerId, userCallSessions.keySet());
|
callId, callerId, userCallSessions.keySet());
|
||||||
try {
|
try {
|
||||||
// 确保通话会话存在
|
|
||||||
callSessions.computeIfAbsent(callId, k -> new CopyOnWriteArraySet<>());
|
|
||||||
|
|
||||||
WebSocketSession callerSession = userCallSessions.get(callerId);
|
WebSocketSession callerSession = userCallSessions.get(callerId);
|
||||||
if (callerSession != null && callerSession.isOpen()) {
|
if (callerSession != null && callerSession.isOpen()) {
|
||||||
// 确保主叫方在通话会话中
|
|
||||||
joinCallSession(callId, callerSession);
|
|
||||||
sessionCallMap.put(callerSession.getId(), callId);
|
|
||||||
|
|
||||||
ObjectNode notify = objectMapper.createObjectNode();
|
ObjectNode notify = objectMapper.createObjectNode();
|
||||||
notify.put("type", "call_accepted");
|
notify.put("type", "call_accepted");
|
||||||
notify.put("callId", callId);
|
notify.put("callId", callId);
|
||||||
|
|
|
||||||
|
|
@ -84,8 +84,7 @@ debug: true
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
io.swagger.*: error
|
io.swagger.*: error
|
||||||
com.zbkj: debug
|
com.zbjk.crmeb: debug
|
||||||
com.zbkj.front.websocket: info
|
|
||||||
org.springframework.boot.autoconfigure: ERROR
|
org.springframework.boot.autoconfigure: ERROR
|
||||||
config: classpath:logback-spring.xml
|
config: classpath:logback-spring.xml
|
||||||
file:
|
file:
|
||||||
|
|
@ -93,7 +92,7 @@ logging:
|
||||||
|
|
||||||
# mybatis 配置
|
# mybatis 配置
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
mapper-locations: classpath*:mapper/**/*.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
mapper-locations: classpath*:mapper/*/*Mapper.xml #xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
|
||||||
# 配置sql打印日志
|
# 配置sql打印日志
|
||||||
configuration:
|
configuration:
|
||||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,4 @@ dependencies {
|
||||||
implementation("com.github.andnux:ijkplayer:0.0.1") {
|
implementation("com.github.andnux:ijkplayer:0.0.1") {
|
||||||
exclude("com.google.android.exoplayer", "exoplayer")
|
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.MODIFY_AUDIO_SETTINGS" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
<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
|
<application
|
||||||
android:name=".LiveStreamingApplication"
|
android:name=".LiveStreamingApplication"
|
||||||
|
|
|
||||||
|
|
@ -183,11 +183,41 @@ public class CategoryFilterManager {
|
||||||
String roomType = r.getType();
|
String roomType = r.getType();
|
||||||
if (c.equals(roomType)) {
|
if (c.equals(roomType)) {
|
||||||
filtered.add(r);
|
filtered.add(r);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 降级到演示数据分类算法
|
||||||
|
String demoCategory = getDemoCategoryForRoom(r);
|
||||||
|
if (c.equals(demoCategory)) {
|
||||||
|
filtered.add(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered;
|
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() {
|
private List<ConversationItem> buildDemoLikes() {
|
||||||
List<ConversationItem> list = new ArrayList<>();
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,7 @@ import com.google.android.material.textfield.MaterialAutoCompleteTextView;
|
||||||
import com.google.android.material.textfield.TextInputLayout;
|
import com.google.android.material.textfield.TextInputLayout;
|
||||||
import com.example.livestreaming.net.ApiClient;
|
import com.example.livestreaming.net.ApiClient;
|
||||||
import com.example.livestreaming.net.ApiResponse;
|
import com.example.livestreaming.net.ApiResponse;
|
||||||
import com.example.livestreaming.net.ConversationResponse;
|
|
||||||
import com.example.livestreaming.net.CreateRoomRequest;
|
import com.example.livestreaming.net.CreateRoomRequest;
|
||||||
import com.example.livestreaming.net.PageResponse;
|
|
||||||
import com.example.livestreaming.net.Room;
|
import com.example.livestreaming.net.Room;
|
||||||
import com.example.livestreaming.net.StreamConfig;
|
import com.example.livestreaming.net.StreamConfig;
|
||||||
|
|
||||||
|
|
@ -54,7 +52,6 @@ import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
|
|
@ -99,7 +96,17 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
// 用户打开APP时不需要强制登录,可以直接使用APP
|
// 用户打开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());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
|
|
@ -113,10 +120,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
loadAvatarFromPrefs();
|
loadAvatarFromPrefs();
|
||||||
setupSpeechRecognizer();
|
setupSpeechRecognizer();
|
||||||
|
|
||||||
// 初始化未读消息数量
|
// TODO: 接入后端接口 - 获取未读消息总数
|
||||||
|
// 接口路径: GET /api/messages/unread/count
|
||||||
|
// 请求参数: 无(从token中获取userId)
|
||||||
|
// 返回数据格式: ApiResponse<Integer> 或 ApiResponse<{unreadCount: number}>
|
||||||
|
// 返回当前用户所有会话的未读消息总数
|
||||||
|
// 初始化未读消息数量(演示数据)
|
||||||
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
if (UnreadMessageManager.getUnreadCount(this) == 0) {
|
||||||
// 从会话列表获取总未读数量
|
// 从消息列表计算总未读数量
|
||||||
fetchUnreadMessageCount();
|
UnreadMessageManager.setUnreadCount(this, calculateTotalUnreadCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化顶部标签页数据
|
// 初始化顶部标签页数据
|
||||||
|
|
@ -535,7 +547,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// 如果文本为空,启动语音识别
|
// 如果文本为空,启动语音识别
|
||||||
startVoiceRecognition();
|
startVoiceRecognition();
|
||||||
} else {
|
} else {
|
||||||
// 如果文本不为空,跳转到搜索页面
|
// 如果文本不为空,执行搜索
|
||||||
|
// TODO: 接入后端接口 - 搜索功能
|
||||||
|
// 接口路径: GET /api/search
|
||||||
|
// 请求参数:
|
||||||
|
// - keyword: 搜索关键词
|
||||||
|
// - type (可选): 搜索类型(room/user/all)
|
||||||
|
// - page (可选): 页码
|
||||||
|
// 返回数据格式: ApiResponse<{rooms: Room[], users: User[]}>
|
||||||
|
// 跳转到搜索页面并传递搜索关键词
|
||||||
SearchActivity.start(MainActivity.this, searchText);
|
SearchActivity.start(MainActivity.this, searchText);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -821,48 +841,20 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// 更新未读消息徽章
|
// 更新未读消息徽章
|
||||||
UnreadMessageManager.updateBadge(bottomNavigation);
|
UnreadMessageManager.updateBadge(bottomNavigation);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保通话信令 WebSocket 保持连接(用于接收来电通知)
|
|
||||||
LiveStreamingApplication app = (LiveStreamingApplication) getApplication();
|
|
||||||
app.connectCallSignalingIfLoggedIn();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从后端获取未读消息总数
|
* 计算总未读消息数量(从演示数据中计算)
|
||||||
*/
|
*/
|
||||||
private void fetchUnreadMessageCount() {
|
private int calculateTotalUnreadCount() {
|
||||||
// 检查登录状态
|
// 模拟从消息列表计算总未读数量
|
||||||
if (!AuthHelper.isLoggedIn(this)) {
|
// 这里使用 MessagesActivity 中的演示数据
|
||||||
return;
|
int total = 0;
|
||||||
}
|
total += 2; // 系统通知
|
||||||
|
total += 5; // 附近的人
|
||||||
// 从会话列表接口获取未读消息总数
|
total += 19; // 直播间群聊
|
||||||
ApiClient.getService(getApplicationContext()).getConversations()
|
total += 1; // 客服
|
||||||
.enqueue(new Callback<ApiResponse<List<ConversationResponse>>>() {
|
return total;
|
||||||
@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) {
|
|
||||||
// 网络错误,忽略
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -978,7 +970,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
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"))
|
ApiClient.getService(getApplicationContext()).createRoom(new CreateRoomRequest(title, streamerName, "live"))
|
||||||
.enqueue(new Callback<ApiResponse<Room>>() {
|
.enqueue(new Callback<ApiResponse<Room>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -1317,6 +1316,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
String roomType = r.getType();
|
String roomType = r.getType();
|
||||||
if (c.equals(roomType)) {
|
if (c.equals(roomType)) {
|
||||||
filtered.add(r);
|
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() {
|
private void initializeTopTabData() {
|
||||||
// 初始化关注页面数据
|
// 初始化关注页面数据(已关注主播的直播)- 使用演示数据
|
||||||
followRooms.clear();
|
followRooms.clear();
|
||||||
|
followRooms.addAll(buildFollowRooms());
|
||||||
|
|
||||||
// 初始化发现页面数据 - 从后端获取真实直播间
|
// 初始化发现页面数据 - 从后端获取真实直播间
|
||||||
fetchDiscoverRooms();
|
fetchDiscoverRooms();
|
||||||
|
|
||||||
// 初始化附近页面数据
|
// 初始化附近页面数据(模拟位置数据)
|
||||||
nearbyUsers.clear();
|
nearbyUsers.clear();
|
||||||
|
nearbyUsers.addAll(buildNearbyUsers());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1770,77 +1843,45 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示关注页面时从后端获取关注主播的直播间列表
|
* 构建关注页面的房间列表(已关注主播的直播)
|
||||||
* 注意:关注功能需要用户登录,在showFollowTab()中会检查登录状态
|
|
||||||
*/
|
*/
|
||||||
private void fetchFollowRooms() {
|
private List<Room> buildFollowRooms() {
|
||||||
// 检查登录状态
|
// TODO: 接入后端接口 - 获取关注主播的直播间列表
|
||||||
if (!AuthHelper.isLoggedIn(this)) {
|
// 接口路径: GET /api/following/rooms 或 GET /api/rooms?type=following
|
||||||
followRooms.clear();
|
// 请求参数:
|
||||||
if (adapter != null) {
|
// - userId: 当前用户ID(从token中获取)
|
||||||
adapter.submitList(new ArrayList<>());
|
// - page (可选): 页码
|
||||||
}
|
// - pageSize (可选): 每页数量
|
||||||
return;
|
// 返回数据格式: 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载状态
|
return list;
|
||||||
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<>());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1858,7 +1899,62 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// 后端应根据用户观看历史、点赞记录、关注关系等进行个性化推荐
|
// 后端应根据用户观看历史、点赞记录、关注关系等进行个性化推荐
|
||||||
List<Room> list = new ArrayList<>();
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
@ -1880,7 +1976,28 @@ public class MainActivity extends AppCompatActivity {
|
||||||
// 需要先获取用户位置权限,然后调用此接口
|
// 需要先获取用户位置权限,然后调用此接口
|
||||||
List<NearbyUser> list = new ArrayList<>();
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,9 @@ public class MessageSendHelper {
|
||||||
// 3. 调用接口
|
// 3. 调用接口
|
||||||
// 4. 处理响应
|
// 4. 处理响应
|
||||||
|
|
||||||
|
// 临时模拟成功
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onError("消息发送功能待接入后端接口");
|
callback.onSuccess("temp_message_id_" + System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -149,8 +150,9 @@ public class MessageSendHelper {
|
||||||
// 5. 上传图片
|
// 5. 上传图片
|
||||||
// 6. 处理响应
|
// 6. 处理响应
|
||||||
|
|
||||||
|
// 临时模拟成功
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
callback.onError("图片消息发送功能待接入后端接口");
|
callback.onSuccess("temp_image_message_id_" + System.currentTimeMillis());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,8 +224,9 @@ public class MessageSendHelper {
|
||||||
// 4. 上传语音
|
// 4. 上传语音
|
||||||
// 5. 处理响应
|
// 5. 处理响应
|
||||||
|
|
||||||
|
// 临时模拟成功
|
||||||
if (callback != null) {
|
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() {
|
private List<ConversationItem> buildDemoConversations() {
|
||||||
List<ConversationItem> list = new ArrayList<>();
|
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;
|
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 - 弹幕
|
// WebSocket - 弹幕
|
||||||
private WebSocket chatWebSocket;
|
private WebSocket chatWebSocket;
|
||||||
private OkHttpClient chatWsClient;
|
private OkHttpClient chatWsClient;
|
||||||
|
private static final String WS_CHAT_BASE_URL = "ws://192.168.1.164:8081/ws/live/chat/";
|
||||||
|
|
||||||
// WebSocket - 在线人数
|
// WebSocket - 在线人数
|
||||||
private WebSocket onlineCountWebSocket;
|
private WebSocket onlineCountWebSocket;
|
||||||
private OkHttpClient onlineCountWsClient;
|
private OkHttpClient onlineCountWsClient;
|
||||||
|
private static final String WS_ONLINE_BASE_URL = "ws://192.168.1.164:8081/ws/live/";
|
||||||
// 动态获取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/";
|
|
||||||
}
|
|
||||||
|
|
||||||
// WebSocket 心跳检测 - 弹幕
|
// WebSocket 心跳检测 - 弹幕
|
||||||
private Runnable chatHeartbeatRunnable;
|
private Runnable chatHeartbeatRunnable;
|
||||||
|
|
@ -281,7 +265,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS) // OkHttp 内置 ping
|
||||||
.build();
|
.build();
|
||||||
Request request = new Request.Builder()
|
Request request = new Request.Builder()
|
||||||
.url(getWsChatBaseUrl() + roomId)
|
.url(WS_CHAT_BASE_URL + roomId)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
chatWebSocket = chatWsClient.newWebSocket(request, new WebSocketListener() {
|
||||||
|
|
@ -388,7 +372,7 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
|
String clientId = (userIdStr != null && !userIdStr.isEmpty()) ?
|
||||||
userIdStr :
|
userIdStr :
|
||||||
"guest_" + System.currentTimeMillis();
|
"guest_" + System.currentTimeMillis();
|
||||||
String wsUrl = getWsOnlineBaseUrl() + roomId + "?clientId=" + clientId;
|
String wsUrl = WS_ONLINE_BASE_URL + roomId + "?clientId=" + clientId;
|
||||||
|
|
||||||
onlineCountWsClient = new OkHttpClient.Builder()
|
onlineCountWsClient = new OkHttpClient.Builder()
|
||||||
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
|
.pingInterval(30, java.util.concurrent.TimeUnit.SECONDS)
|
||||||
|
|
@ -1066,19 +1050,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
if (ijkSurface == null) return;
|
if (ijkSurface == null) return;
|
||||||
|
|
||||||
IjkMediaPlayer p = new IjkMediaPlayer();
|
IjkMediaPlayer p = new IjkMediaPlayer();
|
||||||
// 优化缓冲设置,减少卡顿
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 1); // 开启缓冲
|
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer");
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 100000); // 100ms分析时长
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 10240); // 10KB探测大小
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probesize", 1024);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 5); // 允许丢帧
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 3000); // 3秒缓存
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "max_cached_duration", 300);
|
||||||
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min_frames", 50); // 最小缓冲帧数
|
p.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "infbuf", 1);
|
||||||
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.setOnPreparedListener(mp -> {
|
p.setOnPreparedListener(mp -> {
|
||||||
binding.offlineLayout.setVisibility(View.GONE);
|
binding.offlineLayout.setVisibility(View.GONE);
|
||||||
|
|
@ -1087,15 +1066,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
|
|
||||||
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
p.setOnErrorListener((IMediaPlayer mp, int what, int extra) -> {
|
||||||
android.util.Log.e("IjkPlayer", "播放错误: what=" + what + ", extra=" + extra);
|
|
||||||
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
if (ijkFallbackTried || TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
||||||
binding.offlineLayout.setVisibility(View.VISIBLE);
|
binding.offlineLayout.setVisibility(View.VISIBLE);
|
||||||
// 5秒后尝试重新连接
|
|
||||||
handler.postDelayed(() -> {
|
|
||||||
if (!isFinishing() && !isDestroyed() && room != null && room.isLive()) {
|
|
||||||
fetchRoom(); // 重新获取房间信息并播放
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ijkFallbackTried = true;
|
ijkFallbackTried = true;
|
||||||
|
|
@ -1103,28 +1075,12 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
return true;
|
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;
|
ijkPlayer = p;
|
||||||
try {
|
try {
|
||||||
p.setSurface(ijkSurface);
|
p.setSurface(ijkSurface);
|
||||||
p.setDataSource(url);
|
p.setDataSource(url);
|
||||||
p.prepareAsync();
|
p.prepareAsync();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
android.util.Log.e("IjkPlayer", "播放器初始化失败: " + e.getMessage());
|
|
||||||
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
if (!TextUtils.isEmpty(ijkFallbackHlsUrl)) {
|
||||||
startHls(ijkFallbackHlsUrl, null);
|
startHls(ijkFallbackHlsUrl, null);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1276,7 +1232,14 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
*/
|
*/
|
||||||
private void setDefaultGifts() {
|
private void setDefaultGifts() {
|
||||||
availableGifts = new ArrayList<>();
|
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,
|
private void showPaymentMethodDialog(String orderId, RechargeOption selectedOption,
|
||||||
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
androidx.appcompat.app.AlertDialog rechargeDialog) {
|
||||||
String[] paymentMethods = {"支付宝支付", "微信支付"};
|
String[] paymentMethods = {"支付宝支付", "微信支付", "余额支付(模拟)"};
|
||||||
|
|
||||||
new MaterialAlertDialogBuilder(this)
|
new MaterialAlertDialogBuilder(this)
|
||||||
.setTitle("选择支付方式")
|
.setTitle("选择支付方式")
|
||||||
|
|
@ -1560,6 +1523,10 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
payType = "weixin";
|
payType = "weixin";
|
||||||
payChannel = "weixinAppAndroid";
|
payChannel = "weixinAppAndroid";
|
||||||
break;
|
break;
|
||||||
|
case 2: // 余额支付(模拟)
|
||||||
|
// 模拟充值成功
|
||||||
|
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1601,9 +1568,8 @@ public class RoomDetailActivity extends AppCompatActivity {
|
||||||
"\n请集成支付SDK完成实际支付",
|
"\n请集成支付SDK完成实际支付",
|
||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
// TODO: 集成支付SDK后,在支付成功回调中更新余额
|
// 暂时模拟支付成功
|
||||||
// 支付成功后应该调用后端接口查询订单状态并更新余额
|
simulateRechargeSuccess(selectedOption, rechargeDialog);
|
||||||
rechargeDialog.dismiss();
|
|
||||||
} else {
|
} else {
|
||||||
Toast.makeText(RoomDetailActivity.this,
|
Toast.makeText(RoomDetailActivity.this,
|
||||||
"支付失败: " + apiResponse.getMessage(),
|
"支付失败: " + 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) {
|
private List<Room> buildFollowDemoRooms(int count) {
|
||||||
// 不再使用模拟数据,只从后端接口获取真实关注主播的直播间数据
|
List<Room> list = new ArrayList<>();
|
||||||
return 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) {
|
private List<NearbyUser> buildNearbyDemoUsers(int count) {
|
||||||
// 不再使用模拟数据,只从后端接口获取真实附近用户数据
|
List<NearbyUser> list = new ArrayList<>();
|
||||||
return 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) {
|
private List<Room> buildDiscoverDemoRooms(int count) {
|
||||||
// 不再使用模拟数据,只从后端接口获取真实推荐直播间数据
|
List<Room> list = new ArrayList<>();
|
||||||
return 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() {
|
private void showParkBadges() {
|
||||||
|
|
@ -424,8 +453,17 @@ public class TabPlaceholderActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BadgeItem> buildDemoBadges() {
|
private List<BadgeItem> buildDemoBadges() {
|
||||||
// 不再使用模拟数据,只从后端接口获取真实勋章数据
|
List<BadgeItem> list = new ArrayList<>();
|
||||||
return 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() {
|
private void showMore() {
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,14 @@ public class WatchHistoryActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Room> buildDemoHistory(int count) {
|
private List<Room> buildDemoHistory(int count) {
|
||||||
// 不再使用模拟数据,只从后端接口获取真实观看历史数据
|
List<Room> list = new ArrayList<>();
|
||||||
return 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;
|
package com.example.livestreaming.call;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.ActivityCompat;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.example.livestreaming.R;
|
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
|
public class CallActivity extends AppCompatActivity implements CallManager.CallStateListener {
|
||||||
CallManager.CallStateListener,
|
|
||||||
WebRTCClient.WebRTCListener {
|
|
||||||
|
|
||||||
private static final String TAG = "CallActivity";
|
|
||||||
private static final int PERMISSION_REQUEST_CODE = 100;
|
|
||||||
|
|
||||||
// UI 组件
|
|
||||||
private ImageView ivBackgroundAvatar;
|
private ImageView ivBackgroundAvatar;
|
||||||
private ImageView ivAvatar;
|
private ImageView ivAvatar;
|
||||||
private TextView tvUserName;
|
private TextView tvUserName;
|
||||||
|
|
@ -60,20 +36,11 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
private ImageButton btnSwitchCamera;
|
private ImageButton btnSwitchCamera;
|
||||||
private LinearLayout layoutCallControls;
|
private LinearLayout layoutCallControls;
|
||||||
private LinearLayout layoutVideoToggle;
|
private LinearLayout layoutVideoToggle;
|
||||||
private FrameLayout layoutLocalVideo;
|
|
||||||
private FrameLayout layoutRemoteVideo;
|
|
||||||
|
|
||||||
// WebRTC 视频渲染器
|
|
||||||
private SurfaceViewRenderer localRenderer;
|
|
||||||
private SurfaceViewRenderer remoteRenderer;
|
|
||||||
|
|
||||||
// 管理器
|
|
||||||
private CallManager callManager;
|
private CallManager callManager;
|
||||||
private WebRTCClient webRTCClient;
|
|
||||||
private AudioManager audioManager;
|
private AudioManager audioManager;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
|
||||||
// 通话信息
|
|
||||||
private String callId;
|
private String callId;
|
||||||
private String callType;
|
private String callType;
|
||||||
private boolean isCaller;
|
private boolean isCaller;
|
||||||
|
|
@ -81,18 +48,12 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
private String otherUserName;
|
private String otherUserName;
|
||||||
private String otherUserAvatar;
|
private String otherUserAvatar;
|
||||||
|
|
||||||
// 状态
|
|
||||||
private boolean isMuted = false;
|
private boolean isMuted = false;
|
||||||
private boolean isSpeakerOn = false;
|
private boolean isSpeakerOn = false;
|
||||||
private boolean isVideoEnabled = true;
|
private boolean isVideoEnabled = true;
|
||||||
private boolean isConnected = false;
|
private boolean isConnected = false;
|
||||||
private boolean isWebRTCInitialized = false;
|
|
||||||
private long callStartTime = 0;
|
private long callStartTime = 0;
|
||||||
|
|
||||||
// ICE Candidate 缓存(在远程 SDP 设置前收到的)
|
|
||||||
private List<IceCandidate> pendingIceCandidates = new ArrayList<>();
|
|
||||||
private boolean remoteDescriptionSet = false;
|
|
||||||
|
|
||||||
private Runnable durationRunnable = new Runnable() {
|
private Runnable durationRunnable = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
@ -109,8 +70,6 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
Log.d(TAG, "========== CallActivity onCreate ==========");
|
|
||||||
Toast.makeText(this, "通话界面已打开", Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
// 保持屏幕常亮,显示在锁屏上方
|
// 保持屏幕常亮,显示在锁屏上方
|
||||||
getWindow().addFlags(
|
getWindow().addFlags(
|
||||||
|
|
@ -123,20 +82,9 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
|
|
||||||
initViews();
|
initViews();
|
||||||
initData();
|
initData();
|
||||||
|
initCallManager();
|
||||||
Log.d(TAG, "callId=" + callId + ", callType=" + callType + ", isCaller=" + isCaller);
|
setupListeners();
|
||||||
Toast.makeText(this, "通话类型: " + callType + ", 主叫: " + isCaller, Toast.LENGTH_SHORT).show();
|
updateUI();
|
||||||
|
|
||||||
// 检查权限
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViews() {
|
private void initViews() {
|
||||||
|
|
@ -154,62 +102,9 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
|
btnSwitchCamera = findViewById(R.id.btnSwitchCamera);
|
||||||
layoutCallControls = findViewById(R.id.layoutCallControls);
|
layoutCallControls = findViewById(R.id.layoutCallControls);
|
||||||
layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
|
layoutVideoToggle = findViewById(R.id.layoutVideoToggle);
|
||||||
layoutLocalVideo = findViewById(R.id.layoutLocalVideo);
|
|
||||||
layoutRemoteVideo = findViewById(R.id.layoutRemoteVideo);
|
|
||||||
|
|
||||||
handler = new Handler(Looper.getMainLooper());
|
handler = new Handler(Looper.getMainLooper());
|
||||||
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
|
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() {
|
private void initData() {
|
||||||
|
|
@ -223,175 +118,36 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
// 如果是被叫方接听,直接进入通话状态
|
// 如果是被叫方接听,直接进入通话状态
|
||||||
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
|
boolean alreadyConnected = getIntent().getBooleanExtra("isConnected", false);
|
||||||
if (alreadyConnected) {
|
if (alreadyConnected) {
|
||||||
Log.d(TAG, "被叫方接听,直接进入通话状态");
|
android.util.Log.d("CallActivity", "被叫方接听,直接进入通话状态");
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
callStartTime = System.currentTimeMillis();
|
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() {
|
private void initCallManager() {
|
||||||
callManager = CallManager.getInstance(this);
|
callManager = CallManager.getInstance(this);
|
||||||
callManager.setStateListener(this);
|
callManager.setStateListener(this);
|
||||||
|
|
||||||
// 确保WebSocket已连接
|
// 确保WebSocket已连接(主叫方需要接收接听/拒绝通知)
|
||||||
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
String userId = com.example.livestreaming.net.AuthStore.getUserId(this);
|
||||||
if (userId != null && !userId.isEmpty()) {
|
if (userId != null && !userId.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
int uid = (int) Double.parseDouble(userId);
|
int uid = (int) Double.parseDouble(userId);
|
||||||
if (uid > 0) {
|
if (uid > 0) {
|
||||||
Log.d(TAG, "确保WebSocket连接,userId: " + uid);
|
android.util.Log.d("CallActivity", "确保WebSocket连接,userId: " + uid);
|
||||||
callManager.connect(uid);
|
callManager.connect(uid);
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException e) {
|
} 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() {
|
private void setupListeners() {
|
||||||
btnMinimize.setOnClickListener(v -> moveTaskToBack(true));
|
btnMinimize.setOnClickListener(v -> {
|
||||||
|
// 最小化通话(后台运行)
|
||||||
|
moveTaskToBack(true);
|
||||||
|
});
|
||||||
|
|
||||||
btnMute.setOnClickListener(v -> toggleMute());
|
btnMute.setOnClickListener(v -> toggleMute());
|
||||||
btnSpeaker.setOnClickListener(v -> toggleSpeaker());
|
btnSpeaker.setOnClickListener(v -> toggleSpeaker());
|
||||||
|
|
@ -406,7 +162,7 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
} else {
|
} else {
|
||||||
callManager.rejectCall(callId);
|
callManager.rejectCall(callId);
|
||||||
}
|
}
|
||||||
releaseAndFinish();
|
finish();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,11 +183,14 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
|
|
||||||
// 根据连接状态设置界面
|
// 根据连接状态设置界面
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
|
// 已接通,显示通话中界面
|
||||||
tvCallStatus.setVisibility(View.GONE);
|
tvCallStatus.setVisibility(View.GONE);
|
||||||
tvCallDuration.setVisibility(View.VISIBLE);
|
tvCallDuration.setVisibility(View.VISIBLE);
|
||||||
layoutCallControls.setVisibility(View.VISIBLE);
|
layoutCallControls.setVisibility(View.VISIBLE);
|
||||||
handler.post(durationRunnable);
|
handler.post(durationRunnable);
|
||||||
|
android.util.Log.d("CallActivity", "updateUI: 已接通状态,显示计时器");
|
||||||
} else {
|
} else {
|
||||||
|
// 未接通,显示等待状态
|
||||||
if (isCaller) {
|
if (isCaller) {
|
||||||
tvCallStatus.setText("正在呼叫...");
|
tvCallStatus.setText("正在呼叫...");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -444,10 +203,7 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
isMuted = !isMuted;
|
isMuted = !isMuted;
|
||||||
btnMute.setImageResource(isMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic);
|
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);
|
btnMute.setBackgroundResource(isMuted ? R.drawable.bg_call_button_active : R.drawable.bg_call_button);
|
||||||
|
// TODO: 实际静音控制
|
||||||
if (webRTCClient != null) {
|
|
||||||
webRTCClient.setMuted(isMuted);
|
|
||||||
}
|
|
||||||
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, isMuted ? "已静音" : "已取消静音", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,35 +217,17 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
private void toggleVideo() {
|
private void toggleVideo() {
|
||||||
isVideoEnabled = !isVideoEnabled;
|
isVideoEnabled = !isVideoEnabled;
|
||||||
btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active);
|
btnVideo.setBackgroundResource(isVideoEnabled ? R.drawable.bg_call_button : R.drawable.bg_call_button_active);
|
||||||
|
// TODO: 实际视频控制
|
||||||
if (webRTCClient != null) {
|
|
||||||
webRTCClient.setVideoEnabled(isVideoEnabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (localRenderer != null) {
|
|
||||||
localRenderer.setVisibility(isVideoEnabled ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, isVideoEnabled ? "已开启摄像头" : "已关闭摄像头", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchCamera() {
|
private void switchCamera() {
|
||||||
if (webRTCClient != null) {
|
// TODO: 切换前后摄像头
|
||||||
webRTCClient.switchCamera();
|
|
||||||
}
|
|
||||||
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "切换摄像头", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCallConnected() {
|
private void onCallConnected() {
|
||||||
Log.d(TAG, "========== 通话已连接 ==========");
|
android.util.Log.d("CallActivity", "onCallConnected() 开始执行");
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
callStartTime = System.currentTimeMillis();
|
callStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
|
@ -498,22 +236,14 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
layoutCallControls.setVisibility(View.VISIBLE);
|
layoutCallControls.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
handler.post(durationRunnable);
|
handler.post(durationRunnable);
|
||||||
|
android.util.Log.d("CallActivity", "onCallConnected() 执行完成,通话已连接");
|
||||||
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "通话已接通", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void releaseAndFinish() {
|
// CallStateListener 实现
|
||||||
if (webRTCClient != null) {
|
|
||||||
webRTCClient.release();
|
|
||||||
webRTCClient = null;
|
|
||||||
}
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== CallStateListener 实现 ====================
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallStateChanged(String state, String callId) {
|
public void onCallStateChanged(String state, String callId) {
|
||||||
Log.d(TAG, "onCallStateChanged: " + state);
|
// 状态变化处理
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -523,8 +253,13 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCallConnected(String callId) {
|
public void onCallConnected(String callId) {
|
||||||
Log.d(TAG, "onCallConnected: " + callId);
|
android.util.Log.d("CallActivity", "========== onCallConnected 被调用 ==========");
|
||||||
runOnUiThread(this::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
|
@Override
|
||||||
|
|
@ -551,7 +286,7 @@ public class CallActivity extends AppCompatActivity implements
|
||||||
message = "通话已结束";
|
message = "通话已结束";
|
||||||
}
|
}
|
||||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
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
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
handler.removeCallbacks(durationRunnable);
|
handler.removeCallbacks(durationRunnable);
|
||||||
|
|
||||||
// 恢复音频模式
|
|
||||||
if (audioManager != null) {
|
|
||||||
audioManager.setMode(AudioManager.MODE_NORMAL);
|
|
||||||
audioManager.setSpeakerphoneOn(false);
|
|
||||||
Log.d(TAG, "音频模式已恢复");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callManager != null) {
|
if (callManager != null) {
|
||||||
callManager.setStateListener(null);
|
callManager.setStateListener(null);
|
||||||
}
|
}
|
||||||
if (webRTCClient != null) {
|
|
||||||
webRTCClient.release();
|
|
||||||
webRTCClient = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
|
// 禁止返回键退出,需要点击挂断
|
||||||
moveTaskToBack(true);
|
moveTaskToBack(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -351,87 +351,19 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
// 启动来电界面
|
// 启动来电界面
|
||||||
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
Log.d(TAG, "启动来电界面 IncomingCallActivity");
|
||||||
Intent intent = new Intent(context, IncomingCallActivity.class);
|
Intent intent = new Intent(context, IncomingCallActivity.class);
|
||||||
// 添加多个 flags 确保能从后台启动
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
| Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
| Intent.FLAG_ACTIVITY_SINGLE_TOP
|
|
||||||
| Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
|
||||||
intent.putExtra("callId", callId);
|
intent.putExtra("callId", callId);
|
||||||
intent.putExtra("callType", callType);
|
intent.putExtra("callType", callType);
|
||||||
intent.putExtra("callerId", callerId);
|
intent.putExtra("callerId", callerId);
|
||||||
intent.putExtra("callerName", callerName);
|
intent.putExtra("callerName", callerName);
|
||||||
intent.putExtra("callerAvatar", callerAvatar);
|
intent.putExtra("callerAvatar", callerAvatar);
|
||||||
|
|
||||||
try {
|
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
Log.d(TAG, "来电界面启动成功");
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "启动来电界面失败: " + e.getMessage(), e);
|
|
||||||
// 如果直接启动失败,尝试使用通知方式
|
|
||||||
showIncomingCallNotification(callId, callerId, callerName, callerAvatar, callType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stateListener != null) {
|
if (stateListener != null) {
|
||||||
stateListener.onIncomingCall(callId, callerId, callerName, callerAvatar, callType);
|
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
|
@Override
|
||||||
public void onCallAccepted(String callId) {
|
public void onCallAccepted(String callId) {
|
||||||
Log.d(TAG, "========== 收到通话接听通知 ==========");
|
Log.d(TAG, "========== 收到通话接听通知 ==========");
|
||||||
|
|
@ -485,56 +417,17 @@ public class CallManager implements CallSignalingClient.SignalingListener {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onOffer(String callId, String sdp) {
|
public void onOffer(String callId, String sdp) {
|
||||||
Log.d(TAG, "收到 Offer");
|
// WebRTC offer处理 - 后续实现
|
||||||
// 通知 CallActivity 处理 Offer
|
|
||||||
if (stateListener instanceof CallActivity) {
|
|
||||||
((CallActivity) stateListener).handleRemoteOffer(sdp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAnswer(String callId, String sdp) {
|
public void onAnswer(String callId, String sdp) {
|
||||||
Log.d(TAG, "收到 Answer");
|
// WebRTC answer处理 - 后续实现
|
||||||
// 通知 CallActivity 处理 Answer
|
|
||||||
if (stateListener instanceof CallActivity) {
|
|
||||||
((CallActivity) stateListener).handleRemoteAnswer(sdp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onIceCandidate(String callId, JSONObject candidate) {
|
public void onIceCandidate(String callId, JSONObject candidate) {
|
||||||
Log.d(TAG, "收到 ICE Candidate");
|
// WebRTC 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface CallCallback {
|
public interface CallCallback {
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,6 @@ public class CallSignalingClient {
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
private int userId;
|
private int userId;
|
||||||
private boolean isConnected = false;
|
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 {
|
public interface SignalingListener {
|
||||||
void onConnected();
|
void onConnected();
|
||||||
|
|
@ -60,26 +54,19 @@ public class CallSignalingClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connect() {
|
public void connect() {
|
||||||
isManualDisconnect = false;
|
|
||||||
doConnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doConnect() {
|
|
||||||
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
|
String wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
|
||||||
if (!wsUrl.endsWith("/")) wsUrl += "/";
|
if (!wsUrl.endsWith("/")) wsUrl += "/";
|
||||||
wsUrl += "ws/call";
|
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();
|
Request request = new Request.Builder().url(wsUrl).build();
|
||||||
webSocket = client.newWebSocket(request, new WebSocketListener() {
|
webSocket = client.newWebSocket(request, new WebSocketListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onOpen(WebSocket webSocket, Response response) {
|
public void onOpen(WebSocket webSocket, Response response) {
|
||||||
Log.d(TAG, "WebSocket connected successfully!");
|
Log.d(TAG, "WebSocket connected");
|
||||||
isConnected = true;
|
isConnected = true;
|
||||||
reconnectAttempts = 0; // 重置重连计数
|
|
||||||
register();
|
register();
|
||||||
startHeartbeat();
|
|
||||||
notifyConnected();
|
notifyConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,49 +86,18 @@ public class CallSignalingClient {
|
||||||
Log.d(TAG, "WebSocket closed: " + reason);
|
Log.d(TAG, "WebSocket closed: " + reason);
|
||||||
isConnected = false;
|
isConnected = false;
|
||||||
notifyDisconnected();
|
notifyDisconnected();
|
||||||
// 非手动断开时尝试重连
|
|
||||||
if (!isManualDisconnect) {
|
|
||||||
scheduleReconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
|
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;
|
isConnected = false;
|
||||||
notifyError("连接失败: " + t.getMessage());
|
notifyError(t.getMessage());
|
||||||
// 尝试重连
|
|
||||||
if (!isManualDisconnect) {
|
|
||||||
scheduleReconnect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
public void disconnect() {
|
||||||
isManualDisconnect = true;
|
|
||||||
reconnectAttempts = 0;
|
|
||||||
stopHeartbeat();
|
|
||||||
if (webSocket != null) {
|
if (webSocket != null) {
|
||||||
webSocket.close(1000, "User disconnect");
|
webSocket.close(1000, "User disconnect");
|
||||||
webSocket = null;
|
webSocket = null;
|
||||||
|
|
@ -149,34 +105,6 @@ public class CallSignalingClient {
|
||||||
isConnected = false;
|
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() {
|
public boolean isConnected() {
|
||||||
return 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;
|
package com.example.livestreaming.net;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
public class ConversationResponse {
|
public class ConversationResponse {
|
||||||
private Integer id;
|
|
||||||
private Integer targetUserId;
|
@SerializedName("id")
|
||||||
private String targetUserName;
|
private String id;
|
||||||
private String targetUserAvatar;
|
|
||||||
|
@SerializedName("title")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@SerializedName("lastMessage")
|
||||||
private String lastMessage;
|
private String lastMessage;
|
||||||
private String lastMessageTime;
|
|
||||||
|
@SerializedName("timeText")
|
||||||
|
private String timeText;
|
||||||
|
|
||||||
|
@SerializedName("unreadCount")
|
||||||
private Integer unreadCount;
|
private Integer unreadCount;
|
||||||
|
|
||||||
public Integer getId() { return id; }
|
@SerializedName("muted")
|
||||||
public void setId(Integer id) { this.id = id; }
|
private Boolean muted;
|
||||||
|
|
||||||
public Integer getTargetUserId() { return targetUserId; }
|
@SerializedName("avatarUrl")
|
||||||
public void setTargetUserId(Integer targetUserId) { this.targetUserId = targetUserId; }
|
private String avatarUrl;
|
||||||
|
|
||||||
public String getTargetUserName() { return targetUserName; }
|
@SerializedName("otherUserId")
|
||||||
public void setTargetUserName(String targetUserName) { this.targetUserName = targetUserName; }
|
private Integer otherUserId;
|
||||||
|
|
||||||
public String getTargetUserAvatar() { return targetUserAvatar; }
|
|
||||||
public void setTargetUserAvatar(String targetUserAvatar) { this.targetUserAvatar = targetUserAvatar; }
|
|
||||||
|
|
||||||
|
public String getId() { return id; }
|
||||||
|
public String getTitle() { return title; }
|
||||||
public String getLastMessage() { return lastMessage; }
|
public String getLastMessage() { return lastMessage; }
|
||||||
public void setLastMessage(String lastMessage) { this.lastMessage = lastMessage; }
|
public String getTimeText() { return timeText; }
|
||||||
|
|
||||||
public String getLastMessageTime() { return lastMessageTime; }
|
|
||||||
public void setLastMessageTime(String lastMessageTime) { this.lastMessageTime = lastMessageTime; }
|
|
||||||
|
|
||||||
public Integer getUnreadCount() { return unreadCount; }
|
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;
|
package com.example.livestreaming.net;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PageResponse<T> {
|
public class PageResponse<T> {
|
||||||
|
|
||||||
|
@SerializedName("list")
|
||||||
private List<T> list;
|
private List<T> list;
|
||||||
|
|
||||||
|
@SerializedName("total")
|
||||||
|
private Long total;
|
||||||
|
|
||||||
|
@SerializedName("page")
|
||||||
private Integer page;
|
private Integer page;
|
||||||
|
|
||||||
|
@SerializedName("limit")
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
private Integer total;
|
|
||||||
|
@SerializedName("totalPage")
|
||||||
private Integer totalPage;
|
private Integer totalPage;
|
||||||
|
|
||||||
public List<T> getList() { return list; }
|
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 Integer getPage() { return page; }
|
||||||
public void setPage(Integer page) { this.page = page; }
|
|
||||||
|
|
||||||
public Integer getLimit() { return limit; }
|
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 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:layout_height="match_parent"
|
||||||
android:background="#1A1A2E">
|
android:background="#1A1A2E">
|
||||||
|
|
||||||
<!-- 背景模糊头像(语音通话时显示) -->
|
<!-- 背景模糊头像 -->
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/ivBackgroundAvatar"
|
android:id="@+id/ivBackgroundAvatar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
@ -13,22 +13,21 @@
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
android:alpha="0.3" />
|
android:alpha="0.3" />
|
||||||
|
|
||||||
<!-- 视频通话时的远程视频容器 -->
|
<!-- 视频通话时的远程视频 -->
|
||||||
<FrameLayout
|
<SurfaceView
|
||||||
android:id="@+id/layoutRemoteVideo"
|
android:id="@+id/remoteVideoView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<!-- 视频通话时的本地视频容器(小窗口) -->
|
<!-- 视频通话时的本地视频(小窗口) -->
|
||||||
<FrameLayout
|
<SurfaceView
|
||||||
android:id="@+id/layoutLocalVideo"
|
android:id="@+id/localVideoView"
|
||||||
android:layout_width="120dp"
|
android:layout_width="120dp"
|
||||||
android:layout_height="160dp"
|
android:layout_height="160dp"
|
||||||
android:layout_gravity="top|end"
|
android:layout_gravity="top|end"
|
||||||
android:layout_marginTop="80dp"
|
android:layout_marginTop="80dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:background="#333333"
|
|
||||||
android:visibility="gone" />
|
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"?>
|
<?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"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="4dp"
|
android:layout_margin="6dp"
|
||||||
app:cardCornerRadius="8dp"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardElevation="2dp">
|
app:cardElevation="0dp">
|
||||||
|
|
||||||
<FrameLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/coverImage"
|
android:id="@+id/coverImage"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="120dp"
|
android:layout_height="0dp"
|
||||||
android:scaleType="centerCrop" />
|
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
|
<TextView
|
||||||
android:id="@+id/liveBadge"
|
android:id="@+id/liveBadge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top|end"
|
android:layout_marginStart="8dp"
|
||||||
android:layout_margin="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:background="@android:color/holo_red_light"
|
android:paddingStart="10dp"
|
||||||
android:paddingHorizontal="6dp"
|
android:paddingEnd="10dp"
|
||||||
android:paddingVertical="2dp"
|
android:paddingTop="4dp"
|
||||||
android:text="直播中"
|
android:paddingBottom="4dp"
|
||||||
|
android:text="LIVE"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="10sp"
|
android:textSize="11sp"
|
||||||
android:visibility="gone" />
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/coverImage"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/coverImage" />
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/infoContainer"
|
||||||
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="bottom"
|
android:paddingStart="10dp"
|
||||||
android:background="#80000000"
|
android:paddingEnd="10dp"
|
||||||
android:orientation="vertical"
|
android:paddingTop="8dp"
|
||||||
android:padding="8dp">
|
android:paddingBottom="10dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/coverImage">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/roomTitle"
|
android:id="@+id/roomTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="2"
|
||||||
android:textColor="@android:color/white"
|
android:text="王者荣耀陪练"
|
||||||
android:textSize="14sp" />
|
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
|
<ImageView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/streamerAvatar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="18dp"
|
||||||
android:orientation="horizontal">
|
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
|
<TextView
|
||||||
android:id="@+id/streamerName"
|
android:id="@+id/streamerName"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_marginStart="6dp"
|
||||||
android:textColor="#CCC"
|
android:layout_marginTop="8dp"
|
||||||
android:textSize="12sp" />
|
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" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/likeIcon"
|
android:id="@+id/likeIcon"
|
||||||
android:layout_width="14dp"
|
android:layout_width="16dp"
|
||||||
android:layout_height="14dp"
|
android:layout_height="16dp"
|
||||||
android:src="@android:drawable/btn_star_big_on"
|
android:layout_marginTop="8dp"
|
||||||
android:visibility="gone" />
|
android:src="@drawable/ic_heart_24"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/likeCount"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/likeCount"
|
android:id="@+id/likeCount"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
android:textColor="#CCC"
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="184"
|
||||||
|
android:textColor="#888888"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
android:visibility="gone" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
</LinearLayout>
|
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||||
</LinearLayout>
|
|
||||||
</FrameLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</androidx.cardview.widget.CardView>
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
|
|
||||||
# 使用 Android Studio 自带的 JDK 17
|
|
||||||
org.gradle.java.home=C:/Program Files/Android/Android Studio/jbr
|
|
||||||
|
|
||||||
systemProp.gradle.wrapperUser=myuser
|
systemProp.gradle.wrapperUser=myuser
|
||||||
systemProp.gradle.wrapperPassword=mypassword
|
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