diff --git a/Log/微信图片_20251216175455_1_40.png b/Log/微信图片_20251216175455_1_40.png new file mode 100644 index 00000000..62559037 Binary files /dev/null and b/Log/微信图片_20251216175455_1_40.png differ diff --git a/OBS推流问题诊断.md b/OBS推流问题诊断.md new file mode 100644 index 00000000..8b1452f5 --- /dev/null +++ b/OBS推流问题诊断.md @@ -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 的推流地址吧!** 🚀 diff --git a/android-app/.gradle/8.14/checksums/checksums.lock b/android-app/.gradle/8.14/checksums/checksums.lock new file mode 100644 index 00000000..9ae74ae9 Binary files /dev/null and b/android-app/.gradle/8.14/checksums/checksums.lock differ diff --git a/android-app/.gradle/8.14/checksums/md5-checksums.bin b/android-app/.gradle/8.14/checksums/md5-checksums.bin new file mode 100644 index 00000000..8b127a29 Binary files /dev/null and b/android-app/.gradle/8.14/checksums/md5-checksums.bin differ diff --git a/android-app/.gradle/8.14/checksums/sha1-checksums.bin b/android-app/.gradle/8.14/checksums/sha1-checksums.bin new file mode 100644 index 00000000..81eca621 Binary files /dev/null and b/android-app/.gradle/8.14/checksums/sha1-checksums.bin differ diff --git a/android-app/.gradle/8.14/executionHistory/executionHistory.bin b/android-app/.gradle/8.14/executionHistory/executionHistory.bin new file mode 100644 index 00000000..b64fc610 Binary files /dev/null and b/android-app/.gradle/8.14/executionHistory/executionHistory.bin differ diff --git a/android-app/.gradle/8.14/executionHistory/executionHistory.lock b/android-app/.gradle/8.14/executionHistory/executionHistory.lock new file mode 100644 index 00000000..f4974e8d Binary files /dev/null and b/android-app/.gradle/8.14/executionHistory/executionHistory.lock differ diff --git a/android-app/.gradle/8.14/fileChanges/last-build.bin b/android-app/.gradle/8.14/fileChanges/last-build.bin new file mode 100644 index 00000000..f76dd238 Binary files /dev/null and b/android-app/.gradle/8.14/fileChanges/last-build.bin differ diff --git a/android-app/.gradle/8.14/fileHashes/fileHashes.bin b/android-app/.gradle/8.14/fileHashes/fileHashes.bin new file mode 100644 index 00000000..5805bed4 Binary files /dev/null and b/android-app/.gradle/8.14/fileHashes/fileHashes.bin differ diff --git a/android-app/.gradle/8.14/fileHashes/fileHashes.lock b/android-app/.gradle/8.14/fileHashes/fileHashes.lock new file mode 100644 index 00000000..34c27fb8 Binary files /dev/null and b/android-app/.gradle/8.14/fileHashes/fileHashes.lock differ diff --git a/android-app/.gradle/8.14/fileHashes/resourceHashesCache.bin b/android-app/.gradle/8.14/fileHashes/resourceHashesCache.bin new file mode 100644 index 00000000..8b858a16 Binary files /dev/null and b/android-app/.gradle/8.14/fileHashes/resourceHashesCache.bin differ diff --git a/android-app/.gradle/8.14/gc.properties b/android-app/.gradle/8.14/gc.properties new file mode 100644 index 00000000..e69de29b diff --git a/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock index c2f8b135..7c007c78 100644 Binary files a/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/android-app/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/android-app/.gradle/buildOutputCleanup/cache.properties b/android-app/.gradle/buildOutputCleanup/cache.properties index abf2bf9b..c141e468 100644 --- a/android-app/.gradle/buildOutputCleanup/cache.properties +++ b/android-app/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Mon Dec 15 18:02:26 CST 2025 -gradle.version=8.1 +#Tue Dec 16 16:06:46 CST 2025 +gradle.version=8.14 diff --git a/android-app/.gradle/buildOutputCleanup/outputFiles.bin b/android-app/.gradle/buildOutputCleanup/outputFiles.bin index f219c029..c2e07730 100644 Binary files a/android-app/.gradle/buildOutputCleanup/outputFiles.bin and b/android-app/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/android-app/.gradle/file-system.probe b/android-app/.gradle/file-system.probe index aa2336d6..9893a358 100644 Binary files a/android-app/.gradle/file-system.probe and b/android-app/.gradle/file-system.probe differ diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java index b75f026b..7b937ac9 100644 --- a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java +++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.java @@ -32,7 +32,7 @@ import retrofit2.Response; public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; - private RoomsAdapter adapter; + private WaterfallRoomsAdapter waterfallAdapter; private final Handler handler = new Handler(Looper.getMainLooper()); private Runnable pollRunnable; @@ -43,15 +43,19 @@ public class MainActivity extends AppCompatActivity { binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - adapter = new RoomsAdapter(room -> { + // 使用瀑布流 Adapter + waterfallAdapter = new WaterfallRoomsAdapter(room -> { if (room == null) return; Intent intent = new Intent(MainActivity.this, RoomDetailActivity.class); intent.putExtra(RoomDetailActivity.EXTRA_ROOM_ID, room.getId()); startActivity(intent); }); - binding.roomsRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - binding.roomsRecyclerView.setAdapter(adapter); + // 设置瀑布流布局管理器(2列) + 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()); } @@ -167,15 +171,21 @@ public class MainActivity extends AppCompatActivity { String msg = ""; if (!TextUtils.isEmpty(rtmp)) { - msg += "推流地址(服务器):\n" + rtmp + "\n\n"; + msg += "推流地址:\n" + rtmp + "\n\n"; } 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)) { - 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 copyKey = streamKey; @@ -184,7 +194,7 @@ public class MainActivity extends AppCompatActivity { .setTitle("已创建直播间") .setMessage(msg) .setNegativeButton("复制地址", (d, w) -> copyToClipboard("rtmp", copyRtmp)) - .setPositiveButton("复制密钥", (d, w) -> copyToClipboard("streamKey", copyKey)) + .setPositiveButton("知道了", null) .show(); } @@ -208,13 +218,13 @@ public class MainActivity extends AppCompatActivity { binding.loading.setVisibility(View.GONE); ApiResponse> body = response.body(); List rooms = body != null && body.getData() != null ? body.getData() : Collections.emptyList(); - adapter.submitList(rooms); + waterfallAdapter.submitList(rooms); } @Override public void onFailure(Call>> call, Throwable t) { binding.loading.setVisibility(View.GONE); - adapter.submitList(Collections.emptyList()); + waterfallAdapter.submitList(Collections.emptyList()); } }); } diff --git a/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java new file mode 100644 index 00000000..89a11705 --- /dev/null +++ b/android-app/app/src/main/java/com/example/livestreaming/WaterfallRoomsAdapter.java @@ -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 { + + 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 DIFF = new DiffUtil.ItemCallback() { + @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); + } + }; +} diff --git a/android-app/app/src/main/res/drawable/category_normal.xml b/android-app/app/src/main/res/drawable/category_normal.xml new file mode 100644 index 00000000..aef9bdd8 --- /dev/null +++ b/android-app/app/src/main/res/drawable/category_normal.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/category_selected.xml b/android-app/app/src/main/res/drawable/category_selected.xml new file mode 100644 index 00000000..f68a70ba --- /dev/null +++ b/android-app/app/src/main/res/drawable/category_selected.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/hot_badge_background.xml b/android-app/app/src/main/res/drawable/hot_badge_background.xml new file mode 100644 index 00000000..065312d9 --- /dev/null +++ b/android-app/app/src/main/res/drawable/hot_badge_background.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/live_badge_background.xml b/android-app/app/src/main/res/drawable/live_badge_background.xml new file mode 100644 index 00000000..3e16cba3 --- /dev/null +++ b/android-app/app/src/main/res/drawable/live_badge_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/search_background.xml b/android-app/app/src/main/res/drawable/search_background.xml new file mode 100644 index 00000000..4d708941 --- /dev/null +++ b/android-app/app/src/main/res/drawable/search_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/drawable/viewer_count_background.xml b/android-app/app/src/main/res/drawable/viewer_count_background.xml new file mode 100644 index 00000000..d90e787e --- /dev/null +++ b/android-app/app/src/main/res/drawable/viewer_count_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml index 5f70914e..061f9882 100644 --- a/android-app/app/src/main/res/layout/activity_main.xml +++ b/android-app/app/src/main/res/layout/activity_main.xml @@ -2,40 +2,192 @@ + android:layout_height="match_parent" + android:background="#F5F5F5"> - + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/searchBar"> + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/categoryScroll" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main_new.xml b/android-app/app/src/main/res/layout/activity_main_new.xml new file mode 100644 index 00000000..061f9882 --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main_new.xml @@ -0,0 +1,322 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/activity_main_old.xml b/android-app/app/src/main/res/layout/activity_main_old.xml new file mode 100644 index 00000000..5f70914e --- /dev/null +++ b/android-app/app/src/main/res/layout/activity_main_old.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_room.xml b/android-app/app/src/main/res/layout/item_room.xml index e502de12..f977ed40 100644 --- a/android-app/app/src/main/res/layout/item_room.xml +++ b/android-app/app/src/main/res/layout/item_room.xml @@ -3,49 +3,143 @@ 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"> + android:layout_margin="8dp" + app:cardCornerRadius="12dp" + app:cardElevation="2dp"> + android:layout_height="wrap_content"> - + + - + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/coverImage"> + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_room_old.xml b/android-app/app/src/main/res/layout/item_room_old.xml new file mode 100644 index 00000000..e502de12 --- /dev/null +++ b/android-app/app/src/main/res/layout/item_room_old.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/layout/item_room_waterfall.xml b/android-app/app/src/main/res/layout/item_room_waterfall.xml new file mode 100644 index 00000000..f977ed40 --- /dev/null +++ b/android-app/app/src/main/res/layout/item_room_waterfall.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml index 044672a5..4c4b3402 100644 --- a/android-app/app/src/main/res/values/themes.xml +++ b/android-app/app/src/main/res/values/themes.xml @@ -7,9 +7,23 @@ @color/teal_200 @color/teal_700 @android:color/black - ?attr/colorPrimaryVariant - @android:color/black - false + @android:color/white + @android:color/white + true + + + + diff --git a/android-app/gradle/wrapper/gradle-wrapper.properties b/android-app/gradle/wrapper/gradle-wrapper.properties index 8b6cf8e6..a82cac06 100644 --- a/android-app/gradle/wrapper/gradle-wrapper.properties +++ b/android-app/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=file:///D:/soft/gradle-8.1-bin.zip +distributionUrl=file:///D:/soft/gradle-8.14-bin.zip networkTimeout=600000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/android-app/新UI设计说明.md b/android-app/新UI设计说明.md new file mode 100644 index 00000000..67c1dd54 --- /dev/null +++ b/android-app/新UI设计说明.md @@ -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 效果了!** 🎉 diff --git a/android-app/网络连接修复说明.md b/android-app/网络连接修复说明.md new file mode 100644 index 00000000..e64f031c --- /dev/null +++ b/android-app/网络连接修复说明.md @@ -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` diff --git a/android-app/问题修复总结.md b/android-app/问题修复总结.md new file mode 100644 index 00000000..96b32e0a --- /dev/null +++ b/android-app/问题修复总结.md @@ -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` diff --git a/live-streaming/.env b/live-streaming/.env index a2b6c071..7430304b 100644 --- a/live-streaming/.env +++ b/live-streaming/.env @@ -3,5 +3,6 @@ PORT=3001 SRS_HOST=localhost SRS_RTMP_PORT=1935 SRS_HTTP_PORT=8080 +SRS_API_PORT=1985 CLIENT_URL=http://localhost:3000 EMBEDDED_MEDIA_SERVER=0 \ No newline at end of file diff --git a/live-streaming/data/rooms.json b/live-streaming/data/rooms.json new file mode 100644 index 00000000..8876abec --- /dev/null +++ b/live-streaming/data/rooms.json @@ -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 + } +] \ No newline at end of file diff --git a/live-streaming/node_modules/.package-lock.json b/live-streaming/node_modules/.package-lock.json index d8df9236..94862771 100644 --- a/live-streaming/node_modules/.package-lock.json +++ b/live-streaming/node_modules/.package-lock.json @@ -35,6 +35,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1405,6 +1406,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2291,6 +2293,22 @@ "dev": true, "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": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", diff --git a/live-streaming/package-lock.json b/live-streaming/package-lock.json index 156d0448..d404d14b 100644 --- a/live-streaming/package-lock.json +++ b/live-streaming/package-lock.json @@ -52,6 +52,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1422,6 +1423,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", diff --git a/live-streaming/server/index.js b/live-streaming/server/index.js index a1a71685..c2ea327e 100644 --- a/live-streaming/server/index.js +++ b/live-streaming/server/index.js @@ -145,8 +145,9 @@ app.get('/health', (req, res) => { app.use(errorHandler); // 启动服务 -app.listen(PORT, () => { +app.listen(PORT, '0.0.0.0', () => { 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 HTTP: http://${process.env.SRS_HOST || 'localhost'}:${process.env.SRS_HTTP_PORT || 8080}/live/{streamKey}.flv`); }); diff --git a/live-streaming/server/store/roomStore.js b/live-streaming/server/store/roomStore.js index 7fd6aea8..21ecaaf5 100644 --- a/live-streaming/server/store/roomStore.js +++ b/live-streaming/server/store/roomStore.js @@ -1,7 +1,47 @@ 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 = { // 创建房间 @@ -18,6 +58,7 @@ const roomStore = { startedAt: null }; rooms.set(id, room); + saveRooms(rooms); return room; }, @@ -43,6 +84,7 @@ const roomStore = { const updated = { ...room, ...data }; rooms.set(id, updated); + saveRooms(rooms); return updated; }, @@ -54,12 +96,15 @@ const roomStore = { room.isLive = isLive; room.startedAt = isLive ? new Date().toISOString() : null; rooms.set(streamKey, room); + saveRooms(rooms); return room; }, // 删除房间 delete(id) { - return rooms.delete(id); + const result = rooms.delete(id); + if (result) saveRooms(rooms); + return result; }, // 清空所有房间 (测试用) diff --git a/live-streaming/server/utils/srsHttpApi.js b/live-streaming/server/utils/srsHttpApi.js index 2939b80d..e1974c08 100644 --- a/live-streaming/server/utils/srsHttpApi.js +++ b/live-streaming/server/utils/srsHttpApi.js @@ -59,7 +59,7 @@ const getActiveStreamKeys = async ({ app = 'live' } = {}) => { const host = process.env.SRS_HOST || 'localhost'; 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 { const payload = await requestJson(url, { timeoutMs: 1500 }); @@ -77,7 +77,9 @@ const getActiveStreamKeys = async ({ app = 'live' } = {}) => { } catch (e) { if (!warnedOnce) { 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(); } diff --git a/live-streaming/server/utils/streamUrl.js b/live-streaming/server/utils/streamUrl.js index 55a7c27b..78e1952d 100644 --- a/live-streaming/server/utils/streamUrl.js +++ b/live-streaming/server/utils/streamUrl.js @@ -11,8 +11,8 @@ const generateStreamUrls = (streamKey, requestHost) => { : !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase()); return { - // 推流地址 (给主播用) - rtmp: `rtmp://${host}:${rtmpPort}/live`, + // 推流地址 (给主播用) - 完整路径包含 streamKey + rtmp: `rtmp://${host}:${rtmpPort}/live/${streamKey}`, // 播放地址 (给观众用) flv: `http://${host}:${httpPort}/live/${streamKey}.flv`, diff --git a/使用教程.md b/使用教程.md new file mode 100644 index 00000000..af195b14 --- /dev/null +++ b/使用教程.md @@ -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 + +--- + +**祝你使用愉快!🎉** diff --git a/如何获取RTMP地址.md b/如何获取RTMP地址.md new file mode 100644 index 00000000..5181f752 --- /dev/null +++ b/如何获取RTMP地址.md @@ -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` 解决常见问题 diff --git a/推流问题完整诊断.md b/推流问题完整诊断.md new file mode 100644 index 00000000..d406944b --- /dev/null +++ b/推流问题完整诊断.md @@ -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 日志中有什么错误?