From 827c71461021b1bc49d8c722083e3dfd1daa5519 Mon Sep 17 00:00:00 2001
From: ShiQi <3572915148@qq.com>
Date: Tue, 16 Dec 2025 15:47:36 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=BA=E5=AE=89=E5=8D=93?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
android-app/app/build.gradle.kts | 56 ++++++++++++++
android-app/app/proguard-rules.pro | 1 +
android-app/app/src/main/AndroidManifest.xml | 33 ++++++++
.../com/example/livestreaming/MainActivity.kt | 2 +
.../example/livestreaming/PlayerActivity.kt | 2 +
.../com/example/livestreaming/RoomsAdapter.kt | 2 +
.../example/livestreaming/net/ApiClient.kt | 2 +
.../example/livestreaming/net/ApiModels.kt | 2 +
.../example/livestreaming/net/ApiService.kt | 2 +
.../app/src/main/res/drawable/ic_launcher.xml | 15 ++++
.../main/res/drawable/ic_launcher_round.xml | 15 ++++
.../app/src/main/res/layout/activity_main.xml | 49 ++++++++++++
.../src/main/res/layout/activity_player.xml | 16 ++++
.../app/src/main/res/layout/item_room.xml | 52 +++++++++++++
.../res/mipmap-anydpi-v26/ic_launcher.xml | 5 ++
.../ic_launcher_foreground.xml | 11 +++
.../mipmap-anydpi-v26/ic_launcher_round.xml | 5 ++
.../app/src/main/res/values/colors.xml | 7 ++
.../res/values/ic_launcher_background.xml | 3 +
.../app/src/main/res/values/strings.xml | 3 +
.../app/src/main/res/values/themes.xml | 15 ++++
android-app/build.gradle.kts | 3 +
android-app/gradle.properties | 6 ++
android-app/settings.gradle.kts | 18 +++++
live-streaming/.env | 1 +
live-streaming/docker-compose.yml | 3 +
live-streaming/docker/srs/srs.conf | 4 +-
.../node_modules/.package-lock.json | 6 +-
live-streaming/package-lock.json | 8 +-
live-streaming/package.json | 2 +-
live-streaming/server/index.js | 74 +++++++++++++++++-
live-streaming/server/routes/rooms.js | 15 +++-
live-streaming/server/utils/streamUrl.js | 18 +++--
run_emulator.bat | 75 +++++++++++++++++++
34 files changed, 511 insertions(+), 20 deletions(-)
create mode 100644 android-app/app/build.gradle.kts
create mode 100644 android-app/app/proguard-rules.pro
create mode 100644 android-app/app/src/main/AndroidManifest.xml
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/MainActivity.kt
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.kt
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.kt
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.kt
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/ApiModels.kt
create mode 100644 android-app/app/src/main/java/com/example/livestreaming/net/ApiService.kt
create mode 100644 android-app/app/src/main/res/drawable/ic_launcher.xml
create mode 100644 android-app/app/src/main/res/drawable/ic_launcher_round.xml
create mode 100644 android-app/app/src/main/res/layout/activity_main.xml
create mode 100644 android-app/app/src/main/res/layout/activity_player.xml
create mode 100644 android-app/app/src/main/res/layout/item_room.xml
create mode 100644 android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
create mode 100644 android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground.xml
create mode 100644 android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
create mode 100644 android-app/app/src/main/res/values/colors.xml
create mode 100644 android-app/app/src/main/res/values/ic_launcher_background.xml
create mode 100644 android-app/app/src/main/res/values/strings.xml
create mode 100644 android-app/app/src/main/res/values/themes.xml
create mode 100644 android-app/build.gradle.kts
create mode 100644 android-app/gradle.properties
create mode 100644 android-app/settings.gradle.kts
create mode 100644 run_emulator.bat
diff --git a/android-app/app/build.gradle.kts b/android-app/app/build.gradle.kts
new file mode 100644
index 00000000..05ead42b
--- /dev/null
+++ b/android-app/app/build.gradle.kts
@@ -0,0 +1,56 @@
+plugins {
+ id("com.android.application")
+}
+
+android {
+ namespace = "com.example.livestreaming"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.example.livestreaming"
+ minSdk = 21
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ buildConfigField("String", "API_BASE_URL", "\"http://10.0.2.2:3001/api/\"")
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "proguard-rules.pro"
+ )
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ buildFeatures {
+ viewBinding = true
+ buildConfig = true
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core:1.12.0")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.11.0")
+ implementation("androidx.constraintlayout:constraintlayout:2.1.4")
+ implementation("androidx.recyclerview:recyclerview:1.3.2")
+
+ implementation("com.squareup.retrofit2:retrofit:2.9.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.9.0")
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ val media3Version = "1.2.1"
+ implementation("androidx.media3:media3-exoplayer:$media3Version")
+ implementation("androidx.media3:media3-exoplayer-hls:$media3Version")
+ implementation("androidx.media3:media3-ui:$media3Version")
+}
diff --git a/android-app/app/proguard-rules.pro b/android-app/app/proguard-rules.pro
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/android-app/app/proguard-rules.pro
@@ -0,0 +1 @@
+
diff --git a/android-app/app/src/main/AndroidManifest.xml b/android-app/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..f56514d3
--- /dev/null
+++ b/android-app/app/src/main/AndroidManifest.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/MainActivity.kt b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.kt
new file mode 100644
index 00000000..4da28947
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/MainActivity.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.kt b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.kt
new file mode 100644
index 00000000..4da28947
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/PlayerActivity.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.kt b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.kt
new file mode 100644
index 00000000..4da28947
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/RoomsAdapter.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.kt b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.kt
new file mode 100644
index 00000000..1692e0f7
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiClient.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming.net
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiModels.kt b/android-app/app/src/main/java/com/example/livestreaming/net/ApiModels.kt
new file mode 100644
index 00000000..1692e0f7
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiModels.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming.net
+
diff --git a/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.kt b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.kt
new file mode 100644
index 00000000..1692e0f7
--- /dev/null
+++ b/android-app/app/src/main/java/com/example/livestreaming/net/ApiService.kt
@@ -0,0 +1,2 @@
+package com.example.livestreaming.net
+
diff --git a/android-app/app/src/main/res/drawable/ic_launcher.xml b/android-app/app/src/main/res/drawable/ic_launcher.xml
new file mode 100644
index 00000000..e426ead3
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_launcher.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/drawable/ic_launcher_round.xml b/android-app/app/src/main/res/drawable/ic_launcher_round.xml
new file mode 100644
index 00000000..9654cc7d
--- /dev/null
+++ b/android-app/app/src/main/res/drawable/ic_launcher_round.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_main.xml b/android-app/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..5f70914e
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/activity_player.xml b/android-app/app/src/main/res/layout/activity_player.xml
new file mode 100644
index 00000000..f234a53a
--- /dev/null
+++ b/android-app/app/src/main/res/layout/activity_player.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/layout/item_room.xml b/android-app/app/src/main/res/layout/item_room.xml
new file mode 100644
index 00000000..e502de12
--- /dev/null
+++ b/android-app/app/src/main/res/layout/item_room.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..be438580
--- /dev/null
+++ b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground.xml
new file mode 100644
index 00000000..51371852
--- /dev/null
+++ b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_foreground.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 00000000..be438580
--- /dev/null
+++ b/android-app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/android-app/app/src/main/res/values/colors.xml b/android-app/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..16808c35
--- /dev/null
+++ b/android-app/app/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+ #6200EE
+ #3700B3
+ #03DAC5
+ #018786
+ #E53935
+
diff --git a/android-app/app/src/main/res/values/ic_launcher_background.xml b/android-app/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 00000000..daa60c37
--- /dev/null
+++ b/android-app/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,3 @@
+
+ #000000
+
diff --git a/android-app/app/src/main/res/values/strings.xml b/android-app/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..45727604
--- /dev/null
+++ b/android-app/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Live Streaming
+
diff --git a/android-app/app/src/main/res/values/themes.xml b/android-app/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..044672a5
--- /dev/null
+++ b/android-app/app/src/main/res/values/themes.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/android-app/build.gradle.kts b/android-app/build.gradle.kts
new file mode 100644
index 00000000..9d720661
--- /dev/null
+++ b/android-app/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("com.android.application") version "8.1.2" apply false
+}
diff --git a/android-app/gradle.properties b/android-app/gradle.properties
new file mode 100644
index 00000000..7f4b982d
--- /dev/null
+++ b/android-app/gradle.properties
@@ -0,0 +1,6 @@
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+android.useAndroidX=true
+
+
+systemProp.gradle.wrapperUser=myuser
+systemProp.gradle.wrapperPassword=mypassword
\ No newline at end of file
diff --git a/android-app/settings.gradle.kts b/android-app/settings.gradle.kts
new file mode 100644
index 00000000..060156b8
--- /dev/null
+++ b/android-app/settings.gradle.kts
@@ -0,0 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "LiveStreamingAndroid"
+include(":app")
diff --git a/live-streaming/.env b/live-streaming/.env
index 1cfe7032..a2b6c071 100644
--- a/live-streaming/.env
+++ b/live-streaming/.env
@@ -4,3 +4,4 @@ SRS_HOST=localhost
SRS_RTMP_PORT=1935
SRS_HTTP_PORT=8080
CLIENT_URL=http://localhost:3000
+EMBEDDED_MEDIA_SERVER=0
\ No newline at end of file
diff --git a/live-streaming/docker-compose.yml b/live-streaming/docker-compose.yml
index 1085d1ce..a8362d98 100644
--- a/live-streaming/docker-compose.yml
+++ b/live-streaming/docker-compose.yml
@@ -29,6 +29,9 @@ services:
- SRS_HOST=srs
- SRS_RTMP_PORT=1935
- SRS_HTTP_PORT=8080
+ - PUBLIC_SRS_HOST=localhost
+ - PUBLIC_SRS_RTMP_PORT=1935
+ - PUBLIC_SRS_HTTP_PORT=8080
depends_on:
- srs
restart: unless-stopped
diff --git a/live-streaming/docker/srs/srs.conf b/live-streaming/docker/srs/srs.conf
index e68e12be..bba02d12 100644
--- a/live-streaming/docker/srs/srs.conf
+++ b/live-streaming/docker/srs/srs.conf
@@ -36,8 +36,8 @@ vhost __defaultVhost__ {
# HTTP 回调配置
http_hooks {
enabled on;
- on_publish http://localhost:3001/api/srs/on_publish;
- on_unpublish http://localhost:3001/api/srs/on_unpublish;
+ on_publish http://host.docker.internal:3001/api/srs/on_publish;
+ on_unpublish http://host.docker.internal:3001/api/srs/on_unpublish;
}
# GOP 缓存,提高首屏速度
diff --git a/live-streaming/node_modules/.package-lock.json b/live-streaming/node_modules/.package-lock.json
index b590443d..d8df9236 100644
--- a/live-streaming/node_modules/.package-lock.json
+++ b/live-streaming/node_modules/.package-lock.json
@@ -3731,9 +3731,9 @@
"license": "MIT"
},
"node_modules/node-media-server": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz",
- "integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==",
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.3.tgz",
+ "integrity": "sha512-KUx32gXf717zo05EFUIoSvCgeA18M1rTUlifLQWbcsDDVKgM6HNMY/xRyoIFNvEBq+5fu1jrQIhBvEBUyxgUlQ==",
"dependencies": {
"basic-auth-connect": "^1.1.0",
"chalk": "^4.1.2",
diff --git a/live-streaming/package-lock.json b/live-streaming/package-lock.json
index 491a0fbe..156d0448 100644
--- a/live-streaming/package-lock.json
+++ b/live-streaming/package-lock.json
@@ -11,7 +11,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
- "node-media-server": "^2.7.0",
+ "node-media-server": "2.7.3",
"uuid": "^9.0.1"
},
"devDependencies": {
@@ -3763,9 +3763,9 @@
"license": "MIT"
},
"node_modules/node-media-server": {
- "version": "2.7.4",
- "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz",
- "integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==",
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.3.tgz",
+ "integrity": "sha512-KUx32gXf717zo05EFUIoSvCgeA18M1rTUlifLQWbcsDDVKgM6HNMY/xRyoIFNvEBq+5fu1jrQIhBvEBUyxgUlQ==",
"dependencies": {
"basic-auth-connect": "^1.1.0",
"chalk": "^4.1.2",
diff --git a/live-streaming/package.json b/live-streaming/package.json
index 89b0de07..ed3f8f0e 100644
--- a/live-streaming/package.json
+++ b/live-streaming/package.json
@@ -15,7 +15,7 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
- "node-media-server": "^2.7.0",
+ "node-media-server": "2.7.3",
"uuid": "^9.0.1"
},
"devDependencies": {
diff --git a/live-streaming/server/index.js b/live-streaming/server/index.js
index 38bf4f93..a1a71685 100644
--- a/live-streaming/server/index.js
+++ b/live-streaming/server/index.js
@@ -2,6 +2,9 @@ require('dotenv').config();
const express = require('express');
const cors = require('cors');
const NodeMediaServer = require('node-media-server');
+const childProcess = require('child_process');
+const fs = require('fs');
+const path = require('path');
const roomsRouter = require('./routes/rooms');
const srsRouter = require('./routes/srs');
const errorHandler = require('./middleware/errorHandler');
@@ -14,9 +17,54 @@ const startMediaServer = () => {
const host = process.env.SRS_HOST || 'localhost';
const rtmpPort = Number(process.env.SRS_RTMP_PORT || 1935);
const httpPort = Number(process.env.SRS_HTTP_PORT || 8080);
+ const rawFfmpegPath = process.env.FFMPEG_PATH;
+
+ const embeddedEnabledRaw = process.env.EMBEDDED_MEDIA_SERVER;
+ const embeddedEnabled = embeddedEnabledRaw == null
+ ? true
+ : !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase());
+
+ if (!embeddedEnabled) {
+ console.log('[Media Server] Embedded NodeMediaServer disabled (EMBEDDED_MEDIA_SERVER=0).');
+ return;
+ }
+
+ let ffmpegPath = rawFfmpegPath;
+ if (!ffmpegPath) {
+ try {
+ if (process.platform === 'win32') {
+ const out = childProcess.execSync('where ffmpeg', { stdio: ['ignore', 'pipe', 'ignore'] });
+ const first = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
+ if (first) ffmpegPath = first;
+ } else {
+ const out = childProcess.execSync('which ffmpeg', { stdio: ['ignore', 'pipe', 'ignore'] });
+ const first = String(out || '').split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
+ if (first) ffmpegPath = first;
+ }
+ } catch (_) {
+ ffmpegPath = null;
+ }
+ }
+
+ if (ffmpegPath) {
+ const p = String(ffmpegPath).trim().replace(/^"|"$/g, '');
+ ffmpegPath = p;
+ try {
+ if (fs.existsSync(ffmpegPath) && fs.statSync(ffmpegPath).isDirectory()) {
+ ffmpegPath = path.join(ffmpegPath, 'ffmpeg.exe');
+ }
+ } catch (_) {
+ ffmpegPath = p;
+ }
+
+ if (!fs.existsSync(ffmpegPath)) {
+ console.warn(`[Media Server] FFMPEG_PATH set but not found: ${ffmpegPath}`);
+ ffmpegPath = null;
+ }
+ }
try {
- const nms = new NodeMediaServer({
+ const nmsConfig = {
rtmp: {
port: rtmpPort,
chunk_size: 60000,
@@ -26,9 +74,25 @@ const startMediaServer = () => {
},
http: {
port: httpPort,
+ mediaroot: './media',
allow_origin: '*'
}
- });
+ };
+
+ if (ffmpegPath) {
+ nmsConfig.trans = {
+ ffmpeg: ffmpegPath,
+ tasks: [
+ {
+ app: 'live',
+ hls: true,
+ hlsFlags: '[hls_time=2:hls_list_size=6:hls_flags=delete_segments]'
+ }
+ ]
+ };
+ }
+
+ const nms = new NodeMediaServer(nmsConfig);
nms.on('prePublish', (id, streamPath) => {
const parts = String(streamPath || '').split('/').filter(Boolean);
@@ -46,6 +110,12 @@ const startMediaServer = () => {
console.log(`Media Server RTMP: rtmp://${host}:${rtmpPort}/live (Stream Key = streamKey)`);
console.log(`Media Server HTTP-FLV: http://${host}:${httpPort}/live/{streamKey}.flv`);
+ if (ffmpegPath) {
+ console.log(`Media Server HLS: http://${host}:${httpPort}/live/{streamKey}/index.m3u8`);
+ console.log(`[Media Server] Using FFmpeg: ${ffmpegPath}`);
+ } else {
+ console.log('Media Server HLS disabled (set FFMPEG_PATH to enable HLS)');
+ }
} catch (e) {
console.error('[Media Server] Failed to start embedded media server:', e.message);
console.error('[Media Server] If ports 1935/8080 are in use, stop the occupying process or change SRS_RTMP_PORT/SRS_HTTP_PORT.');
diff --git a/live-streaming/server/routes/rooms.js b/live-streaming/server/routes/rooms.js
index 3f01be49..6e899c96 100644
--- a/live-streaming/server/routes/rooms.js
+++ b/live-streaming/server/routes/rooms.js
@@ -5,17 +5,24 @@ const { validateRoom } = require('../middleware/validate');
const { generateStreamUrls } = require('../utils/streamUrl');
const { getActiveStreamKeys } = require('../utils/srsHttpApi');
+ const getRequestHost = (req) => {
+ const hostHeader = req && req.get ? req.get('host') : null;
+ const fromHeader = hostHeader ? String(hostHeader).split(':')[0] : null;
+ return req && req.hostname ? req.hostname : fromHeader;
+ };
+
// GET /api/rooms - 获取所有房间
router.get('/', async (req, res) => {
const rooms = roomStore.getAll();
const activeStreamKeys = await getActiveStreamKeys({ app: 'live' });
+ const requestHost = getRequestHost(req);
res.json({
success: true,
data: rooms.map(room => ({
...room,
isLive: activeStreamKeys.size ? activeStreamKeys.has(room.streamKey) : room.isLive,
- streamUrls: generateStreamUrls(room.streamKey)
+ streamUrls: generateStreamUrls(room.streamKey, requestHost)
}))
});
});
@@ -24,12 +31,13 @@ router.get('/', async (req, res) => {
router.post('/', validateRoom, (req, res) => {
const { title, streamerName } = req.body;
const room = roomStore.create({ title, streamerName });
+ const requestHost = getRequestHost(req);
res.status(201).json({
success: true,
data: {
...room,
- streamUrls: generateStreamUrls(room.streamKey)
+ streamUrls: generateStreamUrls(room.streamKey, requestHost)
}
});
});
@@ -37,6 +45,7 @@ router.post('/', validateRoom, (req, res) => {
// GET /api/rooms/:id - 获取单个房间
router.get('/:id', async (req, res) => {
const room = roomStore.getById(req.params.id);
+ const requestHost = getRequestHost(req);
if (!room) {
return res.status(404).json({
@@ -50,7 +59,7 @@ router.get('/:id', async (req, res) => {
data: {
...room,
isLive: (await getActiveStreamKeys({ app: 'live' })).has(room.streamKey) || room.isLive,
- streamUrls: generateStreamUrls(room.streamKey)
+ streamUrls: generateStreamUrls(room.streamKey, requestHost)
}
});
});
diff --git a/live-streaming/server/utils/streamUrl.js b/live-streaming/server/utils/streamUrl.js
index 8e9f1920..55a7c27b 100644
--- a/live-streaming/server/utils/streamUrl.js
+++ b/live-streaming/server/utils/streamUrl.js
@@ -1,8 +1,14 @@
// 生成流地址
-const generateStreamUrls = (streamKey) => {
- const host = process.env.SRS_HOST || 'localhost';
- const rtmpPort = process.env.SRS_RTMP_PORT || 1935;
- const httpPort = process.env.SRS_HTTP_PORT || 8080;
+const generateStreamUrls = (streamKey, requestHost) => {
+ const host = requestHost || process.env.PUBLIC_SRS_HOST || process.env.SRS_HOST || 'localhost';
+ const rtmpPort = process.env.PUBLIC_SRS_RTMP_PORT || process.env.SRS_RTMP_PORT || 1935;
+ const httpPort = process.env.PUBLIC_SRS_HTTP_PORT || process.env.SRS_HTTP_PORT || 8080;
+ const ffmpegPath = process.env.FFMPEG_PATH;
+
+ const embeddedEnabledRaw = process.env.EMBEDDED_MEDIA_SERVER;
+ const embeddedEnabled = embeddedEnabledRaw == null
+ ? true
+ : !['0', 'false', 'off', 'no'].includes(String(embeddedEnabledRaw).toLowerCase());
return {
// 推流地址 (给主播用)
@@ -10,7 +16,9 @@ const generateStreamUrls = (streamKey) => {
// 播放地址 (给观众用)
flv: `http://${host}:${httpPort}/live/${streamKey}.flv`,
- hls: `http://${host}:${httpPort}/live/${streamKey}.m3u8`
+ hls: embeddedEnabled
+ ? (ffmpegPath ? `http://${host}:${httpPort}/live/${streamKey}/index.m3u8` : null)
+ : `http://${host}:${httpPort}/live/${streamKey}.m3u8`
};
};
diff --git a/run_emulator.bat b/run_emulator.bat
new file mode 100644
index 00000000..4f87f6c5
--- /dev/null
+++ b/run_emulator.bat
@@ -0,0 +1,75 @@
+@echo off
+setlocal
+chcp 65001 >nul
+
+set "ROOT=%~dp0"
+
+if "%JAVA_HOME%"=="" (
+ if exist "%ProgramFiles%\Android\Android Studio\jbr" set "JAVA_HOME=%ProgramFiles%\Android\Android Studio\jbr"
+)
+if not "%JAVA_HOME%"=="" set "PATH=%JAVA_HOME%\bin;%PATH%"
+where java >nul 2>&1
+if errorlevel 1 (
+ echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ echo Please install JDK 17 and set JAVA_HOME, then reopen terminal and rerun.
+ exit /b 1
+)
+
+set "GRADLE_BAT="
+for /f "delims=" %%G in ('where gradle.bat 2^>nul') do set "GRADLE_BAT=%%G"
+if "%GRADLE_BAT%"=="" (
+ for /f "delims=" %%G in ('where gradle 2^>nul') do set "GRADLE_BAT=%%G"
+)
+if "%GRADLE_BAT%"=="" (
+ set "GRADLE_BAT=D:\soft\gradle-8.1\bin\gradle.bat"
+ if not exist "%GRADLE_BAT%" set "GRADLE_BAT=D:\soft\gradle-8.1-bin\bin\gradle.bat"
+ if not exist "%GRADLE_BAT%" set "GRADLE_BAT=D:\soft\gradle-8.1-bin\gradle-8.1\bin\gradle.bat"
+)
+
+if "%GRADLE_BAT%"=="" (
+ echo Gradle not found in PATH.
+ echo Please install Gradle 8.x and ensure 'gradle' is available in PATH, then rerun.
+ exit /b 1
+)
+if not exist "%GRADLE_BAT%" (
+ echo Gradle not found: %GRADLE_BAT%
+ echo Please update GRADLE_BAT in run_emulator.bat to point to your gradle.bat, e.g. D:\soft\gradle-8.1\bin\gradle.bat
+ exit /b 1
+)
+
+where npm >nul 2>&1
+if errorlevel 1 (
+ echo npm not found in PATH
+ exit /b 1
+)
+
+echo Starting backend in a new window...
+start "live-backend" cmd /k "cd /d ""%ROOT%live-streaming"" ^&^& if not exist node_modules (npm install) ^&^& set PORT=3001 ^&^& set PUBLIC_SRS_HOST=10.0.2.2 ^&^& set PUBLIC_SRS_RTMP_PORT=1935 ^&^& set PUBLIC_SRS_HTTP_PORT=8080 ^&^& npm run dev"
+
+echo Preparing Android project (wrapper/build/install)...
+pushd "%ROOT%android-app"
+
+if not exist gradlew.bat (
+ call "%GRADLE_BAT%" wrapper --gradle-version 8.1
+ if errorlevel 1 (
+ popd
+ exit /b 1
+ )
+ if not exist gradlew.bat (
+ echo Failed to generate gradlew.bat. Please ensure JAVA_HOME points to a valid JDK 17 installation.
+ popd
+ exit /b 1
+ )
+)
+
+call "%CD%\gradlew.bat" :app:installDebug
+if errorlevel 1 (
+ popd
+ exit /b 1
+)
+
+popd
+
+echo.
+echo Installed. Make sure your Android Emulator is running, then open the app "Live Streaming".
+endlocal