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