修改为安卓
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_RTMP_PORT=1935
|
||||||
SRS_HTTP_PORT=8080
|
SRS_HTTP_PORT=8080
|
||||||
CLIENT_URL=http://localhost:3000
|
CLIENT_URL=http://localhost:3000
|
||||||
|
EMBEDDED_MEDIA_SERVER=0
|
||||||
|
|
@ -29,6 +29,9 @@ services:
|
||||||
- SRS_HOST=srs
|
- SRS_HOST=srs
|
||||||
- SRS_RTMP_PORT=1935
|
- SRS_RTMP_PORT=1935
|
||||||
- SRS_HTTP_PORT=8080
|
- SRS_HTTP_PORT=8080
|
||||||
|
- PUBLIC_SRS_HOST=localhost
|
||||||
|
- PUBLIC_SRS_RTMP_PORT=1935
|
||||||
|
- PUBLIC_SRS_HTTP_PORT=8080
|
||||||
depends_on:
|
depends_on:
|
||||||
- srs
|
- srs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ vhost __defaultVhost__ {
|
||||||
# HTTP 回调配置
|
# HTTP 回调配置
|
||||||
http_hooks {
|
http_hooks {
|
||||||
enabled on;
|
enabled on;
|
||||||
on_publish http://localhost:3001/api/srs/on_publish;
|
on_publish http://host.docker.internal:3001/api/srs/on_publish;
|
||||||
on_unpublish http://localhost:3001/api/srs/on_unpublish;
|
on_unpublish http://host.docker.internal:3001/api/srs/on_unpublish;
|
||||||
}
|
}
|
||||||
|
|
||||||
# GOP 缓存,提高首屏速度
|
# 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"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-media-server": {
|
"node_modules/node-media-server": {
|
||||||
"version": "2.7.4",
|
"version": "2.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.3.tgz",
|
||||||
"integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==",
|
"integrity": "sha512-KUx32gXf717zo05EFUIoSvCgeA18M1rTUlifLQWbcsDDVKgM6HNMY/xRyoIFNvEBq+5fu1jrQIhBvEBUyxgUlQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"basic-auth-connect": "^1.1.0",
|
"basic-auth-connect": "^1.1.0",
|
||||||
"chalk": "^4.1.2",
|
"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",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-media-server": "^2.7.0",
|
"node-media-server": "2.7.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -3763,9 +3763,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-media-server": {
|
"node_modules/node-media-server": {
|
||||||
"version": "2.7.4",
|
"version": "2.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/node-media-server/-/node-media-server-2.7.3.tgz",
|
||||||
"integrity": "sha512-4nsqfukfnI6tY7d1deqBQSj3KywOiNVlPQkY0tLIjF62rzXo1SDXJXgUU+cVs5e06uO4x/55O4SiAaRH3li/Vg==",
|
"integrity": "sha512-KUx32gXf717zo05EFUIoSvCgeA18M1rTUlifLQWbcsDDVKgM6HNMY/xRyoIFNvEBq+5fu1jrQIhBvEBUyxgUlQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"basic-auth-connect": "^1.1.0",
|
"basic-auth-connect": "^1.1.0",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"node-media-server": "^2.7.0",
|
"node-media-server": "2.7.3",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,9 @@ require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const NodeMediaServer = require('node-media-server');
|
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 roomsRouter = require('./routes/rooms');
|
||||||
const srsRouter = require('./routes/srs');
|
const srsRouter = require('./routes/srs');
|
||||||
const errorHandler = require('./middleware/errorHandler');
|
const errorHandler = require('./middleware/errorHandler');
|
||||||
|
|
@ -14,9 +17,54 @@ const startMediaServer = () => {
|
||||||
const host = process.env.SRS_HOST || 'localhost';
|
const host = process.env.SRS_HOST || 'localhost';
|
||||||
const rtmpPort = Number(process.env.SRS_RTMP_PORT || 1935);
|
const rtmpPort = Number(process.env.SRS_RTMP_PORT || 1935);
|
||||||
const httpPort = Number(process.env.SRS_HTTP_PORT || 8080);
|
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 {
|
try {
|
||||||
const nms = new NodeMediaServer({
|
const nmsConfig = {
|
||||||
rtmp: {
|
rtmp: {
|
||||||
port: rtmpPort,
|
port: rtmpPort,
|
||||||
chunk_size: 60000,
|
chunk_size: 60000,
|
||||||
|
|
@ -26,9 +74,25 @@ const startMediaServer = () => {
|
||||||
},
|
},
|
||||||
http: {
|
http: {
|
||||||
port: httpPort,
|
port: httpPort,
|
||||||
|
mediaroot: './media',
|
||||||
allow_origin: '*'
|
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) => {
|
nms.on('prePublish', (id, streamPath) => {
|
||||||
const parts = String(streamPath || '').split('/').filter(Boolean);
|
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 RTMP: rtmp://${host}:${rtmpPort}/live (Stream Key = streamKey)`);
|
||||||
console.log(`Media Server HTTP-FLV: http://${host}:${httpPort}/live/{streamKey}.flv`);
|
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) {
|
} catch (e) {
|
||||||
console.error('[Media Server] Failed to start embedded media server:', e.message);
|
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.');
|
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 { generateStreamUrls } = require('../utils/streamUrl');
|
||||||
const { getActiveStreamKeys } = require('../utils/srsHttpApi');
|
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 - 获取所有房间
|
// GET /api/rooms - 获取所有房间
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const rooms = roomStore.getAll();
|
const rooms = roomStore.getAll();
|
||||||
const activeStreamKeys = await getActiveStreamKeys({ app: 'live' });
|
const activeStreamKeys = await getActiveStreamKeys({ app: 'live' });
|
||||||
|
const requestHost = getRequestHost(req);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: rooms.map(room => ({
|
data: rooms.map(room => ({
|
||||||
...room,
|
...room,
|
||||||
isLive: activeStreamKeys.size ? activeStreamKeys.has(room.streamKey) : room.isLive,
|
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) => {
|
router.post('/', validateRoom, (req, res) => {
|
||||||
const { title, streamerName } = req.body;
|
const { title, streamerName } = req.body;
|
||||||
const room = roomStore.create({ title, streamerName });
|
const room = roomStore.create({ title, streamerName });
|
||||||
|
const requestHost = getRequestHost(req);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
...room,
|
...room,
|
||||||
streamUrls: generateStreamUrls(room.streamKey)
|
streamUrls: generateStreamUrls(room.streamKey, requestHost)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -37,6 +45,7 @@ router.post('/', validateRoom, (req, res) => {
|
||||||
// GET /api/rooms/:id - 获取单个房间
|
// GET /api/rooms/:id - 获取单个房间
|
||||||
router.get('/:id', async (req, res) => {
|
router.get('/:id', async (req, res) => {
|
||||||
const room = roomStore.getById(req.params.id);
|
const room = roomStore.getById(req.params.id);
|
||||||
|
const requestHost = getRequestHost(req);
|
||||||
|
|
||||||
if (!room) {
|
if (!room) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
|
|
@ -50,7 +59,7 @@ router.get('/:id', async (req, res) => {
|
||||||
data: {
|
data: {
|
||||||
...room,
|
...room,
|
||||||
isLive: (await getActiveStreamKeys({ app: 'live' })).has(room.streamKey) || room.isLive,
|
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 generateStreamUrls = (streamKey, requestHost) => {
|
||||||
const host = process.env.SRS_HOST || 'localhost';
|
const host = requestHost || process.env.PUBLIC_SRS_HOST || process.env.SRS_HOST || 'localhost';
|
||||||
const rtmpPort = process.env.SRS_RTMP_PORT || 1935;
|
const rtmpPort = process.env.PUBLIC_SRS_RTMP_PORT || process.env.SRS_RTMP_PORT || 1935;
|
||||||
const httpPort = process.env.SRS_HTTP_PORT || 8080;
|
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 {
|
return {
|
||||||
// 推流地址 (给主播用)
|
// 推流地址 (给主播用)
|
||||||
|
|
@ -10,7 +16,9 @@ const generateStreamUrls = (streamKey) => {
|
||||||
|
|
||||||
// 播放地址 (给观众用)
|
// 播放地址 (给观众用)
|
||||||
flv: `http://${host}:${httpPort}/live/${streamKey}.flv`,
|
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