修改为安卓

This commit is contained in:
ShiQi 2025-12-16 15:47:36 +08:00
parent 86b21d022a
commit 827c714610
34 changed files with 511 additions and 20 deletions

View 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
View File

@ -0,0 +1 @@

View 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>

View File

@ -0,0 +1,2 @@
package com.example.livestreaming

View File

@ -0,0 +1,2 @@
package com.example.livestreaming

View File

@ -0,0 +1,2 @@
package com.example.livestreaming

View File

@ -0,0 +1,2 @@
package com.example.livestreaming.net

View File

@ -0,0 +1,2 @@
package com.example.livestreaming.net

View File

@ -0,0 +1,2 @@
package com.example.livestreaming.net

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -0,0 +1,3 @@
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@ -0,0 +1,3 @@
<resources>
<string name="app_name">Live Streaming</string>
</resources>

View 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>

View File

@ -0,0 +1,3 @@
plugins {
id("com.android.application") version "8.1.2" apply false
}

View File

@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
android.useAndroidX=true
systemProp.gradle.wrapperUser=myuser
systemProp.gradle.wrapperPassword=mypassword

View 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")

View File

@ -4,3 +4,4 @@ SRS_HOST=localhost
SRS_RTMP_PORT=1935
SRS_HTTP_PORT=8080
CLIENT_URL=http://localhost:3000
EMBEDDED_MEDIA_SERVER=0

View File

@ -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

View File

@ -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 缓存,提高首屏速度

View File

@ -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",

View File

@ -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",

View File

@ -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": {

View File

@ -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.');

View File

@ -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)
}
});
});

View File

@ -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
View 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