修改为安卓
This commit is contained in:
parent
86b21d022a
commit
827c714610
56
android-app/app/build.gradle.kts
Normal file
56
android-app/app/build.gradle.kts
Normal file
|
|
@ -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")
|
||||
}
|
||||
1
android-app/app/proguard-rules.pro
vendored
Normal file
1
android-app/app/proguard-rules.pro
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
33
android-app/app/src/main/AndroidManifest.xml
Normal file
33
android-app/app/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Live Streaming"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.LiveStreaming"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.PlayerActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.RoomDetailActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name="com.example.livestreaming.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming.net
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming.net
|
||||
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
package com.example.livestreaming.net
|
||||
|
||||
15
android-app/app/src/main/res/drawable/ic_launcher.xml
Normal file
15
android-app/app/src/main/res/drawable/ic_launcher.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,22c17.673,0 32,14.327 32,32s-14.327,32 -32,32 -32,-14.327 -32,-32 14.327,-32 32,-32z" />
|
||||
</vector>
|
||||
15
android-app/app/src/main/res/drawable/ic_launcher_round.xml
Normal file
15
android-app/app/src/main/res/drawable/ic_launcher_round.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M54,0c29.823,0 54,24.177 54,54s-24.177,54 -54,54 -54,-24.177 -54,-54 24.177,-54 54,-54z" />
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,26c15.464,0 28,12.536 28,28s-12.536,28 -28,28 -28,-12.536 -28,-28 12.536,-28 28,-28z" />
|
||||
</vector>
|
||||
49
android-app/app/src/main/res/layout/activity_main.xml
Normal file
49
android-app/app/src/main/res/layout/activity_main.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>
|
||||
16
android-app/app/src/main/res/layout/activity_player.xml
Normal file
16
android-app/app/src/main/res/layout/activity_player.xml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?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">
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/playerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
52
android-app/app/src/main/res/layout/item_room.xml
Normal file
52
android-app/app/src/main/res/layout/item_room.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>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M54,16c21,0 38,17 38,38s-17,38 -38,38 -38,-17 -38,-38 17,-38 38,-38z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
7
android-app/app/src/main/res/values/colors.xml
Normal file
7
android-app/app/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<resources>
|
||||
<color name="purple_500">#6200EE</color>
|
||||
<color name="purple_700">#3700B3</color>
|
||||
<color name="teal_200">#03DAC5</color>
|
||||
<color name="teal_700">#018786</color>
|
||||
<color name="live_red">#E53935</color>
|
||||
</resources>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<color name="ic_launcher_background">#000000</color>
|
||||
</resources>
|
||||
3
android-app/app/src/main/res/values/strings.xml
Normal file
3
android-app/app/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Live Streaming</string>
|
||||
</resources>
|
||||
15
android-app/app/src/main/res/values/themes.xml
Normal file
15
android-app/app/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.LiveStreaming" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="colorPrimary">@color/purple_500</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
<item name="colorOnPrimary">@android:color/white</item>
|
||||
<item name="colorSecondary">@color/teal_200</item>
|
||||
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||
<item name="colorOnSecondary">@android:color/black</item>
|
||||
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
|
||||
<item name="android:navigationBarColor">@android:color/black</item>
|
||||
<item name="android:windowLightStatusBar" tools:targetApi="m">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
3
android-app/build.gradle.kts
Normal file
3
android-app/build.gradle.kts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
plugins {
|
||||
id("com.android.application") version "8.1.2" apply false
|
||||
}
|
||||
6
android-app/gradle.properties
Normal file
6
android-app/gradle.properties
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
|
||||
|
||||
systemProp.gradle.wrapperUser=myuser
|
||||
systemProp.gradle.wrapperPassword=mypassword
|
||||
18
android-app/settings.gradle.kts
Normal file
18
android-app/settings.gradle.kts
Normal file
|
|
@ -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")
|
||||
|
|
@ -4,3 +4,4 @@ SRS_HOST=localhost
|
|||
SRS_RTMP_PORT=1935
|
||||
SRS_HTTP_PORT=8080
|
||||
CLIENT_URL=http://localhost:3000
|
||||
EMBEDDED_MEDIA_SERVER=0
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 缓存,提高首屏速度
|
||||
|
|
|
|||
6
live-streaming/node_modules/.package-lock.json
generated
vendored
6
live-streaming/node_modules/.package-lock.json
generated
vendored
|
|
@ -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",
|
||||
|
|
|
|||
8
live-streaming/package-lock.json
generated
8
live-streaming/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
75
run_emulator.bat
Normal file
75
run_emulator.bat
Normal file
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user