直播功能正常使用
This commit is contained in:
parent
44d4ded7b2
commit
40bfd4ec5c
BIN
Log/微信图片_20251216175455_1_40.png
Normal file
BIN
Log/微信图片_20251216175455_1_40.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
344
OBS推流问题诊断.md
Normal file
344
OBS推流问题诊断.md
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
# OBS 推流问题诊断与解决
|
||||||
|
|
||||||
|
## 🔍 问题现象
|
||||||
|
|
||||||
|
- ✅ OBS 显示"直播中"
|
||||||
|
- ❌ Android 模拟器显示"未开播"
|
||||||
|
- ❌ 视频无法播放
|
||||||
|
|
||||||
|
## 🎯 根本原因
|
||||||
|
|
||||||
|
**你在 OBS 中使用了错误的推流地址!**
|
||||||
|
|
||||||
|
### ❌ 错误做法
|
||||||
|
你可能直接复制了 Android 应用显示的地址:
|
||||||
|
```
|
||||||
|
rtmp://10.0.2.2:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题**:`10.0.2.2` 是 Android 模拟器专用的地址,OBS 无法识别!
|
||||||
|
|
||||||
|
### ✅ 正确做法
|
||||||
|
OBS 应该使用:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 立即修复(3 步)
|
||||||
|
|
||||||
|
### 步骤 1:停止 OBS 推流
|
||||||
|
1. 在 OBS 中点击 **"停止推流"** 按钮
|
||||||
|
2. 等待状态栏显示停止
|
||||||
|
|
||||||
|
### 步骤 2:修改推流地址
|
||||||
|
|
||||||
|
#### 方法 A:完整地址方式
|
||||||
|
1. 点击 **"设置"** → **"推流"**
|
||||||
|
2. **服务器** 改为:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
**注意**:把 `10.0.2.2` 改成 `localhost`
|
||||||
|
3. **串流密钥**:留空
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
|
||||||
|
#### 方法 B:分开填写方式
|
||||||
|
1. 点击 **"设置"** → **"推流"**
|
||||||
|
2. **服务器**:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live
|
||||||
|
```
|
||||||
|
3. **串流密钥**:
|
||||||
|
```
|
||||||
|
a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 步骤 3:重新开始推流
|
||||||
|
1. 点击 **"开始推流"** 按钮
|
||||||
|
2. 等待 2-3 秒
|
||||||
|
3. 检查状态栏是否显示"直播中"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 验证推流成功
|
||||||
|
|
||||||
|
### 方法 1:查看 OBS 状态栏
|
||||||
|
正常推流应该显示:
|
||||||
|
```
|
||||||
|
直播中 | 00:00:15 | 2500 kb/s | 0 丢帧
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功标志**:
|
||||||
|
- ✅ 显示"直播中"
|
||||||
|
- ✅ 有比特率数据(如 2500 kb/s)
|
||||||
|
- ✅ 丢帧数为 0 或很少
|
||||||
|
|
||||||
|
**失败标志**:
|
||||||
|
- ❌ 提示"连接失败"
|
||||||
|
- ❌ 状态栏显示红色错误
|
||||||
|
- ❌ 比特率为 0
|
||||||
|
|
||||||
|
### 方法 2:检查 SRS 服务器
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:1985/api/v1/streams/
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功时应该看到**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"name": "a9c62788-135e-44cb-bbd1-6dbac2f5bf14",
|
||||||
|
"app": "live",
|
||||||
|
"publish": {
|
||||||
|
"active": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败时会看到**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"streams": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 3:检查后端 API
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:3001/api/rooms
|
||||||
|
```
|
||||||
|
|
||||||
|
找到你的直播间,检查 `isLive` 字段:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "a9c62788-135e-44cb-bbd1-6dbac2f5bf14",
|
||||||
|
"isLive": true, ← 应该是 true
|
||||||
|
"streamUrls": {
|
||||||
|
"rtmp": "rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 4:在 Android 应用中查看
|
||||||
|
1. 回到 Android 模拟器
|
||||||
|
2. 下拉刷新直播间列表
|
||||||
|
3. 直播间状态应该显示 **"直播中"**(红色标签)
|
||||||
|
4. 点击进入应该能看到视频
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 完整的正确配置示例
|
||||||
|
|
||||||
|
### 你的直播间信息
|
||||||
|
```
|
||||||
|
直播间 ID: a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
Stream Key: a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
### OBS 推流设置(完整地址方式)
|
||||||
|
```
|
||||||
|
服务: 自定义...
|
||||||
|
服务器: rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
串流密钥: (留空)
|
||||||
|
```
|
||||||
|
|
||||||
|
### OBS 推流设置(分开填写方式)
|
||||||
|
```
|
||||||
|
服务: 自定义...
|
||||||
|
服务器: rtmp://localhost:1935/live
|
||||||
|
串流密钥: a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
### Android 应用中显示的地址
|
||||||
|
```
|
||||||
|
推流地址: rtmp://10.0.2.2:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
电脑本机 OBS 可用: rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
**记住**:OBS 使用第二个地址(localhost)!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎬 完整操作流程(重新来一遍)
|
||||||
|
|
||||||
|
### 1. 确保后端服务器运行
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 Android 应用中创建直播间
|
||||||
|
- 点击"开始直播"
|
||||||
|
- 填写标题和主播名称
|
||||||
|
- 点击"创建"
|
||||||
|
- **记下 streamKey**(或点击"复制地址")
|
||||||
|
|
||||||
|
### 3. 配置 OBS
|
||||||
|
- 打开 OBS Studio
|
||||||
|
- 点击"设置" → "推流"
|
||||||
|
- 服务:自定义...
|
||||||
|
- 服务器:`rtmp://localhost:1935/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14`
|
||||||
|
- **重要**:使用 `localhost`,不是 `10.0.2.2`
|
||||||
|
- 点击"确定"
|
||||||
|
|
||||||
|
### 4. 添加视频源(如果还没有)
|
||||||
|
- 点击"来源"区域的 + 号
|
||||||
|
- 选择"视频捕获设备"(摄像头)或"显示器捕获"(屏幕)
|
||||||
|
- 配置并确定
|
||||||
|
|
||||||
|
### 5. 开始推流
|
||||||
|
- 点击"开始推流"
|
||||||
|
- 等待 2-3 秒
|
||||||
|
- 检查状态栏显示"直播中"
|
||||||
|
|
||||||
|
### 6. 在 Android 应用中观看
|
||||||
|
- 回到 Android 模拟器
|
||||||
|
- 下拉刷新或重新打开应用
|
||||||
|
- 直播间应该显示"直播中"
|
||||||
|
- 点击进入观看
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 常见错误及解决
|
||||||
|
|
||||||
|
### 错误 1:OBS 提示"连接失败"
|
||||||
|
|
||||||
|
**原因**:推流地址错误
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 检查地址是否使用 `localhost` 而不是 `10.0.2.2`
|
||||||
|
2. 检查 streamKey 是否正确
|
||||||
|
3. 检查 SRS Docker 容器是否运行:`docker ps`
|
||||||
|
|
||||||
|
### 错误 2:OBS 显示"直播中"但 Android 显示"未开播"
|
||||||
|
|
||||||
|
**原因**:推流到了错误的地址或 streamKey 不匹配
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 访问 http://localhost:1985/api/v1/streams/ 查看实际推流的 streamKey
|
||||||
|
2. 对比 Android 应用中的 streamKey 是否一致
|
||||||
|
3. 如果不一致,修改 OBS 的推流地址
|
||||||
|
|
||||||
|
### 错误 3:Android 应用显示"直播中"但视频黑屏
|
||||||
|
|
||||||
|
**原因**:视频格式或网络问题
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 等待 5-10 秒(HLS 需要缓冲)
|
||||||
|
2. 退出直播间重新进入
|
||||||
|
3. 检查后端日志是否有错误
|
||||||
|
4. 确认 HLS 地址可访问:http://localhost:8080/live/a9c62788-135e-44cb-bbd1-6dbac2f5bf14.m3u8
|
||||||
|
|
||||||
|
### 错误 4:推流一段时间后断开
|
||||||
|
|
||||||
|
**原因**:网络不稳定或比特率过高
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 降低 OBS 的视频比特率(设置 → 输出 → 2500 → 1500 Kbps)
|
||||||
|
2. 降低分辨率(设置 → 视频 → 1080p → 720p)
|
||||||
|
3. 检查网络连接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 OBS 设置截图说明
|
||||||
|
|
||||||
|
### 推流设置界面
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 设置 │
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 推流 │
|
||||||
|
│ │
|
||||||
|
│ 服务: [自定义... ▼] │
|
||||||
|
│ │
|
||||||
|
│ 服务器: │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │rtmp://localhost:1935/live/a9c62│ │ ← 填这里
|
||||||
|
│ │788-135e-44cb-bbd1-6dbac2f5bf14 │ │
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 串流密钥: │
|
||||||
|
│ ┌─────────────────────────────────┐ │
|
||||||
|
│ │ │ │ ← 留空
|
||||||
|
│ └─────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [取消] [确定] │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 调试命令
|
||||||
|
|
||||||
|
### 检查 SRS 推流状态
|
||||||
|
```bash
|
||||||
|
curl http://localhost:1985/api/v1/streams/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查后端直播间状态
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/rooms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查特定直播间
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/rooms/a9c62788-135e-44cb-bbd1-6dbac2f5bf14
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查 Docker 容器
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查端口占用
|
||||||
|
```bash
|
||||||
|
netstat -ano | findstr :1935
|
||||||
|
netstat -ano | findstr :3001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 关键要点总结
|
||||||
|
|
||||||
|
### ✅ 正确的做法
|
||||||
|
1. **Android 应用**:用于创建直播间和观看
|
||||||
|
2. **OBS 推流地址**:使用 `localhost`,不是 `10.0.2.2`
|
||||||
|
3. **streamKey**:必须与直播间的 streamKey 完全一致
|
||||||
|
4. **验证推流**:访问 http://localhost:1985/api/v1/streams/ 确认
|
||||||
|
|
||||||
|
### ❌ 常见错误
|
||||||
|
1. 直接复制 Android 应用显示的 `10.0.2.2` 地址给 OBS
|
||||||
|
2. streamKey 输入错误或不完整
|
||||||
|
3. 忘记把 `10.0.2.2` 改成 `localhost`
|
||||||
|
4. SRS 服务器未启动
|
||||||
|
|
||||||
|
### 🎯 记住这个公式
|
||||||
|
```
|
||||||
|
Android 应用显示: rtmp://10.0.2.2:1935/live/{streamKey}
|
||||||
|
↓ 改成 ↓
|
||||||
|
OBS 实际使用: rtmp://localhost:1935/live/{streamKey}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 需要帮助?
|
||||||
|
|
||||||
|
如果按照以上步骤操作后仍然有问题:
|
||||||
|
|
||||||
|
1. 检查后端服务器日志(命令行窗口)
|
||||||
|
2. 检查 OBS 日志(帮助 → 日志文件)
|
||||||
|
3. 访问 http://localhost:1985/api/v1/streams/ 查看实际推流状态
|
||||||
|
4. 截图 OBS 的推流设置和错误信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**现在就去修改 OBS 的推流地址吧!** 🚀
|
||||||
BIN
android-app/.gradle/8.14/checksums/checksums.lock
Normal file
BIN
android-app/.gradle/8.14/checksums/checksums.lock
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/checksums/md5-checksums.bin
Normal file
BIN
android-app/.gradle/8.14/checksums/md5-checksums.bin
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/checksums/sha1-checksums.bin
Normal file
BIN
android-app/.gradle/8.14/checksums/sha1-checksums.bin
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/executionHistory/executionHistory.bin
Normal file
BIN
android-app/.gradle/8.14/executionHistory/executionHistory.bin
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/executionHistory/executionHistory.lock
Normal file
BIN
android-app/.gradle/8.14/executionHistory/executionHistory.lock
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/fileChanges/last-build.bin
Normal file
BIN
android-app/.gradle/8.14/fileChanges/last-build.bin
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/fileHashes/fileHashes.bin
Normal file
BIN
android-app/.gradle/8.14/fileHashes/fileHashes.bin
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/fileHashes/fileHashes.lock
Normal file
BIN
android-app/.gradle/8.14/fileHashes/fileHashes.lock
Normal file
Binary file not shown.
BIN
android-app/.gradle/8.14/fileHashes/resourceHashesCache.bin
Normal file
BIN
android-app/.gradle/8.14/fileHashes/resourceHashesCache.bin
Normal file
Binary file not shown.
0
android-app/.gradle/8.14/gc.properties
Normal file
0
android-app/.gradle/8.14/gc.properties
Normal file
Binary file not shown.
|
|
@ -1,2 +1,2 @@
|
||||||
#Mon Dec 15 18:02:26 CST 2025
|
#Tue Dec 16 16:06:46 CST 2025
|
||||||
gradle.version=8.1
|
gradle.version=8.14
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -32,7 +32,7 @@ import retrofit2.Response;
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private ActivityMainBinding binding;
|
private ActivityMainBinding binding;
|
||||||
private RoomsAdapter adapter;
|
private WaterfallRoomsAdapter waterfallAdapter;
|
||||||
|
|
||||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
private Runnable pollRunnable;
|
private Runnable pollRunnable;
|
||||||
|
|
@ -43,15 +43,19 @@ public class MainActivity extends AppCompatActivity {
|
||||||
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
binding = ActivityMainBinding.inflate(getLayoutInflater());
|
||||||
setContentView(binding.getRoot());
|
setContentView(binding.getRoot());
|
||||||
|
|
||||||
adapter = new RoomsAdapter(room -> {
|
// 使用瀑布流 Adapter
|
||||||
|
waterfallAdapter = new WaterfallRoomsAdapter(room -> {
|
||||||
if (room == null) return;
|
if (room == null) return;
|
||||||
Intent intent = new Intent(MainActivity.this, RoomDetailActivity.class);
|
Intent intent = new Intent(MainActivity.this, RoomDetailActivity.class);
|
||||||
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
|
intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId());
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.roomsRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
// 设置瀑布流布局管理器(2列)
|
||||||
binding.roomsRecyclerView.setAdapter(adapter);
|
androidx.recyclerview.widget.StaggeredGridLayoutManager layoutManager =
|
||||||
|
new androidx.recyclerview.widget.StaggeredGridLayoutManager(2, androidx.recyclerview.widget.StaggeredGridLayoutManager.VERTICAL);
|
||||||
|
binding.roomsRecyclerView.setLayoutManager(layoutManager);
|
||||||
|
binding.roomsRecyclerView.setAdapter(waterfallAdapter);
|
||||||
|
|
||||||
binding.startLiveButton.setOnClickListener(v -> showCreateRoomDialog());
|
binding.startLiveButton.setOnClickListener(v -> showCreateRoomDialog());
|
||||||
}
|
}
|
||||||
|
|
@ -167,15 +171,21 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
String msg = "";
|
String msg = "";
|
||||||
if (!TextUtils.isEmpty(rtmp)) {
|
if (!TextUtils.isEmpty(rtmp)) {
|
||||||
msg += "推流地址(服务器):\n" + rtmp + "\n\n";
|
msg += "推流地址:\n" + rtmp + "\n\n";
|
||||||
}
|
}
|
||||||
if (!TextUtils.isEmpty(rtmpForObs)) {
|
if (!TextUtils.isEmpty(rtmpForObs)) {
|
||||||
msg += "电脑本机 OBS 可用(等价地址):\n" + rtmpForObs + "\n\n";
|
msg += "电脑本机 OBS 可用:\n" + rtmpForObs + "\n\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msg += "📺 OBS 推流设置:\n";
|
||||||
|
msg += "服务器:rtmp://localhost:1935/live\n";
|
||||||
if (!TextUtils.isEmpty(streamKey)) {
|
if (!TextUtils.isEmpty(streamKey)) {
|
||||||
msg += "推流密钥(Stream Key):\n" + streamKey + "\n\n";
|
msg += "串流密钥:" + streamKey + "\n\n";
|
||||||
}
|
}
|
||||||
msg += "提示:用 OBS 推流时,服务器填上面的推流地址,密钥填 streamKey。";
|
msg += "⚠️ 注意:\n";
|
||||||
|
msg += "1. 不要直接粘贴完整地址\n";
|
||||||
|
msg += "2. 要分开填写服务器和密钥\n";
|
||||||
|
msg += "3. Android 应用只能观看直播";
|
||||||
|
|
||||||
String copyRtmp = rtmp;
|
String copyRtmp = rtmp;
|
||||||
String copyKey = streamKey;
|
String copyKey = streamKey;
|
||||||
|
|
@ -184,7 +194,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
.setTitle("已创建直播间")
|
.setTitle("已创建直播间")
|
||||||
.setMessage(msg)
|
.setMessage(msg)
|
||||||
.setNegativeButton("复制地址", (d, w) -> copyToClipboard("rtmp", copyRtmp))
|
.setNegativeButton("复制地址", (d, w) -> copyToClipboard("rtmp", copyRtmp))
|
||||||
.setPositiveButton("复制密钥", (d, w) -> copyToClipboard("streamKey", copyKey))
|
.setPositiveButton("知道了", null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,13 +218,13 @@ public class MainActivity extends AppCompatActivity {
|
||||||
binding.loading.setVisibility(View.GONE);
|
binding.loading.setVisibility(View.GONE);
|
||||||
ApiResponse<List<Room>> body = response.body();
|
ApiResponse<List<Room>> body = response.body();
|
||||||
List<Room> rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList();
|
List<Room> rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList();
|
||||||
adapter.submitList(rooms);
|
waterfallAdapter.submitList(rooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
|
public void onFailure(Call<ApiResponse<List<Room>>> call, Throwable t) {
|
||||||
binding.loading.setVisibility(View.GONE);
|
binding.loading.setVisibility(View.GONE);
|
||||||
adapter.submitList(Collections.emptyList());
|
waterfallAdapter.submitList(Collections.emptyList());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
package com.example.livestreaming;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
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.example.livestreaming.net.Room;
|
||||||
|
|
||||||
|
public class WaterfallRoomsAdapter extends ListAdapter<Room, WaterfallRoomsAdapter.RoomVH> {
|
||||||
|
|
||||||
|
public interface OnRoomClickListener {
|
||||||
|
void onRoomClick(Room room);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OnRoomClickListener onRoomClick;
|
||||||
|
|
||||||
|
public WaterfallRoomsAdapter(OnRoomClickListener onRoomClick) {
|
||||||
|
super(DIFF);
|
||||||
|
this.onRoomClick = onRoomClick;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public RoomVH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.item_room_waterfall, parent, false);
|
||||||
|
return new RoomVH(view, onRoomClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull RoomVH holder, int position) {
|
||||||
|
holder.bind(getItem(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class RoomVH extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
private final ImageView coverImage;
|
||||||
|
private final TextView liveBadge;
|
||||||
|
private final LinearLayout viewerCountLayout;
|
||||||
|
private final TextView viewerCount;
|
||||||
|
private final TextView roomTitle;
|
||||||
|
private final ImageView streamerAvatar;
|
||||||
|
private final TextView streamerName;
|
||||||
|
private final TextView hotBadge;
|
||||||
|
private final OnRoomClickListener onRoomClick;
|
||||||
|
|
||||||
|
RoomVH(View itemView, OnRoomClickListener onRoomClick) {
|
||||||
|
super(itemView);
|
||||||
|
this.onRoomClick = onRoomClick;
|
||||||
|
|
||||||
|
coverImage = itemView.findViewById(R.id.coverImage);
|
||||||
|
liveBadge = itemView.findViewById(R.id.liveBadge);
|
||||||
|
viewerCountLayout = itemView.findViewById(R.id.viewerCountLayout);
|
||||||
|
viewerCount = itemView.findViewById(R.id.viewerCount);
|
||||||
|
roomTitle = itemView.findViewById(R.id.roomTitle);
|
||||||
|
streamerAvatar = itemView.findViewById(R.id.streamerAvatar);
|
||||||
|
streamerName = itemView.findViewById(R.id.streamerName);
|
||||||
|
hotBadge = itemView.findViewById(R.id.hotBadge);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(Room room) {
|
||||||
|
if (room == null) return;
|
||||||
|
|
||||||
|
// 设置标题
|
||||||
|
roomTitle.setText(room.getTitle() != null ? room.getTitle() : "(无标题)");
|
||||||
|
|
||||||
|
// 设置主播名称
|
||||||
|
streamerName.setText(room.getStreamerName() != null ? room.getStreamerName() : "");
|
||||||
|
|
||||||
|
// 设置直播状态
|
||||||
|
if (room.isLive()) {
|
||||||
|
liveBadge.setVisibility(View.VISIBLE);
|
||||||
|
viewerCountLayout.setVisibility(View.VISIBLE);
|
||||||
|
viewerCount.setText(String.valueOf(room.getViewerCount()));
|
||||||
|
|
||||||
|
// 如果观看人数超过100,显示热门标签
|
||||||
|
if (room.getViewerCount() > 100) {
|
||||||
|
hotBadge.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
hotBadge.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
liveBadge.setVisibility(View.GONE);
|
||||||
|
viewerCountLayout.setVisibility(View.GONE);
|
||||||
|
hotBadge.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点击事件
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
if (onRoomClick != null) {
|
||||||
|
onRoomClick.onRoomClick(room);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final DiffUtil.ItemCallback<Room> DIFF = new DiffUtil.ItemCallback<Room>() {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
|
||||||
|
String o = oldItem.getId();
|
||||||
|
String n = newItem.getId();
|
||||||
|
return o != null && o.equals(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull Room oldItem, @NonNull Room newItem) {
|
||||||
|
return oldItem.equals(newItem);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#F0F0F0" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/purple_500" />
|
||||||
|
<corners android:radius="16dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/live_red" />
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/live_red" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#F0F0F0" />
|
||||||
|
<corners android:radius="24dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="#80000000" />
|
||||||
|
<corners android:radius="12dp" />
|
||||||
|
</shape>
|
||||||
|
|
@ -2,40 +2,192 @@
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:background="#F5F5F5">
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<!-- 顶部栏 -->
|
||||||
android:id="@+id/startLiveButton"
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/topBar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_margin="16dp"
|
android:layout_height="56dp"
|
||||||
android:text="开始直播"
|
android:background="@android:color/white"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/titleText"
|
android:id="@+id/menuIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="菜单"
|
||||||
|
android:src="@android:drawable/ic_menu_sort_by_size" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:padding="16dp"
|
android:layout_marginHorizontal="16dp"
|
||||||
android:text="Rooms"
|
android:layout_weight="1"
|
||||||
android:textSize="20sp"
|
android:gravity="center"
|
||||||
android:textStyle="bold"
|
android:orientation="horizontal">
|
||||||
app:layout_constraintEnd_toStartOf="@id/startLiveButton"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabFollow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="关注"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabDiscover"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="发现"
|
||||||
|
android:textColor="@color/purple_500"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabNearby"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="附近"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:contentDescription="消息"
|
||||||
|
android:src="@android:drawable/ic_dialog_email" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notificationIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="通知"
|
||||||
|
android:src="@android:drawable/ic_popup_reminder" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/searchBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="@drawable/search_background"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/topBar">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="搜索"
|
||||||
|
android:src="@android:drawable/ic_menu_search"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="搜索主播/房间/标签"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/voiceSearchIcon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="语音搜索"
|
||||||
|
android:src="@android:drawable/ic_btn_speak_now"
|
||||||
|
android:tint="#999999" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/categoryScroll"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchBar">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryRecommend"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:background="@drawable/category_selected"
|
||||||
|
android:text="推荐"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryBeauty"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="颜值" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryTalent"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="才艺" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryOutdoor"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="户外" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryEntertainment"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="娱乐" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryChat"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="聊天" />
|
||||||
|
</LinearLayout>
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<!-- 瀑布流内容区域 -->
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/roomsRecyclerView"
|
android:id="@+id/roomsRecyclerView"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:paddingBottom="16dp"
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
app:layout_constraintTop_toBottomOf="@id/categoryScroll" />
|
||||||
|
|
||||||
|
<!-- 加载指示器 -->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/loading"
|
android:id="@+id/loading"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
@ -46,4 +198,125 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- 底部导航栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottomNav"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navHome"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="首页"
|
||||||
|
android:src="@android:drawable/ic_menu_view"
|
||||||
|
android:tint="@color/purple_500" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="首页"
|
||||||
|
android:textColor="@color/purple_500"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navDiscover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="发现"
|
||||||
|
android:src="@android:drawable/ic_menu_compass"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="发现"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 中间开播按钮 -->
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/startLiveButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:contentDescription="开播"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:backgroundTint="@color/purple_500"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navMessage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="消息"
|
||||||
|
android:src="@android:drawable/ic_dialog_email"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="消息"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navProfile"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="我的"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="我的"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
||||||
322
android-app/app/src/main/res/layout/activity_main_new.xml
Normal file
322
android-app/app/src/main/res/layout/activity_main_new.xml
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||||
|
android:background="#F5F5F5">
|
||||||
|
|
||||||
|
<!-- 顶部栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/topBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="56dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:elevation="4dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/menuIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="菜单"
|
||||||
|
android:src="@android:drawable/ic_menu_sort_by_size" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabFollow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="关注"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabDiscover"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="发现"
|
||||||
|
android:textColor="@color/purple_500"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tabNearby"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
|
android:text="附近"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="16sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/messageIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:contentDescription="消息"
|
||||||
|
android:src="@android:drawable/ic_dialog_email" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/notificationIcon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="通知"
|
||||||
|
android:src="@android:drawable/ic_popup_reminder" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/searchBar"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:background="@drawable/search_background"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/topBar">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="搜索"
|
||||||
|
android:src="@android:drawable/ic_menu_search"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="搜索主播/房间/标签"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/voiceSearchIcon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:contentDescription="语音搜索"
|
||||||
|
android:src="@android:drawable/ic_btn_speak_now"
|
||||||
|
android:tint="#999999" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 分类标签 -->
|
||||||
|
<HorizontalScrollView
|
||||||
|
android:id="@+id/categoryScroll"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/searchBar">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryRecommend"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:background="@drawable/category_selected"
|
||||||
|
android:text="推荐"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryBeauty"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="颜值" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryTalent"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="才艺" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryOutdoor"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="户外" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryEntertainment"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="娱乐" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categoryChat"
|
||||||
|
style="@style/CategoryChip"
|
||||||
|
android:text="聊天" />
|
||||||
|
</LinearLayout>
|
||||||
|
</HorizontalScrollView>
|
||||||
|
|
||||||
|
<!-- 瀑布流内容区域 -->
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/roomsRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingBottom="80dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/categoryScroll" />
|
||||||
|
|
||||||
|
<!-- 加载指示器 -->
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- 底部导航栏 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/bottomNav"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navHome"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="首页"
|
||||||
|
android:src="@android:drawable/ic_menu_view"
|
||||||
|
android:tint="@color/purple_500" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="首页"
|
||||||
|
android:textColor="@color/purple_500"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navDiscover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="发现"
|
||||||
|
android:src="@android:drawable/ic_menu_compass"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="发现"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 中间开播按钮 -->
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/startLiveButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:contentDescription="开播"
|
||||||
|
android:src="@android:drawable/ic_input_add"
|
||||||
|
app:backgroundTint="@color/purple_500"
|
||||||
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navMessage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="消息"
|
||||||
|
android:src="@android:drawable/ic_dialog_email"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="消息"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/navProfile"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="我的"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:tint="#999999" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="我的"
|
||||||
|
android:textColor="#999999"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
49
android-app/app/src/main/res/layout/activity_main_old.xml
Normal file
49
android-app/app/src/main/res/layout/activity_main_old.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent">
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/startLiveButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dp"
|
||||||
|
android:text="开始直播"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/titleText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:text="Rooms"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/startLiveButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/roomsRecyclerView"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/titleText" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loading"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
@ -3,49 +3,143 @@
|
||||||
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_marginHorizontal="16dp"
|
android:layout_margin="8dp"
|
||||||
android:layout_marginTop="12dp"
|
app:cardCornerRadius="12dp"
|
||||||
app:cardCornerRadius="12dp">
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
<!-- 封面图 -->
|
||||||
android:id="@+id/roomTitle"
|
<ImageView
|
||||||
|
android:id="@+id/coverImage"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:text="Room Title"
|
android:contentDescription="封面"
|
||||||
android:textSize="16sp"
|
android:scaleType="centerCrop"
|
||||||
android:textStyle="bold"
|
android:src="@android:drawable/ic_menu_gallery"
|
||||||
app:layout_constraintEnd_toStartOf="@id/liveBadge"
|
app:layout_constraintDimensionRatio="3:4"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="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:paddingHorizontal="10dp"
|
android:layout_margin="8dp"
|
||||||
|
android:background="@drawable/live_badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
android:paddingVertical="4dp"
|
android:paddingVertical="4dp"
|
||||||
android:text="LIVE"
|
android:text="直播中"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp"
|
android:textSize="10sp"
|
||||||
|
android:textStyle="bold"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- 观看人数 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/viewerCountLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:background="@drawable/viewer_count_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:contentDescription="观看"
|
||||||
|
android:src="@android:drawable/ic_menu_view"
|
||||||
|
android:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/viewerCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:text="0"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 底部信息区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/coverImage">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="直播间标题"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 主播信息 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<!-- 主播头像 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/streamerAvatar"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="主播头像"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:tint="@color/purple_500" />
|
||||||
|
|
||||||
|
<!-- 主播名称 -->
|
||||||
<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_marginTop="6dp"
|
android:layout_marginStart="8dp"
|
||||||
android:text="Streamer"
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="主播名称"
|
||||||
android:textColor="#666666"
|
android:textColor="#666666"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:textSize="12sp" />
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
<!-- 热度标签 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/hotBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/hot_badge_background"
|
||||||
|
android:paddingHorizontal="6dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:text="热"
|
||||||
|
android:textColor="@color/live_red"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
|
||||||
52
android-app/app/src/main/res/layout/item_room_old.xml
Normal file
52
android-app/app/src/main/res/layout/item_room_old.xml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
app:cardCornerRadius="12dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Room Title"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/liveBadge"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/liveBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:text="LIVE"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/streamerName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:text="Streamer"
|
||||||
|
android:textColor="#666666"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/roomTitle" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
146
android-app/app/src/main/res/layout/item_room_waterfall.xml
Normal file
146
android-app/app/src/main/res/layout/item_room_waterfall.xml
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
app:cardCornerRadius="12dp"
|
||||||
|
app:cardElevation="2dp">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<!-- 封面图 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/coverImage"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:contentDescription="封面"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@android:drawable/ic_menu_gallery"
|
||||||
|
app:layout_constraintDimensionRatio="3:4"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- 直播标签 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/liveBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:background="@drawable/live_badge_background"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:text="直播中"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- 观看人数 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/viewerCountLayout"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:background="@drawable/viewer_count_background"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="8dp"
|
||||||
|
android:paddingVertical="4dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="12dp"
|
||||||
|
android:layout_height="12dp"
|
||||||
|
android:contentDescription="观看"
|
||||||
|
android:src="@android:drawable/ic_menu_view"
|
||||||
|
android:tint="@android:color/white" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/viewerCount"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:text="0"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="10sp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- 底部信息区域 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/coverImage">
|
||||||
|
|
||||||
|
<!-- 标题 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/roomTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="直播间标题"
|
||||||
|
android:textColor="#333333"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 主播信息 -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<!-- 主播头像 -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/streamerAvatar"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:contentDescription="主播头像"
|
||||||
|
android:src="@android:drawable/ic_menu_myplaces"
|
||||||
|
android:tint="@color/purple_500" />
|
||||||
|
|
||||||
|
<!-- 主播名称 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/streamerName"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="主播名称"
|
||||||
|
android:textColor="#666666"
|
||||||
|
android:textSize="12sp" />
|
||||||
|
|
||||||
|
<!-- 热度标签 -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/hotBadge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/hot_badge_background"
|
||||||
|
android:paddingHorizontal="6dp"
|
||||||
|
android:paddingVertical="2dp"
|
||||||
|
android:text="热"
|
||||||
|
android:textColor="@color/live_red"
|
||||||
|
android:textSize="10sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
@ -7,9 +7,23 @@
|
||||||
<item name="colorSecondary">@color/teal_200</item>
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
<item name="colorOnSecondary">@android:color/black</item>
|
<item name="colorOnSecondary">@android:color/black</item>
|
||||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
<item name="android:statusBarColor">@android:color/white</item>
|
||||||
<item name="android:navigationBarColor">@android:color/black</item>
|
<item name="android:navigationBarColor">@android:color/white</item>
|
||||||
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
<item name="android:windowLightStatusBar" tools:targetApi="m">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 分类标签样式 -->
|
||||||
|
<style name="CategoryChip">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:paddingStart">16dp</item>
|
||||||
|
<item name="android:paddingEnd">16dp</item>
|
||||||
|
<item name="android:paddingTop">8dp</item>
|
||||||
|
<item name="android:paddingBottom">8dp</item>
|
||||||
|
<item name="android:layout_marginEnd">8dp</item>
|
||||||
|
<item name="android:background">@drawable/category_normal</item>
|
||||||
|
<item name="android:textColor">#666666</item>
|
||||||
|
<item name="android:textSize">14sp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=file:///D:/soft/gradle-8.1-bin.zip
|
distributionUrl=file:///D:/soft/gradle-8.14-bin.zip
|
||||||
networkTimeout=600000
|
networkTimeout=600000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
||||||
235
android-app/新UI设计说明.md
Normal file
235
android-app/新UI设计说明.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Android 直播应用新 UI 设计说明
|
||||||
|
|
||||||
|
## 🎨 设计概览
|
||||||
|
|
||||||
|
已将 Android 应用首页重新设计为现代化的瀑布流布局,参考了主流直播应用的设计风格。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 已完成的修改
|
||||||
|
|
||||||
|
### 1. 新增布局文件
|
||||||
|
|
||||||
|
#### 主界面布局 (`activity_main.xml`)
|
||||||
|
- **顶部导航栏**:
|
||||||
|
- 左侧:菜单图标
|
||||||
|
- 中间:关注/发现/附近 标签页
|
||||||
|
- 右侧:消息和通知图标
|
||||||
|
|
||||||
|
- **搜索栏**:
|
||||||
|
- 搜索框 + 语音搜索按钮
|
||||||
|
- 圆角设计,灰色背景
|
||||||
|
|
||||||
|
- **分类标签**:
|
||||||
|
- 横向滚动
|
||||||
|
- 推荐、颜值、才艺、户外、娱乐、聊天
|
||||||
|
- 选中状态:紫色背景 + 白色文字
|
||||||
|
- 未选中:灰色背景 + 灰色文字
|
||||||
|
|
||||||
|
- **内容区域**:
|
||||||
|
- 瀑布流布局(2列)
|
||||||
|
- RecyclerView + StaggeredGridLayoutManager
|
||||||
|
|
||||||
|
- **底部导航栏**:
|
||||||
|
- 首页、发现、开播(中间大按钮)、消息、我的
|
||||||
|
- 固定在底部,白色背景,带阴影
|
||||||
|
|
||||||
|
#### 直播间卡片布局 (`item_room_waterfall.xml`)
|
||||||
|
- **封面图**:3:4 比例
|
||||||
|
- **直播标签**:左上角红色标签(直播中时显示)
|
||||||
|
- **观看人数**:右上角半透明黑色背景
|
||||||
|
- **底部信息**:
|
||||||
|
- 标题(最多2行)
|
||||||
|
- 主播头像 + 名称
|
||||||
|
- 热门标签(观看人数>100时显示)
|
||||||
|
|
||||||
|
### 2. 新增 Drawable 资源
|
||||||
|
|
||||||
|
| 文件名 | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `search_background.xml` | 搜索框背景(圆角灰色) |
|
||||||
|
| `category_selected.xml` | 选中的分类标签背景(紫色) |
|
||||||
|
| `category_normal.xml` | 未选中的分类标签背景(灰色) |
|
||||||
|
| `live_badge_background.xml` | 直播标签背景(红色) |
|
||||||
|
| `viewer_count_background.xml` | 观看人数背景(半透明黑色) |
|
||||||
|
| `hot_badge_background.xml` | 热门标签背景(红色边框) |
|
||||||
|
|
||||||
|
### 3. 新增 Java 类
|
||||||
|
|
||||||
|
#### `WaterfallRoomsAdapter.java`
|
||||||
|
- 瀑布流适配器
|
||||||
|
- 支持直播状态显示
|
||||||
|
- 支持观看人数显示
|
||||||
|
- 支持热门标签(观看人数>100)
|
||||||
|
|
||||||
|
### 4. 修改的文件
|
||||||
|
|
||||||
|
#### `MainActivity.java`
|
||||||
|
- 使用 `WaterfallRoomsAdapter` 替代 `RoomsAdapter`
|
||||||
|
- 使用 `StaggeredGridLayoutManager` 实现瀑布流(2列)
|
||||||
|
- 保持原有的轮询和点击逻辑
|
||||||
|
|
||||||
|
#### `themes.xml`
|
||||||
|
- 添加 `CategoryChip` 样式
|
||||||
|
- 修改状态栏和导航栏颜色为白色
|
||||||
|
- 启用浅色状态栏图标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 设计细节
|
||||||
|
|
||||||
|
### 颜色方案
|
||||||
|
- **主色调**:紫色 (`#6200EE`)
|
||||||
|
- **强调色**:红色 (`#E53935`) - 用于直播标签
|
||||||
|
- **背景色**:浅灰 (`#F5F5F5`)
|
||||||
|
- **文字色**:
|
||||||
|
- 主要文字:`#333333`
|
||||||
|
- 次要文字:`#666666`
|
||||||
|
- 提示文字:`#999999`
|
||||||
|
|
||||||
|
### 圆角设计
|
||||||
|
- 搜索框:24dp
|
||||||
|
- 分类标签:16dp
|
||||||
|
- 直播间卡片:12dp
|
||||||
|
- 直播标签:4dp
|
||||||
|
|
||||||
|
### 间距规范
|
||||||
|
- 页面边距:16dp
|
||||||
|
- 卡片间距:8dp
|
||||||
|
- 元素内边距:12dp
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📱 功能说明
|
||||||
|
|
||||||
|
### 当前已实现
|
||||||
|
- ✅ 瀑布流布局展示直播间
|
||||||
|
- ✅ 直播状态标签
|
||||||
|
- ✅ 观看人数显示
|
||||||
|
- ✅ 点击进入直播间
|
||||||
|
- ✅ 自动刷新列表(5秒轮询)
|
||||||
|
- ✅ 开播按钮(底部导航中间)
|
||||||
|
|
||||||
|
### 待实现(UI已准备好)
|
||||||
|
- ⏳ 搜索功能
|
||||||
|
- ⏳ 语音搜索
|
||||||
|
- ⏳ 分类筛选
|
||||||
|
- ⏳ 关注/发现/附近 标签页切换
|
||||||
|
- ⏳ 消息和通知功能
|
||||||
|
- ⏳ 底部导航其他页面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 如何编译和运行
|
||||||
|
|
||||||
|
### 1. 清理项目
|
||||||
|
```bash
|
||||||
|
cd android-app
|
||||||
|
gradlew clean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 编译
|
||||||
|
```bash
|
||||||
|
gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 安装到模拟器
|
||||||
|
```bash
|
||||||
|
gradlew installDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
或者在 Android Studio 中直接点击 ▶️ Run
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📸 布局预览
|
||||||
|
|
||||||
|
### 主界面结构
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ ☰ 关注 发现 附近 ✉ 🔔 │ ← 顶部导航
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 🔍 搜索主播/房间/标签 🎤 │ ← 搜索栏
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ [推荐][颜值][才艺][户外]... │ ← 分类标签
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ ┌──────┐ ┌──────┐ │
|
||||||
|
│ │ 封面 │ │ 封面 │ │
|
||||||
|
│ │ 图片 │ │ 图片 │ │
|
||||||
|
│ │ │ │ │ │ ← 瀑布流内容
|
||||||
|
│ │ 标题 │ │ 标题 │ │
|
||||||
|
│ │ 主播 │ │ 主播 │ │
|
||||||
|
│ └──────┘ └──────┘ │
|
||||||
|
│ ┌──────┐ ┌──────┐ │
|
||||||
|
│ │ ... │ │ ... │ │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
│ 🏠 🔍 ➕ 💬 👤 │ ← 底部导航
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 直播间卡片结构
|
||||||
|
```
|
||||||
|
┌─────────────────┐
|
||||||
|
│ [直播中] 👁 123│ ← 标签和观看人数
|
||||||
|
│ │
|
||||||
|
│ 封面图片 │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
├─────────────────┤
|
||||||
|
│ 直播间标题 │
|
||||||
|
│ 👤 主播名称 [热] │ ← 主播信息
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 已知问题
|
||||||
|
|
||||||
|
1. **封面图片**:当前使用默认图标,需要后端提供封面图 URL
|
||||||
|
2. **主播头像**:当前使用默认图标,需要后端提供头像 URL
|
||||||
|
3. **分类筛选**:UI已完成,但功能未实现
|
||||||
|
4. **搜索功能**:UI已完成,但功能未实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 如何恢复旧版 UI
|
||||||
|
|
||||||
|
如果需要恢复旧版 UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android-app/app/src/main/res/layout
|
||||||
|
Copy-Item activity_main_old.xml activity_main.xml -Force
|
||||||
|
Copy-Item item_room_old.xml item_room.xml -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在 `MainActivity.java` 中:
|
||||||
|
- 将 `WaterfallRoomsAdapter` 改回 `RoomsAdapter`
|
||||||
|
- 将 `StaggeredGridLayoutManager` 改回 `LinearLayoutManager`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 后续优化建议
|
||||||
|
|
||||||
|
### 1. 添加封面图支持
|
||||||
|
- 后端返回封面图 URL
|
||||||
|
- 使用 Glide 或 Picasso 加载图片
|
||||||
|
|
||||||
|
### 2. 实现分类筛选
|
||||||
|
- 点击分类标签时筛选对应类型的直播间
|
||||||
|
- 添加分类字段到 Room 模型
|
||||||
|
|
||||||
|
### 3. 实现搜索功能
|
||||||
|
- 点击搜索框打开搜索页面
|
||||||
|
- 支持搜索主播名称和直播间标题
|
||||||
|
|
||||||
|
### 4. 添加下拉刷新
|
||||||
|
- 使用 SwipeRefreshLayout
|
||||||
|
- 手动刷新直播间列表
|
||||||
|
|
||||||
|
### 5. 添加加载更多
|
||||||
|
- 监听滚动到底部
|
||||||
|
- 分页加载直播间数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**设计完成!现在可以编译运行查看新的 UI 效果了!** 🎉
|
||||||
132
android-app/网络连接修复说明.md
Normal file
132
android-app/网络连接修复说明.md
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
# Android 直播应用问题修复说明
|
||||||
|
|
||||||
|
## 问题诊断
|
||||||
|
|
||||||
|
### 1. RTMP 地址格式错误 ❌
|
||||||
|
**问题**:后端返回的 RTMP 地址不完整
|
||||||
|
- 错误格式:`rtmp://10.0.2.2:1935/live`
|
||||||
|
- 正确格式:`rtmp://10.0.2.2:1935/live/{streamKey}`
|
||||||
|
|
||||||
|
**原因**:`live-streaming/server/utils/streamUrl.js` 中缺少 streamKey
|
||||||
|
|
||||||
|
### 2. Android 应用功能误解 ⚠️
|
||||||
|
**重要**:你的 Android 应用**不支持推流**!
|
||||||
|
|
||||||
|
当前应用功能:
|
||||||
|
- ✅ 创建直播间
|
||||||
|
- ✅ 查看直播列表
|
||||||
|
- ✅ 观看直播(播放 HLS/FLV 流)
|
||||||
|
- ❌ **不能**从 Android 设备推流
|
||||||
|
|
||||||
|
正确的使用流程:
|
||||||
|
1. 在 Android 应用中创建直播间
|
||||||
|
2. 获取 RTMP 推流地址
|
||||||
|
3. **在电脑上用 OBS Studio 推流**
|
||||||
|
4. 在 Android 应用中观看直播
|
||||||
|
|
||||||
|
## 已完成的修复 ✅
|
||||||
|
|
||||||
|
### 1. 修复后端 RTMP 地址
|
||||||
|
修改了 `live-streaming/server/utils/streamUrl.js`:
|
||||||
|
```javascript
|
||||||
|
// 修复前
|
||||||
|
rtmp: `rtmp://${host}:${rtmpPort}/live`,
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 更新 Android 应用提示
|
||||||
|
修改了 `MainActivity.java` 中的提示信息,明确说明需要用 OBS 推流。
|
||||||
|
|
||||||
|
## 如何使用
|
||||||
|
|
||||||
|
### 步骤 1:启动后端服务器
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
确认服务器运行:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:重新构建 Android 应用
|
||||||
|
```bash
|
||||||
|
cd android-app
|
||||||
|
gradlew clean assembleDebug
|
||||||
|
gradlew installDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
或在 Android Studio 中点击 "Run"
|
||||||
|
|
||||||
|
### 步骤 3:创建直播间
|
||||||
|
1. 在 Android 应用中点击"开始直播"
|
||||||
|
2. 填写直播间标题和主播名称
|
||||||
|
3. 创建成功后会显示 RTMP 推流地址
|
||||||
|
|
||||||
|
### 步骤 4:使用 OBS 推流
|
||||||
|
1. 打开 OBS Studio
|
||||||
|
2. 设置 → 推流
|
||||||
|
3. 服务器:`rtmp://localhost:1935/live/{你的streamKey}`
|
||||||
|
- 或直接填写应用显示的完整地址
|
||||||
|
4. 开始推流
|
||||||
|
|
||||||
|
### 步骤 5:在 Android 应用中观看
|
||||||
|
推流成功后,直播间会显示"直播中"状态,点击进入即可观看。
|
||||||
|
|
||||||
|
## Android 模拟器网络说明
|
||||||
|
|
||||||
|
在 Android 模拟器中:
|
||||||
|
- `10.0.2.2` = 宿主机的 `localhost`
|
||||||
|
- `10.0.2.16` = 模拟器自己的 IP
|
||||||
|
|
||||||
|
所以:
|
||||||
|
- Android 应用访问 API:`http://10.0.2.2:3001/api/`
|
||||||
|
- OBS 推流地址:`rtmp://localhost:1935/live/{streamKey}`
|
||||||
|
- Android 观看地址:`http://10.0.2.2:8080/live/{streamKey}.flv`
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 为什么 Android 应用不能直接推流?
|
||||||
|
A: 从 Android 设备推流需要:
|
||||||
|
- 摄像头/麦克风权限
|
||||||
|
- RTMP 推流库(如 librtmp)
|
||||||
|
- 视频编码处理
|
||||||
|
- 当前应用没有实现这些功能
|
||||||
|
|
||||||
|
### Q: 如果想让 Android 应用支持推流怎么办?
|
||||||
|
A: 需要添加推流功能,可以使用:
|
||||||
|
- [yasea](https://github.com/begeekmyfriend/yasea) - Android RTMP 推流库
|
||||||
|
- [LiveVideoBroadcaster](https://github.com/ant-media/LiveVideoBroadcaster) - Ant Media 的推流方案
|
||||||
|
|
||||||
|
### Q: 推流后 Android 应用看不到直播?
|
||||||
|
A: 检查:
|
||||||
|
1. OBS 是否成功推流(查看 OBS 状态栏)
|
||||||
|
2. 后端服务器是否收到推流(查看服务器日志)
|
||||||
|
3. RTMP 地址和 streamKey 是否正确
|
||||||
|
4. 防火墙是否阻止了端口 1935 和 8080
|
||||||
|
|
||||||
|
### Q: 出现"服务器异常"错误?
|
||||||
|
A: 可能原因:
|
||||||
|
1. 后端服务器未启动
|
||||||
|
2. 网络连接问题
|
||||||
|
3. API 地址配置错误
|
||||||
|
4. 查看 Android Studio 的 Logcat 获取详细错误信息
|
||||||
|
|
||||||
|
## 防火墙配置
|
||||||
|
|
||||||
|
如果遇到连接问题,添加防火墙规则:
|
||||||
|
```powershell
|
||||||
|
netsh advfirewall firewall add rule name="Node.js API" dir=in action=allow protocol=TCP localport=3001
|
||||||
|
netsh advfirewall firewall add rule name="RTMP Server" dir=in action=allow protocol=TCP localport=1935
|
||||||
|
netsh advfirewall firewall add rule name="HTTP-FLV" dir=in action=allow protocol=TCP localport=8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
- 后端服务器:`live-streaming/server/index.js`
|
||||||
|
- RTMP 地址生成:`live-streaming/server/utils/streamUrl.js`
|
||||||
|
- Android 配置:`android-app/app/build.gradle.kts`
|
||||||
|
- Android 主界面:`android-app/app/src/main/java/com/example/livestreaming/MainActivity.java`
|
||||||
|
- Android 直播间:`android-app/app/src/main/java/com/example/livestreaming/RoomDetailActivity.java`
|
||||||
160
android-app/问题修复总结.md
Normal file
160
android-app/问题修复总结.md
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
# Android 直播应用问题修复总结
|
||||||
|
|
||||||
|
## 问题现象
|
||||||
|
OBS 推流成功,但 Android 模拟器显示"主播尚未开播"(未开播状态)
|
||||||
|
|
||||||
|
## 根本原因
|
||||||
|
|
||||||
|
### 1. SRS API URL 格式错误 ❌
|
||||||
|
后端代码访问 SRS API 时缺少末尾斜杠:
|
||||||
|
- 错误:`http://localhost:1985/api/v1/streams?count=100`
|
||||||
|
- 正确:`http://localhost:1985/api/v1/streams/?count=100`
|
||||||
|
|
||||||
|
SRS 服务器对缺少斜杠的请求返回 302 重定向,导致后端无法获取推流状态。
|
||||||
|
|
||||||
|
### 2. 缺少 SRS_API_PORT 配置 ❌
|
||||||
|
`.env` 文件中没有配置 `SRS_API_PORT=1985`,虽然代码有默认值,但明确配置更好。
|
||||||
|
|
||||||
|
### 3. 房间数据未持久化 ❌
|
||||||
|
重启服务器后,内存中的房间数据丢失,导致推流无法关联到房间。
|
||||||
|
|
||||||
|
## 已完成的修复 ✅
|
||||||
|
|
||||||
|
### 1. 修复 SRS API URL
|
||||||
|
**文件**:`live-streaming/server/utils/srsHttpApi.js`
|
||||||
|
```javascript
|
||||||
|
// 修复前
|
||||||
|
const url = `http://${host}:${apiPort}/api/v1/streams?count=100`;
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
const url = `http://${host}:${apiPort}/api/v1/streams/?count=100`;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 添加 SRS_API_PORT 配置
|
||||||
|
**文件**:`live-streaming/.env`
|
||||||
|
```env
|
||||||
|
SRS_API_PORT=1985
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 实现房间数据持久化
|
||||||
|
**文件**:`live-streaming/server/store/roomStore.js`
|
||||||
|
- 添加了文件存储功能
|
||||||
|
- 房间数据保存在 `live-streaming/data/rooms.json`
|
||||||
|
- 服务器重启后自动加载房间数据
|
||||||
|
|
||||||
|
### 4. 修复 RTMP 地址格式
|
||||||
|
**文件**:`live-streaming/server/utils/streamUrl.js`
|
||||||
|
```javascript
|
||||||
|
// 修复前
|
||||||
|
rtmp: `rtmp://${host}:${rtmpPort}/live`,
|
||||||
|
|
||||||
|
// 修复后
|
||||||
|
rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`,
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 改进错误日志
|
||||||
|
添加了更详细的 SRS API 错误信息,便于调试。
|
||||||
|
|
||||||
|
## 验证结果 ✅
|
||||||
|
|
||||||
|
测试 API 返回:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isLive": true,
|
||||||
|
"streamKey": "868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"streamUrls": {
|
||||||
|
"rtmp": "rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"flv": "http://localhost:8080/live/868c49cc-1021-4664-95a3-ed71e789adb2.flv",
|
||||||
|
"hls": "http://localhost:8080/live/868c49cc-1021-4664-95a3-ed71e789adb2.m3u8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 1. 启动服务
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 Android 应用中
|
||||||
|
- 打开应用,查看房间列表
|
||||||
|
- 点击房间进入,应该显示"直播中"状态
|
||||||
|
- 视频播放器会自动加载 HLS 流
|
||||||
|
|
||||||
|
### 3. OBS 推流设置
|
||||||
|
- 服务器:`rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2`
|
||||||
|
- 或者分开填写:
|
||||||
|
- 服务器:`rtmp://localhost:1935/live`
|
||||||
|
- 串流密钥:`868c49cc-1021-4664-95a3-ed71e789adb2`
|
||||||
|
|
||||||
|
## 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐
|
||||||
|
│ OBS 推流 │
|
||||||
|
└──────┬──────┘
|
||||||
|
│ RTMP (1935)
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ SRS 服务器 │
|
||||||
|
│ (Docker) │
|
||||||
|
│ - RTMP: 1935 │
|
||||||
|
│ - HTTP: 8080 │
|
||||||
|
│ - API: 1985 │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
│ HTTP API (1985)
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Node.js 后端 │
|
||||||
|
│ (端口 3001) │
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
│ REST API
|
||||||
|
↓
|
||||||
|
┌─────────────────┐
|
||||||
|
│ Android 应用 │
|
||||||
|
│ (模拟器) │
|
||||||
|
└─────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 网络地址映射
|
||||||
|
|
||||||
|
| 位置 | localhost | 10.0.2.2 | 说明 |
|
||||||
|
|------|-----------|----------|------|
|
||||||
|
| 电脑 OBS | ✅ | ❌ | 使用 localhost |
|
||||||
|
| Android 模拟器 | ❌ | ✅ | 使用 10.0.2.2 |
|
||||||
|
| 后端服务器 | ✅ | ✅ | 监听 0.0.0.0 |
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 重启服务器后房间消失?
|
||||||
|
A: 已修复!现在房间数据会保存在 `live-streaming/data/rooms.json`
|
||||||
|
|
||||||
|
### Q: Android 应用显示"未开播"?
|
||||||
|
A: 检查:
|
||||||
|
1. OBS 是否正在推流
|
||||||
|
2. 后端服务器是否运行
|
||||||
|
3. SRS Docker 容器是否运行:`docker ps`
|
||||||
|
4. 查看后端日志是否有 SRS API 错误
|
||||||
|
|
||||||
|
### Q: 视频播放黑屏?
|
||||||
|
A: 可能原因:
|
||||||
|
1. HLS 转码未启用(需要 FFmpeg)
|
||||||
|
2. Android 播放器不支持 FLV
|
||||||
|
3. 网络延迟或缓冲
|
||||||
|
|
||||||
|
### Q: 如何查看 SRS 推流状态?
|
||||||
|
A: 访问 `http://localhost:1985/api/v1/streams/`
|
||||||
|
|
||||||
|
## 相关文件
|
||||||
|
|
||||||
|
- 后端服务器:`live-streaming/server/index.js`
|
||||||
|
- SRS API 工具:`live-streaming/server/utils/srsHttpApi.js`
|
||||||
|
- 流地址生成:`live-streaming/server/utils/streamUrl.js`
|
||||||
|
- 房间存储:`live-streaming/server/store/roomStore.js`
|
||||||
|
- 环境配置:`live-streaming/.env`
|
||||||
|
- SRS 配置:`live-streaming/docker/srs/srs.conf`
|
||||||
|
- Android 配置:`android-app/app/build.gradle.kts`
|
||||||
|
|
@ -3,5 +3,6 @@ PORT=3001
|
||||||
SRS_HOST=localhost
|
SRS_HOST=localhost
|
||||||
SRS_RTMP_PORT=1935
|
SRS_RTMP_PORT=1935
|
||||||
SRS_HTTP_PORT=8080
|
SRS_HTTP_PORT=8080
|
||||||
|
SRS_API_PORT=1985
|
||||||
CLIENT_URL=http://localhost:3000
|
CLIENT_URL=http://localhost:3000
|
||||||
EMBEDDED_MEDIA_SERVER=0
|
EMBEDDED_MEDIA_SERVER=0
|
||||||
22
live-streaming/data/rooms.json
Normal file
22
live-streaming/data/rooms.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "7f4acb94-f91c-4ec0-84eb-fbb1855f8f18",
|
||||||
|
"title": "11",
|
||||||
|
"streamerName": "111",
|
||||||
|
"streamKey": "7f4acb94-f91c-4ec0-84eb-fbb1855f8f18",
|
||||||
|
"isLive": false,
|
||||||
|
"viewerCount": 0,
|
||||||
|
"createdAt": "2025-12-16T09:37:13.684Z",
|
||||||
|
"startedAt": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a85b8d00-5f9c-46a9-b664-f3153812b516",
|
||||||
|
"title": "22",
|
||||||
|
"streamerName": "22",
|
||||||
|
"streamKey": "a85b8d00-5f9c-46a9-b664-f3153812b516",
|
||||||
|
"isLive": false,
|
||||||
|
"viewerCount": 0,
|
||||||
|
"createdAt": "2025-12-16T09:50:13.396Z",
|
||||||
|
"startedAt": null
|
||||||
|
}
|
||||||
|
]
|
||||||
18
live-streaming/node_modules/.package-lock.json
generated
vendored
18
live-streaming/node_modules/.package-lock.json
generated
vendored
|
|
@ -35,6 +35,7 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -1405,6 +1406,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -2291,6 +2293,22 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"ideallyInert": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
|
|
||||||
2
live-streaming/package-lock.json
generated
2
live-streaming/package-lock.json
generated
|
|
@ -52,6 +52,7 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -1422,6 +1423,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
|
||||||
|
|
@ -145,8 +145,9 @@ app.get('/health', (req, res) => {
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
// 启动服务
|
// 启动服务
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`API 服务运行在 http://localhost:${PORT}`);
|
console.log(`API 服务运行在 http://localhost:${PORT}`);
|
||||||
|
console.log(`API 服务也可通过 http://0.0.0.0:${PORT} 访问(用于 Android 模拟器)`);
|
||||||
console.log(`SRS RTMP: rtmp://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_RTMP_PORT || 1935}/live/{streamKey}`);
|
console.log(`SRS RTMP: rtmp://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_RTMP_PORT || 1935}/live/{streamKey}`);
|
||||||
console.log(`SRS HTTP: http://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_HTTP_PORT || 8080}/live/{streamKey}.flv`);
|
console.log(`SRS HTTP: http://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_HTTP_PORT || 8080}/live/{streamKey}.flv`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,47 @@
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require('uuid');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
// 内存存储
|
// 持久化文件路径
|
||||||
const rooms = new Map();
|
const STORAGE_FILE = path.join(__dirname, '../../data/rooms.json');
|
||||||
|
|
||||||
|
// 确保数据目录存在
|
||||||
|
const ensureDataDir = () => {
|
||||||
|
const dir = path.dirname(STORAGE_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从文件加载房间数据
|
||||||
|
const loadRooms = () => {
|
||||||
|
try {
|
||||||
|
ensureDataDir();
|
||||||
|
if (fs.existsSync(STORAGE_FILE)) {
|
||||||
|
const data = fs.readFileSync(STORAGE_FILE, 'utf8');
|
||||||
|
const roomsArray = JSON.parse(data);
|
||||||
|
return new Map(roomsArray.map(room => [room.id, room]));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[RoomStore] Failed to load rooms from file:', e.message);
|
||||||
|
}
|
||||||
|
return new Map();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存房间数据到文件
|
||||||
|
const saveRooms = (rooms) => {
|
||||||
|
try {
|
||||||
|
ensureDataDir();
|
||||||
|
const roomsArray = Array.from(rooms.values());
|
||||||
|
fs.writeFileSync(STORAGE_FILE, JSON.stringify(roomsArray, null, 2), 'utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[RoomStore] Failed to save rooms to file:', e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内存存储(从文件加载)
|
||||||
|
const rooms = loadRooms();
|
||||||
|
console.log(`[RoomStore] Loaded ${rooms.size} rooms from storage`);
|
||||||
|
|
||||||
const roomStore = {
|
const roomStore = {
|
||||||
// 创建房间
|
// 创建房间
|
||||||
|
|
@ -18,6 +58,7 @@ const roomStore = {
|
||||||
startedAt: null
|
startedAt: null
|
||||||
};
|
};
|
||||||
rooms.set(id, room);
|
rooms.set(id, room);
|
||||||
|
saveRooms(rooms);
|
||||||
return room;
|
return room;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -43,6 +84,7 @@ const roomStore = {
|
||||||
|
|
||||||
const updated = { ...room, ...data };
|
const updated = { ...room, ...data };
|
||||||
rooms.set(id, updated);
|
rooms.set(id, updated);
|
||||||
|
saveRooms(rooms);
|
||||||
return updated;
|
return updated;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -54,12 +96,15 @@ const roomStore = {
|
||||||
room.isLive = isLive;
|
room.isLive = isLive;
|
||||||
room.startedAt = isLive ? new Date().toISOString() : null;
|
room.startedAt = isLive ? new Date().toISOString() : null;
|
||||||
rooms.set(streamKey, room);
|
rooms.set(streamKey, room);
|
||||||
|
saveRooms(rooms);
|
||||||
return room;
|
return room;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除房间
|
// 删除房间
|
||||||
delete(id) {
|
delete(id) {
|
||||||
return rooms.delete(id);
|
const result = rooms.delete(id);
|
||||||
|
if (result) saveRooms(rooms);
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 清空所有房间 (测试用)
|
// 清空所有房间 (测试用)
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ const getActiveStreamKeys = async ({ app = 'live' } = {}) => {
|
||||||
const host = process.env.SRS_HOST || 'localhost';
|
const host = process.env.SRS_HOST || 'localhost';
|
||||||
const apiPort = process.env.SRS_API_PORT || 1985;
|
const apiPort = process.env.SRS_API_PORT || 1985;
|
||||||
|
|
||||||
const url = `http://${host}:${apiPort}/api/v1/streams?count=100`;
|
const url = `http://${host}:${apiPort}/api/v1/streams/?count=100`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = await requestJson(url, { timeoutMs: 1500 });
|
const payload = await requestJson(url, { timeoutMs: 1500 });
|
||||||
|
|
@ -77,7 +77,9 @@ const getActiveStreamKeys = async ({ app = 'live' } = {}) => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!warnedOnce) {
|
if (!warnedOnce) {
|
||||||
warnedOnce = true;
|
warnedOnce = true;
|
||||||
console.warn(`[SRS] HTTP API unavailable at ${url}. Will fallback to callbacks/in-memory status.`);
|
console.warn(`[SRS] HTTP API unavailable at ${url}`);
|
||||||
|
console.warn(`[SRS] Error: ${e.message}`);
|
||||||
|
console.warn(`[SRS] Will fallback to callbacks/in-memory status.`);
|
||||||
}
|
}
|
||||||
return new Set();
|
return new Set();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ const generateStreamUrls = (streamKey, requestHost) => {
|
||||||
: !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase());
|
: !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 推流地址 (给主播用)
|
// 推流地址 (给主播用) - 完整路径包含 streamKey
|
||||||
rtmp: `rtmp://${host}:${rtmpPort}/live`,
|
rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`,
|
||||||
|
|
||||||
// 播放地址 (给观众用)
|
// 播放地址 (给观众用)
|
||||||
flv: `http://${host}:${httpPort}/live/${streamKey}.flv`,
|
flv: `http://${host}:${httpPort}/live/${streamKey}.flv`,
|
||||||
|
|
|
||||||
594
使用教程.md
Normal file
594
使用教程.md
Normal file
|
|
@ -0,0 +1,594 @@
|
||||||
|
# Android 直播系统完整使用教程
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
1. [系统架构说明](#系统架构说明)
|
||||||
|
2. [环境准备](#环境准备)
|
||||||
|
3. [启动后端服务](#启动后端服务)
|
||||||
|
4. [Android Studio 使用教程](#android-studio-使用教程)
|
||||||
|
5. [OBS Studio 推流教程](#obs-studio-推流教程)
|
||||||
|
6. [完整操作流程](#完整操作流程)
|
||||||
|
7. [常见问题解决](#常见问题解决)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 系统架构说明
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ 电脑 OBS │ ← 你在这里推流(摄像头/屏幕)
|
||||||
|
└──────┬───────┘
|
||||||
|
│ RTMP 推流
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ SRS 服务器 │ ← 接收推流,转换格式
|
||||||
|
│ (Docker) │
|
||||||
|
└──────┬───────┘
|
||||||
|
│
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ Node.js 后端 │ ← 管理直播间,提供 API
|
||||||
|
└──────┬───────┘
|
||||||
|
│ REST API
|
||||||
|
↓
|
||||||
|
┌──────────────┐
|
||||||
|
│ Android 应用 │ ← 观看直播
|
||||||
|
│ (模拟器) │
|
||||||
|
└──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**角色说明**:
|
||||||
|
- **OBS**:主播端,用于推流(发送视频)
|
||||||
|
- **Android 应用**:观众端,用于观看直播
|
||||||
|
- **后端服务**:管理直播间,连接 OBS 和 Android
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 环境准备
|
||||||
|
|
||||||
|
### 1. 必需软件
|
||||||
|
|
||||||
|
#### ✅ 已安装
|
||||||
|
- [x] Node.js
|
||||||
|
- [x] Docker Desktop
|
||||||
|
- [x] Android Studio
|
||||||
|
- [x] Android 模拟器
|
||||||
|
|
||||||
|
#### 📥 需要下载
|
||||||
|
- [ ] **OBS Studio** - 推流软件
|
||||||
|
- 下载地址:https://obsproject.com/download
|
||||||
|
- 选择 Windows 版本
|
||||||
|
- 安装后重启电脑
|
||||||
|
|
||||||
|
### 2. 检查 Docker 容器
|
||||||
|
|
||||||
|
打开命令行,运行:
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到 `srs-server` 容器正在运行:
|
||||||
|
```
|
||||||
|
CONTAINER ID IMAGE PORTS NAMES
|
||||||
|
f24a21e0bc02 ossrs/srs:5 0.0.0.0:1935->1935/tcp srs-server
|
||||||
|
0.0.0.0:8080->8080/tcp
|
||||||
|
0.0.0.0:1985->1985/tcp
|
||||||
|
```
|
||||||
|
|
||||||
|
如果没有运行,启动它:
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 启动后端服务
|
||||||
|
|
||||||
|
### 步骤 1:打开命令行
|
||||||
|
|
||||||
|
在项目根目录打开 PowerShell 或 CMD
|
||||||
|
|
||||||
|
### 步骤 2:进入后端目录
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:启动服务器
|
||||||
|
```bash
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4:验证服务器运行
|
||||||
|
|
||||||
|
你应该看到类似输出:
|
||||||
|
```
|
||||||
|
[RoomStore] Loaded 1 rooms from storage
|
||||||
|
[Media Server] Embedded NodeMediaServer disabled (EMBEDDED_MEDIA_SERVER=0).
|
||||||
|
API 服务运行在 http://localhost:3001
|
||||||
|
API 服务也可通过 http://0.0.0.0:3001 访问(用于 Android 模拟器)
|
||||||
|
SRS RTMP: rtmp://localhost:1935/live/{streamKey}
|
||||||
|
SRS HTTP: http://localhost:8080/live/{streamKey}.flv
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 重要**:保持这个命令行窗口打开,不要关闭!
|
||||||
|
|
||||||
|
### 步骤 5:测试服务器(可选)
|
||||||
|
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:3001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
应该看到:
|
||||||
|
```json
|
||||||
|
{"status":"ok","timestamp":"2025-12-16T09:30:00.000Z"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Android Studio 使用教程
|
||||||
|
|
||||||
|
### 步骤 1:打开项目
|
||||||
|
|
||||||
|
1. 启动 **Android Studio**
|
||||||
|
2. 点击 **Open**
|
||||||
|
3. 选择 `android-app` 文件夹
|
||||||
|
4. 点击 **OK**
|
||||||
|
|
||||||
|
### 步骤 2:等待 Gradle 同步
|
||||||
|
|
||||||
|
- 首次打开会自动下载依赖
|
||||||
|
- 等待底部状态栏显示 "Gradle sync finished"
|
||||||
|
- 可能需要 5-10 分钟
|
||||||
|
|
||||||
|
### 步骤 3:启动 Android 模拟器
|
||||||
|
|
||||||
|
#### 方法 A:使用现有模拟器
|
||||||
|
1. 点击顶部工具栏的设备下拉菜单
|
||||||
|
2. 选择一个模拟器(如 Pixel 5 API 34)
|
||||||
|
3. 点击绿色的 ▶️ **Run** 按钮
|
||||||
|
|
||||||
|
#### 方法 B:创建新模拟器
|
||||||
|
1. 点击 **Tools** → **Device Manager**
|
||||||
|
2. 点击 **Create Device**
|
||||||
|
3. 选择 **Phone** → **Pixel 5**
|
||||||
|
4. 点击 **Next**
|
||||||
|
5. 选择系统镜像(推荐 **API 34**)
|
||||||
|
6. 点击 **Next** → **Finish**
|
||||||
|
7. 点击 ▶️ 启动模拟器
|
||||||
|
|
||||||
|
### 步骤 4:运行应用
|
||||||
|
|
||||||
|
1. 等待模拟器完全启动(显示主屏幕)
|
||||||
|
2. 在 Android Studio 中点击 ▶️ **Run 'app'**
|
||||||
|
3. 应用会自动安装并启动
|
||||||
|
|
||||||
|
### 步骤 5:创建直播间
|
||||||
|
|
||||||
|
在 Android 应用中:
|
||||||
|
|
||||||
|
1. 点击右上角的 **"开始直播"** 按钮
|
||||||
|
2. 填写信息:
|
||||||
|
- **直播间标题**:例如 "我的第一次直播"
|
||||||
|
- **主播名称**:例如 "小明"
|
||||||
|
3. 点击 **"创建"**
|
||||||
|
|
||||||
|
### 步骤 6:获取推流地址
|
||||||
|
|
||||||
|
创建成功后会显示一个对话框,包含:
|
||||||
|
|
||||||
|
```
|
||||||
|
推流地址:
|
||||||
|
rtmp://10.0.2.2:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
|
||||||
|
电脑本机 OBS 可用:
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
|
||||||
|
⚠️ 注意:
|
||||||
|
1. Android 应用只能观看直播
|
||||||
|
2. 需要用 OBS 在电脑上推流
|
||||||
|
3. OBS 设置:服务器填上面地址即可
|
||||||
|
4. 推流后在应用中观看
|
||||||
|
```
|
||||||
|
|
||||||
|
**📋 复制推流地址**:
|
||||||
|
- 点击 **"复制地址"** 按钮
|
||||||
|
- 这个地址稍后在 OBS 中使用
|
||||||
|
|
||||||
|
### 步骤 7:查看直播间列表
|
||||||
|
|
||||||
|
- 点击 **"知道了"** 关闭对话框
|
||||||
|
- 你会看到刚创建的直播间
|
||||||
|
- 状态显示 **"未开播"**(灰色)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OBS Studio 推流教程
|
||||||
|
|
||||||
|
### 步骤 1:下载并安装 OBS
|
||||||
|
|
||||||
|
1. 访问 https://obsproject.com/download
|
||||||
|
2. 下载 Windows 版本
|
||||||
|
3. 安装(使用默认设置)
|
||||||
|
4. 启动 OBS Studio
|
||||||
|
|
||||||
|
### 步骤 2:首次配置向导(如果出现)
|
||||||
|
|
||||||
|
如果是第一次打开 OBS,会出现配置向导:
|
||||||
|
|
||||||
|
1. **使用信息**:选择 "优化推流,录制次之"
|
||||||
|
2. **视频设置**:
|
||||||
|
- 基础分辨率:1920x1080
|
||||||
|
- FPS:30
|
||||||
|
3. **推流信息**:选择 "我将使用自定义流媒体服务器"
|
||||||
|
4. 点击 **"下一步"** 完成
|
||||||
|
|
||||||
|
### 步骤 3:配置推流设置
|
||||||
|
|
||||||
|
#### 3.1 打开设置
|
||||||
|
- 点击右下角 **"设置"** 按钮
|
||||||
|
- 或者菜单栏:**文件** → **设置**
|
||||||
|
|
||||||
|
#### 3.2 配置推流
|
||||||
|
1. 左侧选择 **"推流"**
|
||||||
|
2. **服务**:选择 "自定义..."
|
||||||
|
3. **服务器**:粘贴从 Android 应用复制的地址
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
|
||||||
|
**⚠️ 重要**:
|
||||||
|
- 如果地址是 `rtmp://10.0.2.2:1935/...`,改成 `rtmp://localhost:1935/...`
|
||||||
|
- 完整地址包含 streamKey,不需要单独填写串流密钥
|
||||||
|
|
||||||
|
4. **串流密钥**:留空(因为已经包含在服务器地址中)
|
||||||
|
5. 点击 **"应用"**
|
||||||
|
|
||||||
|
#### 3.3 配置输出(可选,优化画质)
|
||||||
|
1. 左侧选择 **"输出"**
|
||||||
|
2. **输出模式**:简单
|
||||||
|
3. **视频比特率**:2500 Kbps
|
||||||
|
4. **编码器**:x264
|
||||||
|
5. 点击 **"应用"**
|
||||||
|
|
||||||
|
#### 3.4 配置视频(可选)
|
||||||
|
1. 左侧选择 **"视频"**
|
||||||
|
2. **基础分辨率**:1920x1080
|
||||||
|
3. **输出分辨率**:1280x720
|
||||||
|
4. **常用 FPS 值**:30
|
||||||
|
5. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 步骤 4:添加视频源
|
||||||
|
|
||||||
|
#### 方法 A:添加摄像头
|
||||||
|
1. 在 **"来源"** 区域点击 **+** 号
|
||||||
|
2. 选择 **"视频捕获设备"**
|
||||||
|
3. 命名:例如 "摄像头"
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
5. **设备**:选择你的摄像头
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
#### 方法 B:添加屏幕捕获
|
||||||
|
1. 在 **"来源"** 区域点击 **+** 号
|
||||||
|
2. 选择 **"显示器捕获"**
|
||||||
|
3. 命名:例如 "屏幕"
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
5. **显示器**:选择要捕获的屏幕
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
#### 方法 C:添加窗口捕获
|
||||||
|
1. 在 **"来源"** 区域点击 **+** 号
|
||||||
|
2. 选择 **"窗口捕获"**
|
||||||
|
3. 命名:例如 "浏览器"
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
5. **窗口**:选择要捕获的窗口
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 步骤 5:调整画面
|
||||||
|
|
||||||
|
- 拖动红色边框调整大小
|
||||||
|
- 拖动中心调整位置
|
||||||
|
- 右键源 → **变换** → **适应屏幕** 可以自动适配
|
||||||
|
|
||||||
|
### 步骤 6:开始推流
|
||||||
|
|
||||||
|
1. 点击右下角 **"开始推流"** 按钮
|
||||||
|
2. 按钮变成 **"停止推流"**,底部状态栏显示:
|
||||||
|
```
|
||||||
|
直播中 | 00:00:15 | 2500 kb/s | 0 丢帧
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ 推流成功标志**:
|
||||||
|
- 状态栏显示 "直播中"
|
||||||
|
- 有比特率数据(如 2500 kb/s)
|
||||||
|
- 丢帧数为 0 或很少
|
||||||
|
|
||||||
|
**❌ 推流失败标志**:
|
||||||
|
- 提示 "连接失败"
|
||||||
|
- 状态栏显示红色错误
|
||||||
|
- 检查推流地址是否正确
|
||||||
|
|
||||||
|
### 步骤 7:在 Android 应用中观看
|
||||||
|
|
||||||
|
1. 回到 Android 模拟器
|
||||||
|
2. 在直播间列表中,你的直播间状态应该变成 **"直播中"**(红色)
|
||||||
|
3. 点击直播间进入
|
||||||
|
4. 等待 2-3 秒,视频开始播放
|
||||||
|
5. 你应该能看到 OBS 中的画面
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整操作流程
|
||||||
|
|
||||||
|
### 🎬 完整演示流程
|
||||||
|
|
||||||
|
#### 第一步:启动所有服务(5 分钟)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 启动 Docker(如果未运行)
|
||||||
|
cd live-streaming
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 2. 启动后端服务器
|
||||||
|
node server/index.js
|
||||||
|
# 保持窗口打开!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第二步:启动 Android 应用(3 分钟)
|
||||||
|
|
||||||
|
1. 打开 Android Studio
|
||||||
|
2. 打开 `android-app` 项目
|
||||||
|
3. 启动模拟器
|
||||||
|
4. 运行应用(点击 ▶️)
|
||||||
|
|
||||||
|
#### 第三步:创建直播间(1 分钟)
|
||||||
|
|
||||||
|
1. 在 Android 应用中点击 **"开始直播"**
|
||||||
|
2. 填写标题和主播名称
|
||||||
|
3. 点击 **"创建"**
|
||||||
|
4. 点击 **"复制地址"** 复制推流地址
|
||||||
|
5. 点击 **"知道了"**
|
||||||
|
|
||||||
|
#### 第四步:配置 OBS(2 分钟)
|
||||||
|
|
||||||
|
1. 打开 OBS Studio
|
||||||
|
2. 点击 **"设置"** → **"推流"**
|
||||||
|
3. 服务:选择 "自定义..."
|
||||||
|
4. 服务器:粘贴推流地址(改 `10.0.2.2` 为 `localhost`)
|
||||||
|
5. 点击 **"确定"**
|
||||||
|
|
||||||
|
#### 第五步:添加视频源(2 分钟)
|
||||||
|
|
||||||
|
1. 点击 **"来源"** 区域的 **+** 号
|
||||||
|
2. 选择 **"视频捕获设备"**(摄像头)或 **"显示器捕获"**(屏幕)
|
||||||
|
3. 配置并确定
|
||||||
|
|
||||||
|
#### 第六步:开始直播(1 分钟)
|
||||||
|
|
||||||
|
1. 在 OBS 中点击 **"开始推流"**
|
||||||
|
2. 等待 2-3 秒
|
||||||
|
3. 回到 Android 模拟器
|
||||||
|
4. 点击直播间进入
|
||||||
|
5. 观看直播!🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题解决
|
||||||
|
|
||||||
|
### ❌ 问题 1:Android 应用显示"网络错误"
|
||||||
|
|
||||||
|
**原因**:后端服务器未启动
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
验证:浏览器访问 http://localhost:3001/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 2:OBS 提示"连接失败"
|
||||||
|
|
||||||
|
**原因 A**:推流地址错误
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 检查地址格式:`rtmp://localhost:1935/live/{streamKey}`
|
||||||
|
2. 确保使用 `localhost` 而不是 `10.0.2.2`
|
||||||
|
3. 确保包含完整的 streamKey
|
||||||
|
|
||||||
|
**原因 B**:SRS 服务器未运行
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
# 如果没有 srs-server,运行:
|
||||||
|
cd live-streaming
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 3:Android 应用显示"未开播"
|
||||||
|
|
||||||
|
**原因**:后端无法检测到推流
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 确认 OBS 正在推流(状态栏显示"直播中")
|
||||||
|
2. 检查后端日志是否有错误
|
||||||
|
3. 重启后端服务器
|
||||||
|
4. 在 Android 应用中下拉刷新
|
||||||
|
|
||||||
|
**验证推流状态**:
|
||||||
|
浏览器访问 http://localhost:1985/api/v1/streams/
|
||||||
|
应该看到你的 streamKey
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 4:视频播放黑屏
|
||||||
|
|
||||||
|
**原因 A**:HLS 转码未启用
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
检查 `live-streaming/.env` 中是否有 FFmpeg 配置
|
||||||
|
```env
|
||||||
|
FFMPEG_PATH=C:\path\to\ffmpeg.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因 B**:网络延迟
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
- 等待 5-10 秒
|
||||||
|
- 退出直播间重新进入
|
||||||
|
- 检查网络连接
|
||||||
|
|
||||||
|
**原因 C**:Android 播放器不支持格式
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
查看后端日志,确认 HLS 地址可用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 5:模拟器无法连接到后端
|
||||||
|
|
||||||
|
**原因**:防火墙阻止
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
```powershell
|
||||||
|
# 以管理员身份运行 PowerShell
|
||||||
|
netsh advfirewall firewall add rule name="Node.js API" dir=in action=allow protocol=TCP localport=3001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 6:重启后端后房间消失
|
||||||
|
|
||||||
|
**原因**:已修复!现在房间会自动保存
|
||||||
|
|
||||||
|
**验证**:
|
||||||
|
检查 `live-streaming/data/rooms.json` 文件是否存在
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ❌ 问题 7:OBS 画面卡顿
|
||||||
|
|
||||||
|
**原因**:比特率过高或电脑性能不足
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. OBS 设置 → 输出
|
||||||
|
2. 降低视频比特率:2500 → 1500 Kbps
|
||||||
|
3. 降低分辨率:1080p → 720p
|
||||||
|
4. 降低帧率:60fps → 30fps
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速参考
|
||||||
|
|
||||||
|
### 端口说明
|
||||||
|
| 端口 | 服务 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1935 | RTMP | OBS 推流 |
|
||||||
|
| 8080 | HTTP-FLV/HLS | 视频播放 |
|
||||||
|
| 1985 | SRS API | 推流状态检测 |
|
||||||
|
| 3001 | Node.js API | Android 应用接口 |
|
||||||
|
|
||||||
|
### 关键地址
|
||||||
|
| 用途 | 地址 |
|
||||||
|
|------|------|
|
||||||
|
| OBS 推流 | `rtmp://localhost:1935/live/{streamKey}` |
|
||||||
|
| Android API | `http://10.0.2.2:3001/api/` |
|
||||||
|
| 健康检查 | `http://localhost:3001/health` |
|
||||||
|
| SRS 状态 | `http://localhost:1985/api/v1/streams/` |
|
||||||
|
|
||||||
|
### 常用命令
|
||||||
|
```bash
|
||||||
|
# 启动后端
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
|
||||||
|
# 启动 Docker
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 查看 Docker 容器
|
||||||
|
docker ps
|
||||||
|
|
||||||
|
# 停止 Docker
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# 查看端口占用
|
||||||
|
netstat -ano | findstr :3001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 视频教程(建议录制)
|
||||||
|
|
||||||
|
如果你想录制视频教程,可以按照以下脚本:
|
||||||
|
|
||||||
|
### 📹 脚本大纲
|
||||||
|
|
||||||
|
1. **开场**(30秒)
|
||||||
|
- "大家好,今天教大家如何搭建一个 Android 直播系统"
|
||||||
|
- 展示最终效果:OBS 推流 → Android 观看
|
||||||
|
|
||||||
|
2. **环境准备**(2分钟)
|
||||||
|
- 展示必需软件
|
||||||
|
- 检查 Docker 运行状态
|
||||||
|
|
||||||
|
3. **启动后端**(1分钟)
|
||||||
|
- 打开命令行
|
||||||
|
- 运行 `node server/index.js`
|
||||||
|
- 展示成功日志
|
||||||
|
|
||||||
|
4. **Android Studio**(3分钟)
|
||||||
|
- 打开项目
|
||||||
|
- 启动模拟器
|
||||||
|
- 运行应用
|
||||||
|
- 创建直播间
|
||||||
|
- 复制推流地址
|
||||||
|
|
||||||
|
5. **OBS 配置**(3分钟)
|
||||||
|
- 打开设置
|
||||||
|
- 配置推流地址
|
||||||
|
- 添加视频源
|
||||||
|
- 开始推流
|
||||||
|
|
||||||
|
6. **观看直播**(1分钟)
|
||||||
|
- 回到 Android 应用
|
||||||
|
- 点击直播间
|
||||||
|
- 展示播放效果
|
||||||
|
|
||||||
|
7. **结尾**(30秒)
|
||||||
|
- 总结关键步骤
|
||||||
|
- 提醒常见问题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步学习
|
||||||
|
|
||||||
|
### 🚀 进阶功能
|
||||||
|
- [ ] 添加聊天功能
|
||||||
|
- [ ] 添加礼物打赏
|
||||||
|
- [ ] 添加观众人数统计
|
||||||
|
- [ ] 支持多个直播间
|
||||||
|
- [ ] 添加直播回放
|
||||||
|
|
||||||
|
### 📚 推荐资源
|
||||||
|
- OBS 官方文档:https://obsproject.com/wiki/
|
||||||
|
- SRS 文档:https://ossrs.net/
|
||||||
|
- Android ExoPlayer:https://exoplayer.dev/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术支持
|
||||||
|
|
||||||
|
如果遇到问题:
|
||||||
|
1. 查看本教程的"常见问题解决"部分
|
||||||
|
2. 查看 `android-app/问题修复总结.md`
|
||||||
|
3. 查看后端日志输出
|
||||||
|
4. 查看 Android Studio 的 Logcat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**祝你使用愉快!🎉**
|
||||||
336
如何获取RTMP地址.md
Normal file
336
如何获取RTMP地址.md
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# 如何获取 RTMP 推流地址
|
||||||
|
|
||||||
|
## 方法 1:通过 Android 应用获取(最简单)✅
|
||||||
|
|
||||||
|
### 步骤 1:启动后端服务器
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
**重要**:保持这个窗口打开!
|
||||||
|
|
||||||
|
### 步骤 2:在 Android 应用中创建直播间
|
||||||
|
|
||||||
|
1. 打开 Android 应用(在模拟器中)
|
||||||
|
2. 点击右上角的 **"开始直播"** 按钮
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. 填写直播间信息:
|
||||||
|
```
|
||||||
|
直播间标题:我的直播间
|
||||||
|
主播名称:小明
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 点击 **"创建"** 按钮
|
||||||
|
|
||||||
|
### 步骤 3:获取 RTMP 地址
|
||||||
|
|
||||||
|
创建成功后会弹出对话框,显示:
|
||||||
|
|
||||||
|
```
|
||||||
|
推流地址:
|
||||||
|
rtmp://10.0.2.2:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
|
||||||
|
电脑本机 OBS 可用:
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 4:复制地址
|
||||||
|
|
||||||
|
- 点击 **"复制地址"** 按钮
|
||||||
|
- 地址已复制到剪贴板
|
||||||
|
- 可以直接粘贴到 OBS 中使用
|
||||||
|
|
||||||
|
### ⚠️ 重要提示
|
||||||
|
|
||||||
|
**在 OBS 中使用时**:
|
||||||
|
- ✅ 使用:`rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2`
|
||||||
|
- ❌ 不要用:`rtmp://10.0.2.2:1935/live/...`(这是给 Android 模拟器用的)
|
||||||
|
|
||||||
|
**地址说明**:
|
||||||
|
- `rtmp://` - 协议
|
||||||
|
- `localhost` - 本机地址
|
||||||
|
- `1935` - RTMP 端口
|
||||||
|
- `/live/` - 应用名称
|
||||||
|
- `868c49cc-1021-4664-95a3-ed71e789adb2` - 你的 streamKey(每个直播间不同)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法 2:查看已创建的直播间
|
||||||
|
|
||||||
|
如果你已经创建过直播间,想再次查看 RTMP 地址:
|
||||||
|
|
||||||
|
### 在 Android 应用中查看
|
||||||
|
|
||||||
|
1. 打开应用,在首页看到直播间列表
|
||||||
|
2. 点击任意直播间进入详情页
|
||||||
|
3. 向下滚动,找到 **"推流信息"** 区域
|
||||||
|
4. 可以看到:
|
||||||
|
```
|
||||||
|
推流地址
|
||||||
|
rtmp://10.0.2.2:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
|
||||||
|
电脑本机 OBS 可用(等价地址)
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
|
||||||
|
推流密钥
|
||||||
|
868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
5. 点击 **"复制地址"** 按钮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法 3:通过 API 获取(技术方式)
|
||||||
|
|
||||||
|
如果你想通过代码或命令行获取:
|
||||||
|
|
||||||
|
### 查看所有直播间
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/rooms
|
||||||
|
```
|
||||||
|
|
||||||
|
返回示例:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": "868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"title": "我的直播间",
|
||||||
|
"streamerName": "小明",
|
||||||
|
"streamKey": "868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"isLive": false,
|
||||||
|
"streamUrls": {
|
||||||
|
"rtmp": "rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"flv": "http://localhost:8080/live/868c49cc-1021-4664-95a3-ed71e789adb2.flv",
|
||||||
|
"hls": "http://localhost:8080/live/868c49cc-1021-4664-95a3-ed71e789adb2.m3u8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 查看特定直播间
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/rooms/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法 4:查看持久化文件
|
||||||
|
|
||||||
|
直播间数据保存在文件中,可以直接查看:
|
||||||
|
|
||||||
|
### 文件位置
|
||||||
|
```
|
||||||
|
live-streaming/data/rooms.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### 打开文件
|
||||||
|
用记事本或 VS Code 打开,内容示例:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"title": "我的直播间",
|
||||||
|
"streamerName": "小明",
|
||||||
|
"streamKey": "868c49cc-1021-4664-95a3-ed71e789adb2",
|
||||||
|
"isLive": false,
|
||||||
|
"viewerCount": 0,
|
||||||
|
"createdAt": "2025-12-16T09:20:00.000Z",
|
||||||
|
"startedAt": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 构建 RTMP 地址
|
||||||
|
根据 `streamKey` 构建:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/{streamKey}
|
||||||
|
```
|
||||||
|
|
||||||
|
例如:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 在 OBS 中使用 RTMP 地址
|
||||||
|
|
||||||
|
### 方式 A:完整地址(推荐)
|
||||||
|
|
||||||
|
1. 打开 OBS Studio
|
||||||
|
2. 点击 **"设置"** → **"推流"**
|
||||||
|
3. **服务**:选择 "自定义..."
|
||||||
|
4. **服务器**:粘贴完整地址
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
5. **串流密钥**:留空
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 方式 B:分开填写
|
||||||
|
|
||||||
|
1. 打开 OBS Studio
|
||||||
|
2. 点击 **"设置"** → **"推流"**
|
||||||
|
3. **服务**:选择 "自定义..."
|
||||||
|
4. **服务器**:
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live
|
||||||
|
```
|
||||||
|
5. **串流密钥**:
|
||||||
|
```
|
||||||
|
868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
```
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
**两种方式效果相同**,选择你喜欢的即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速参考
|
||||||
|
|
||||||
|
### RTMP 地址格式
|
||||||
|
```
|
||||||
|
rtmp://[主机]:[端口]/[应用]/[streamKey]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/868c49cc-1021-4664-95a3-ed71e789adb2
|
||||||
|
│ │ │ │ │
|
||||||
|
│ │ │ │ └─ streamKey(每个直播间唯一)
|
||||||
|
│ │ │ └────── 应用名称(固定为 live)
|
||||||
|
│ │ └──────────── RTMP 端口(固定为 1935)
|
||||||
|
│ └────────────────────── 主机地址(OBS 用 localhost)
|
||||||
|
└───────────────────────────── 协议(固定为 rtmp)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 地址对照表
|
||||||
|
|
||||||
|
| 使用场景 | 地址 |
|
||||||
|
|---------|------|
|
||||||
|
| OBS 推流 | `rtmp://localhost:1935/live/{streamKey}` |
|
||||||
|
| Android 模拟器 | `rtmp://10.0.2.2:1935/live/{streamKey}` |
|
||||||
|
| 局域网其他设备 | `rtmp://192.168.x.x:1935/live/{streamKey}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### ❓ 为什么有两个不同的地址?
|
||||||
|
|
||||||
|
**答**:
|
||||||
|
- `rtmp://localhost:1935/...` - 给电脑上的 OBS 用
|
||||||
|
- `rtmp://10.0.2.2:1935/...` - 给 Android 模拟器用
|
||||||
|
|
||||||
|
在 Android 模拟器中,`10.0.2.2` 代表宿主机(你的电脑)的 `localhost`。
|
||||||
|
|
||||||
|
### ❓ streamKey 是什么?
|
||||||
|
|
||||||
|
**答**:streamKey 是每个直播间的唯一标识符,就像房间号。每创建一个直播间,系统会自动生成一个新的 streamKey。
|
||||||
|
|
||||||
|
### ❓ 可以自定义 streamKey 吗?
|
||||||
|
|
||||||
|
**答**:当前版本不支持自定义,系统会自动生成 UUID 作为 streamKey。如果需要自定义,需要修改后端代码。
|
||||||
|
|
||||||
|
### ❓ 忘记了 RTMP 地址怎么办?
|
||||||
|
|
||||||
|
**答**:
|
||||||
|
1. 在 Android 应用中点击直播间查看
|
||||||
|
2. 或者查看 `live-streaming/data/rooms.json` 文件
|
||||||
|
3. 或者通过 API 查询:`curl http://localhost:3001/api/rooms`
|
||||||
|
|
||||||
|
### ❓ 可以同时创建多个直播间吗?
|
||||||
|
|
||||||
|
**答**:可以!每个直播间有独立的 streamKey,可以同时推流到不同的直播间。
|
||||||
|
|
||||||
|
### ❓ RTMP 地址会变吗?
|
||||||
|
|
||||||
|
**答**:不会!只要不删除直播间,RTMP 地址(streamKey)就不会改变。即使重启服务器,地址也会保持不变(因为已经持久化保存)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 图文演示
|
||||||
|
|
||||||
|
### 📱 Android 应用界面
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 直播系统 [开始直播] │ ← 点击这里
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ 我的直播间 │ │
|
||||||
|
│ │ 主播: 小明 │ │
|
||||||
|
│ │ [直播中] │ │ ← 点击进入查看地址
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 创建直播间对话框
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 创建直播间 │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 直播间标题 │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ 我的直播间 │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 主播名称 │
|
||||||
|
│ ┌─────────────────────┐ │
|
||||||
|
│ │ 小明 │ │
|
||||||
|
│ └─────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [取消] [创建] │ ← 点击创建
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📺 推流信息对话框
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 已创建直播间 │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 推流地址: │
|
||||||
|
│ rtmp://10.0.2.2:1935/... │
|
||||||
|
│ │
|
||||||
|
│ 电脑本机 OBS 可用: │
|
||||||
|
│ rtmp://localhost:1935/... │ ← 这个地址用于 OBS
|
||||||
|
│ │
|
||||||
|
│ ⚠️ 注意: │
|
||||||
|
│ 1. Android 应用只能观看 │
|
||||||
|
│ 2. 需要用 OBS 在电脑上推流 │
|
||||||
|
│ 3. OBS 设置:服务器填上面 │
|
||||||
|
│ 4. 推流后在应用中观看 │
|
||||||
|
│ │
|
||||||
|
│ [复制地址] [知道了] │ ← 点击复制
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
### 最简单的方法 🎯
|
||||||
|
|
||||||
|
1. **打开 Android 应用**
|
||||||
|
2. **点击"开始直播"**
|
||||||
|
3. **填写信息并创建**
|
||||||
|
4. **点击"复制地址"**
|
||||||
|
5. **粘贴到 OBS 中**(记得把 `10.0.2.2` 改成 `localhost`)
|
||||||
|
|
||||||
|
就这么简单!🎉
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**需要帮助?**
|
||||||
|
- 查看 `使用教程.md` 获取完整教程
|
||||||
|
- 查看 `android-app/问题修复总结.md` 解决常见问题
|
||||||
438
推流问题完整诊断.md
Normal file
438
推流问题完整诊断.md
Normal file
|
|
@ -0,0 +1,438 @@
|
||||||
|
# 推流问题完整诊断与解决
|
||||||
|
|
||||||
|
## 🔍 当前状态
|
||||||
|
|
||||||
|
- ✅ 后端服务器运行正常(端口 3001)
|
||||||
|
- ✅ SRS 服务器运行正常(端口 1935)
|
||||||
|
- ✅ 直播间已创建(streamKey: 7f4acb94-f91c-4ec0-84eb-fbb1855f8f18)
|
||||||
|
- ❌ **SRS 没有收到推流**(streams 数组为空)
|
||||||
|
- ❌ **Android 应用显示"未开播"**
|
||||||
|
|
||||||
|
## 🎯 问题原因
|
||||||
|
|
||||||
|
**OBS 的推流没有成功到达 SRS 服务器!**
|
||||||
|
|
||||||
|
可能的原因:
|
||||||
|
1. OBS 推流地址配置错误
|
||||||
|
2. OBS 推流失败但没有提示
|
||||||
|
3. OBS 没有真正开始推流
|
||||||
|
4. 防火墙阻止了连接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 完整诊断步骤
|
||||||
|
|
||||||
|
### 步骤 1:检查 OBS 推流状态
|
||||||
|
|
||||||
|
#### 1.1 查看 OBS 界面
|
||||||
|
在 OBS 右下角查看:
|
||||||
|
|
||||||
|
**如果推流成功**:
|
||||||
|
```
|
||||||
|
直播中 | 00:01:23 | 2500 kb/s | 0 丢帧
|
||||||
|
```
|
||||||
|
|
||||||
|
**如果推流失败**:
|
||||||
|
```
|
||||||
|
未推流
|
||||||
|
```
|
||||||
|
或显示红色错误提示
|
||||||
|
|
||||||
|
#### 1.2 查看 OBS 日志
|
||||||
|
1. 点击 OBS 菜单栏:**帮助** → **日志文件** → **查看当前日志**
|
||||||
|
2. 搜索 "error" 或 "failed"
|
||||||
|
3. 查看是否有连接错误
|
||||||
|
|
||||||
|
### 步骤 2:验证 OBS 推流设置
|
||||||
|
|
||||||
|
#### 2.1 打开推流设置
|
||||||
|
1. 点击 **"设置"** → **"推流"**
|
||||||
|
2. 检查配置
|
||||||
|
|
||||||
|
#### 2.2 正确的配置应该是
|
||||||
|
|
||||||
|
**方式 A:完整地址**
|
||||||
|
```
|
||||||
|
服务: 自定义...
|
||||||
|
服务器: rtmp://localhost:1935/live/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
串流密钥: (留空)
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式 B:分开填写**
|
||||||
|
```
|
||||||
|
服务: 自定义...
|
||||||
|
服务器: rtmp://localhost:1935/live
|
||||||
|
串流密钥: 7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 常见错误
|
||||||
|
|
||||||
|
❌ **错误 1**:使用了 `10.0.2.2`
|
||||||
|
```
|
||||||
|
服务器: rtmp://10.0.2.2:1935/live/... ← 错误!
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **错误 2**:streamKey 不完整
|
||||||
|
```
|
||||||
|
串流密钥: 7f4acb94-f91c-4ec0 ← 不完整!
|
||||||
|
```
|
||||||
|
|
||||||
|
❌ **错误 3**:多了空格或换行
|
||||||
|
```
|
||||||
|
服务器: rtmp://localhost:1935/live/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
↑ 多了空格
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:重新配置 OBS
|
||||||
|
|
||||||
|
#### 3.1 停止当前推流
|
||||||
|
1. 如果 OBS 显示"直播中",点击 **"停止推流"**
|
||||||
|
2. 等待完全停止
|
||||||
|
|
||||||
|
#### 3.2 清空并重新配置
|
||||||
|
1. 点击 **"设置"** → **"推流"**
|
||||||
|
2. **服务器** 框中,删除所有内容
|
||||||
|
3. 重新输入(不要复制粘贴,手动输入):
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
```
|
||||||
|
4. **串流密钥**:确保为空
|
||||||
|
5. 点击 **"应用"**
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
#### 3.3 测试连接(可选)
|
||||||
|
有些 OBS 版本有"测试连接"按钮,如果有就点击测试。
|
||||||
|
|
||||||
|
### 步骤 4:开始推流并验证
|
||||||
|
|
||||||
|
#### 4.1 确保有视频源
|
||||||
|
1. 在 OBS 的"来源"区域,确保至少有一个视频源
|
||||||
|
2. 如果没有,添加一个:
|
||||||
|
- 点击 + 号
|
||||||
|
- 选择"视频捕获设备"(摄像头)或"显示器捕获"(屏幕)
|
||||||
|
|
||||||
|
#### 4.2 开始推流
|
||||||
|
1. 点击 **"开始推流"** 按钮
|
||||||
|
2. 观察状态栏变化
|
||||||
|
|
||||||
|
#### 4.3 立即验证(推流后 5 秒内)
|
||||||
|
|
||||||
|
**方法 1:浏览器检查 SRS**
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:1985/api/v1/streams/
|
||||||
|
```
|
||||||
|
|
||||||
|
**成功时应该看到**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"streams": [
|
||||||
|
{
|
||||||
|
"id": "vid-xxxxx",
|
||||||
|
"name": "7f4acb94-f91c-4ec0-84eb-fbb1855f8f18",
|
||||||
|
"app": "live",
|
||||||
|
"publish": {
|
||||||
|
"active": true,
|
||||||
|
"cid": "xxxxx"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败时会看到**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"streams": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**方法 2:检查后端 API**
|
||||||
|
打开浏览器访问:
|
||||||
|
```
|
||||||
|
http://localhost:3001/api/rooms/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
```
|
||||||
|
|
||||||
|
查看 `isLive` 字段:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"isLive": true ← 应该是 true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**方法 3:Android 应用**
|
||||||
|
1. 在 Android 模拟器中下拉刷新
|
||||||
|
2. 查看直播间状态是否变成"直播中"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 如果还是失败
|
||||||
|
|
||||||
|
### 诊断 A:检查防火墙
|
||||||
|
|
||||||
|
Windows 防火墙可能阻止了 OBS 连接到 SRS。
|
||||||
|
|
||||||
|
#### 解决方法:
|
||||||
|
以管理员身份运行 PowerShell,执行:
|
||||||
|
```powershell
|
||||||
|
netsh advfirewall firewall add rule name="RTMP Server" dir=in action=allow protocol=TCP localport=1935
|
||||||
|
netsh advfirewall firewall add rule name="RTMP Server Out" dir=out action=allow protocol=TCP localport=1935
|
||||||
|
```
|
||||||
|
|
||||||
|
### 诊断 B:检查 SRS 日志
|
||||||
|
|
||||||
|
#### 查看 Docker 日志:
|
||||||
|
```bash
|
||||||
|
docker logs srs-server --tail 50
|
||||||
|
```
|
||||||
|
|
||||||
|
查找是否有连接尝试或错误信息。
|
||||||
|
|
||||||
|
### 诊断 C:使用简化的测试地址
|
||||||
|
|
||||||
|
#### 测试 1:使用最简单的配置
|
||||||
|
在 OBS 中:
|
||||||
|
```
|
||||||
|
服务器: rtmp://localhost:1935/live
|
||||||
|
串流密钥: test123
|
||||||
|
```
|
||||||
|
|
||||||
|
然后开始推流,访问:
|
||||||
|
```
|
||||||
|
http://localhost:1985/api/v1/streams/
|
||||||
|
```
|
||||||
|
|
||||||
|
如果能看到 `"name": "test123"`,说明 OBS 和 SRS 连接正常,问题在于 streamKey。
|
||||||
|
|
||||||
|
### 诊断 D:检查 OBS 版本
|
||||||
|
|
||||||
|
#### 确认 OBS 版本
|
||||||
|
1. 点击 **帮助** → **关于**
|
||||||
|
2. 确认版本号(推荐 28.0 或更高)
|
||||||
|
|
||||||
|
#### 如果版本太旧
|
||||||
|
下载最新版本:https://obsproject.com/download
|
||||||
|
|
||||||
|
### 诊断 E:重启所有服务
|
||||||
|
|
||||||
|
#### 完全重启流程:
|
||||||
|
```bash
|
||||||
|
# 1. 停止 OBS 推流
|
||||||
|
# 2. 关闭 OBS
|
||||||
|
|
||||||
|
# 3. 重启 SRS Docker
|
||||||
|
docker restart srs-server
|
||||||
|
|
||||||
|
# 4. 重启后端服务器
|
||||||
|
# 在命令行按 Ctrl+C 停止,然后重新运行:
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
|
||||||
|
# 5. 等待 5 秒
|
||||||
|
|
||||||
|
# 6. 重新打开 OBS
|
||||||
|
|
||||||
|
# 7. 重新配置推流地址
|
||||||
|
|
||||||
|
# 8. 开始推流
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 完整的正确操作流程(从头开始)
|
||||||
|
|
||||||
|
### 1. 确保所有服务运行
|
||||||
|
|
||||||
|
#### 检查 Docker
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
```
|
||||||
|
应该看到 `srs-server` 在运行。
|
||||||
|
|
||||||
|
#### 启动后端
|
||||||
|
```bash
|
||||||
|
cd live-streaming
|
||||||
|
node server/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 在 Android 应用中创建直播间
|
||||||
|
|
||||||
|
1. 打开应用
|
||||||
|
2. 点击"开始直播"
|
||||||
|
3. 填写:
|
||||||
|
- 标题:测试直播
|
||||||
|
- 主播:小明
|
||||||
|
4. 点击"创建"
|
||||||
|
5. **记下完整的 streamKey**(例如:7f4acb94-f91c-4ec0-84eb-fbb1855f8f18)
|
||||||
|
|
||||||
|
### 3. 配置 OBS(重要!)
|
||||||
|
|
||||||
|
1. 打开 OBS Studio
|
||||||
|
2. 点击 **"设置"**
|
||||||
|
3. 左侧选择 **"推流"**
|
||||||
|
4. **服务**:下拉选择 **"自定义..."**
|
||||||
|
5. **服务器**:输入(注意不要有空格)
|
||||||
|
```
|
||||||
|
rtmp://localhost:1935/live/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
```
|
||||||
|
6. **串流密钥**:留空(不要填任何东西)
|
||||||
|
7. 点击 **"应用"**
|
||||||
|
8. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 4. 添加视频源(如果还没有)
|
||||||
|
|
||||||
|
1. 在"来源"区域点击 **+** 号
|
||||||
|
2. 选择 **"显示器捕获"**
|
||||||
|
3. 命名:屏幕
|
||||||
|
4. 点击 **"确定"**
|
||||||
|
5. 选择你的显示器
|
||||||
|
6. 点击 **"确定"**
|
||||||
|
|
||||||
|
### 5. 开始推流
|
||||||
|
|
||||||
|
1. 点击 **"开始推流"**
|
||||||
|
2. 观察右下角状态栏
|
||||||
|
|
||||||
|
**成功标志**:
|
||||||
|
```
|
||||||
|
直播中 | 00:00:05 | 2500 kb/s | 0 丢帧
|
||||||
|
```
|
||||||
|
|
||||||
|
**失败标志**:
|
||||||
|
- 提示"连接失败"
|
||||||
|
- 状态栏显示红色
|
||||||
|
- 没有比特率数据
|
||||||
|
|
||||||
|
### 6. 验证推流(推流后立即检查)
|
||||||
|
|
||||||
|
#### 方法 1:浏览器
|
||||||
|
访问:http://localhost:1985/api/v1/streams/
|
||||||
|
|
||||||
|
应该看到你的 streamKey。
|
||||||
|
|
||||||
|
#### 方法 2:Android 应用
|
||||||
|
1. 下拉刷新列表
|
||||||
|
2. 应该显示"直播中"(红色)
|
||||||
|
3. 点击进入观看
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎥 OBS 推流设置截图说明
|
||||||
|
|
||||||
|
### 正确的设置界面
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────┐
|
||||||
|
│ 设置 │
|
||||||
|
├──────────────────────────────────────────┤
|
||||||
|
│ 推流 │
|
||||||
|
│ │
|
||||||
|
│ 服务: [自定义... ▼] │
|
||||||
|
│ │
|
||||||
|
│ 服务器: │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │rtmp://localhost:1935/live/7f4acb94-│ │
|
||||||
|
│ │f91c-4ec0-84eb-fbb1855f8f18 │ │
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ 串流密钥: │
|
||||||
|
│ ┌────────────────────────────────────┐ │
|
||||||
|
│ │ │ │ ← 留空!
|
||||||
|
│ └────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ □ 使用身份验证 │
|
||||||
|
│ │
|
||||||
|
│ [取消] [确定] │
|
||||||
|
└──────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 调试命令速查
|
||||||
|
|
||||||
|
### 检查 SRS 推流状态
|
||||||
|
```bash
|
||||||
|
curl http://localhost:1985/api/v1/streams/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查直播间状态
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3001/api/rooms/7f4acb94-f91c-4ec0-84eb-fbb1855f8f18
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查 Docker 状态
|
||||||
|
```bash
|
||||||
|
docker ps
|
||||||
|
docker logs srs-server --tail 20
|
||||||
|
```
|
||||||
|
|
||||||
|
### 检查端口
|
||||||
|
```bash
|
||||||
|
netstat -ano | findstr :1935
|
||||||
|
netstat -ano | findstr :3001
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重启 SRS
|
||||||
|
```bash
|
||||||
|
docker restart srs-server
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 关键注意事项
|
||||||
|
|
||||||
|
### ✅ 必须做对的事情
|
||||||
|
|
||||||
|
1. **地址必须用 localhost**
|
||||||
|
- ✅ `rtmp://localhost:1935/...`
|
||||||
|
- ❌ `rtmp://10.0.2.2:1935/...`
|
||||||
|
|
||||||
|
2. **streamKey 必须完整**
|
||||||
|
- ✅ `7f4acb94-f91c-4ec0-84eb-fbb1855f8f18`
|
||||||
|
- ❌ `7f4acb94-f91c-4ec0`
|
||||||
|
|
||||||
|
3. **串流密钥框必须为空**
|
||||||
|
- 因为 streamKey 已经包含在服务器地址中了
|
||||||
|
|
||||||
|
4. **推流后立即验证**
|
||||||
|
- 不要等太久,推流后 5 秒内就应该能在 SRS 中看到
|
||||||
|
|
||||||
|
### ❌ 常见错误
|
||||||
|
|
||||||
|
1. 复制地址时带了空格或换行
|
||||||
|
2. 使用了 `10.0.2.2` 而不是 `localhost`
|
||||||
|
3. streamKey 不完整或错误
|
||||||
|
4. 在"串流密钥"框中又填了一遍 streamKey
|
||||||
|
5. 防火墙阻止了连接
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 快速测试方法
|
||||||
|
|
||||||
|
如果你想快速测试 OBS 和 SRS 是否能正常连接:
|
||||||
|
|
||||||
|
### 简化测试
|
||||||
|
1. OBS 设置:
|
||||||
|
```
|
||||||
|
服务器: rtmp://localhost:1935/live
|
||||||
|
串流密钥: test
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 开始推流
|
||||||
|
|
||||||
|
3. 访问:http://localhost:1985/api/v1/streams/
|
||||||
|
|
||||||
|
4. 如果能看到 `"name": "test"`,说明连接正常
|
||||||
|
|
||||||
|
5. 然后再改回正确的 streamKey
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**现在按照上面的步骤重新配置 OBS,特别注意地址格式!** 🚀
|
||||||
|
|
||||||
|
如果还是不行,请告诉我:
|
||||||
|
1. OBS 状态栏显示什么?
|
||||||
|
2. 访问 http://localhost:1985/api/v1/streams/ 看到什么?
|
||||||
|
3. OBS 日志中有什么错误?
|
||||||
Loading…
Reference in New Issue
Block a user