From d90205b9d6c60aaee93a192be495d7ceb083389c Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:47:45 +0200 Subject: [PATCH 01/21] Updated Gradle version from 7.4.2 to 8.8 --- VideoServer/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VideoServer/build.gradle b/VideoServer/build.gradle index d50fb47..448babd 100644 --- a/VideoServer/build.gradle +++ b/VideoServer/build.gradle @@ -24,4 +24,4 @@ compileKotlin { compileTestKotlin { kotlinOptions.jvmTarget = '1.8' -} \ No newline at end of file +} From ff1cab5339dccc77c3eacbb81aab084a1216583a Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:50:15 +0200 Subject: [PATCH 02/21] Updated Gradle version from 7.4.2 to 8.8 --- Andorid/gradle/wrapper/gradle-wrapper.properties | 2 +- VideoServer/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Andorid/gradle/wrapper/gradle-wrapper.properties b/Andorid/gradle/wrapper/gradle-wrapper.properties index e2847c8..a441313 100644 --- a/Andorid/gradle/wrapper/gradle-wrapper.properties +++ b/Andorid/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/VideoServer/gradle/wrapper/gradle-wrapper.properties b/VideoServer/gradle/wrapper/gradle-wrapper.properties index 60c76b3..0d18421 100644 --- a/VideoServer/gradle/wrapper/gradle-wrapper.properties +++ b/VideoServer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists \ No newline at end of file +zipStorePath=wrapper/dists From a3af9e1057e6c2947a5a59e8355aa9bf7e163585 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:52:07 +0200 Subject: [PATCH 03/21] Added Github Actions Worker --- .github/workflows/android-build.yml | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/android-build.yml diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml new file mode 100644 index 0000000..7c2cbbb --- /dev/null +++ b/.github/workflows/android-build.yml @@ -0,0 +1,48 @@ +name: Build Android APK and Video Server + +on: + push: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Make gradlew executable (Android) + run: chmod +x gradlew + working-directory: ./Andorid + + - name: Build APK + run: ./gradlew assembleDebug + working-directory: ./Andorid + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: debug-apk + path: Andorid/app/build/outputs/apk/debug/app-debug.apk + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Make gradlew executable (VideoServer) + run: chmod +x gradlew + working-directory: ./VideoServer + + - name: Build VideoServer + run: ./gradlew build + working-directory: ./VideoServer + + - name: Upload VideoServer jar artifact + uses: actions/upload-artifact@v4 + with: + name: video-server-jar + path: VideoServer/build/libs/*.jar From 57d3e75adcf3c41842b5a78973f338adc9c3de78 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:53:08 +0200 Subject: [PATCH 04/21] Added shadowjar to VideoServer --- VideoServer/build.gradle | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/VideoServer/build.gradle b/VideoServer/build.gradle index 448babd..aed851a 100644 --- a/VideoServer/build.gradle +++ b/VideoServer/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'com.github.johnrengelman.shadow' version '8.1.1' } group = 'org.example' @@ -11,6 +12,7 @@ repositories { dependencies { implementation 'org.java-websocket:Java-WebSocket:1.5.3' + implementation 'org.jetbrains.kotlin:kotlin-stdlib' testImplementation 'org.jetbrains.kotlin:kotlin-test' } @@ -25,3 +27,20 @@ compileKotlin { compileTestKotlin { kotlinOptions.jvmTarget = '1.8' } + +jar { + manifest { + attributes( + 'Main-Class': 'MainKt' + ) + } +} + +shadowJar { + archiveBaseName.set('VideoServer') + archiveClassifier.set('') + archiveVersion.set('1.0-SNAPSHOT') + manifest { + attributes('Main-Class': 'MainKt') + } +} From e8221d520c26333e738637421864e7f737478706 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:53:57 +0200 Subject: [PATCH 05/21] Updated the gradle version from 8.8 to 8.11.1 --- Andorid/gradle/wrapper/gradle-wrapper.properties | 2 +- VideoServer/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Andorid/gradle/wrapper/gradle-wrapper.properties b/Andorid/gradle/wrapper/gradle-wrapper.properties index a441313..e2847c8 100644 --- a/Andorid/gradle/wrapper/gradle-wrapper.properties +++ b/Andorid/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/VideoServer/gradle/wrapper/gradle-wrapper.properties b/VideoServer/gradle/wrapper/gradle-wrapper.properties index 0d18421..81aa1c0 100644 --- a/VideoServer/gradle/wrapper/gradle-wrapper.properties +++ b/VideoServer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d2db7547d2d072d6ae46cbd2f001e1adfc964819 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 14:58:54 +0200 Subject: [PATCH 06/21] Fixed Android folder name typo --- .github/workflows/android-build.yml | 6 +++--- {Andorid => Android}/app/.gitignore | 0 {Andorid => Android}/app/build.gradle | 0 {Andorid => Android}/app/proguard-rules.pro | 0 .../app/src/main/AndroidManifest.xml | 0 .../app/src/main/ic_launcher-playstore.png | Bin .../java/com/ipcamera/CameraPermissionActivity.kt | 0 .../app/src/main/java/com/ipcamera/EdgeToEdge.kt | 0 .../app/src/main/java/com/ipcamera/MainActivity.kt | 0 .../app/src/main/java/com/ipcamera/MainFragment.kt | 0 .../src/main/java/com/ipcamera/SettingsFragment.kt | 0 .../main/java/com/ipcamera/SettingsPreferences.kt | 0 .../src/main/java/com/ipcamera/StreamActivity.kt | 0 .../app/src/main/res/drawable/ic_camera.xml | 0 .../main/res/drawable/ic_launcher_foreground.xml | 0 .../main/res/layout/camera_permission_activity.xml | 0 .../app/src/main/res/layout/main_activity.xml | 0 .../app/src/main/res/layout/main_fragment.xml | 0 .../app/src/main/res/layout/settings_fragment.xml | 0 .../app/src/main/res/layout/stream_activity.xml | 0 .../src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../src/main/res/mipmap-hdpi/ic_launcher_round.webp | Bin .../app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher_round.webp | Bin .../app/src/main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../app/src/main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../app/src/main/res/values/colors.xml | 0 .../app/src/main/res/values/dimens.xml | 0 .../src/main/res/values/ic_launcher_background.xml | 0 .../app/src/main/res/values/strings.xml | 0 .../app/src/main/res/values/themes.xml | 0 .../app/src/main/res/xml/backup_rules.xml | 0 .../app/src/main/res/xml/data_extraction_rules.xml | 0 .../src/test/java/com/ipcamera/ExampleUnitTest.kt | 0 {Andorid => Android}/build.gradle | 0 {Andorid => Android}/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {Andorid => Android}/gradlew | 0 {Andorid => Android}/gradlew.bat | 0 {Andorid => Android}/settings.gradle | 0 47 files changed, 3 insertions(+), 3 deletions(-) rename {Andorid => Android}/app/.gitignore (100%) rename {Andorid => Android}/app/build.gradle (100%) rename {Andorid => Android}/app/proguard-rules.pro (100%) rename {Andorid => Android}/app/src/main/AndroidManifest.xml (100%) rename {Andorid => Android}/app/src/main/ic_launcher-playstore.png (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/CameraPermissionActivity.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/EdgeToEdge.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/MainActivity.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/MainFragment.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/SettingsFragment.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/SettingsPreferences.kt (100%) rename {Andorid => Android}/app/src/main/java/com/ipcamera/StreamActivity.kt (100%) rename {Andorid => Android}/app/src/main/res/drawable/ic_camera.xml (100%) rename {Andorid => Android}/app/src/main/res/drawable/ic_launcher_foreground.xml (100%) rename {Andorid => Android}/app/src/main/res/layout/camera_permission_activity.xml (100%) rename {Andorid => Android}/app/src/main/res/layout/main_activity.xml (100%) rename {Andorid => Android}/app/src/main/res/layout/main_fragment.xml (100%) rename {Andorid => Android}/app/src/main/res/layout/settings_fragment.xml (100%) rename {Andorid => Android}/app/src/main/res/layout/stream_activity.xml (100%) rename {Andorid => Android}/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {Andorid => Android}/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename {Andorid => Android}/app/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename {Andorid => Android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) rename {Andorid => Android}/app/src/main/res/values/colors.xml (100%) rename {Andorid => Android}/app/src/main/res/values/dimens.xml (100%) rename {Andorid => Android}/app/src/main/res/values/ic_launcher_background.xml (100%) rename {Andorid => Android}/app/src/main/res/values/strings.xml (100%) rename {Andorid => Android}/app/src/main/res/values/themes.xml (100%) rename {Andorid => Android}/app/src/main/res/xml/backup_rules.xml (100%) rename {Andorid => Android}/app/src/main/res/xml/data_extraction_rules.xml (100%) rename {Andorid => Android}/app/src/test/java/com/ipcamera/ExampleUnitTest.kt (100%) rename {Andorid => Android}/build.gradle (100%) rename {Andorid => Android}/gradle.properties (100%) rename {Andorid => Android}/gradle/wrapper/gradle-wrapper.jar (100%) rename {Andorid => Android}/gradle/wrapper/gradle-wrapper.properties (100%) rename {Andorid => Android}/gradlew (100%) rename {Andorid => Android}/gradlew.bat (100%) rename {Andorid => Android}/settings.gradle (100%) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 7c2cbbb..f636bbf 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -15,17 +15,17 @@ jobs: - name: Make gradlew executable (Android) run: chmod +x gradlew - working-directory: ./Andorid + working-directory: ./Android - name: Build APK run: ./gradlew assembleDebug - working-directory: ./Andorid + working-directory: ./Android - name: Upload APK artifact uses: actions/upload-artifact@v4 with: name: debug-apk - path: Andorid/app/build/outputs/apk/debug/app-debug.apk + path: Android/app/build/outputs/apk/debug/app-debug.apk - name: Set up JDK uses: actions/setup-java@v4 diff --git a/Andorid/app/.gitignore b/Android/app/.gitignore similarity index 100% rename from Andorid/app/.gitignore rename to Android/app/.gitignore diff --git a/Andorid/app/build.gradle b/Android/app/build.gradle similarity index 100% rename from Andorid/app/build.gradle rename to Android/app/build.gradle diff --git a/Andorid/app/proguard-rules.pro b/Android/app/proguard-rules.pro similarity index 100% rename from Andorid/app/proguard-rules.pro rename to Android/app/proguard-rules.pro diff --git a/Andorid/app/src/main/AndroidManifest.xml b/Android/app/src/main/AndroidManifest.xml similarity index 100% rename from Andorid/app/src/main/AndroidManifest.xml rename to Android/app/src/main/AndroidManifest.xml diff --git a/Andorid/app/src/main/ic_launcher-playstore.png b/Android/app/src/main/ic_launcher-playstore.png similarity index 100% rename from Andorid/app/src/main/ic_launcher-playstore.png rename to Android/app/src/main/ic_launcher-playstore.png diff --git a/Andorid/app/src/main/java/com/ipcamera/CameraPermissionActivity.kt b/Android/app/src/main/java/com/ipcamera/CameraPermissionActivity.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/CameraPermissionActivity.kt rename to Android/app/src/main/java/com/ipcamera/CameraPermissionActivity.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/EdgeToEdge.kt b/Android/app/src/main/java/com/ipcamera/EdgeToEdge.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/EdgeToEdge.kt rename to Android/app/src/main/java/com/ipcamera/EdgeToEdge.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/MainActivity.kt b/Android/app/src/main/java/com/ipcamera/MainActivity.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/MainActivity.kt rename to Android/app/src/main/java/com/ipcamera/MainActivity.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/MainFragment.kt b/Android/app/src/main/java/com/ipcamera/MainFragment.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/MainFragment.kt rename to Android/app/src/main/java/com/ipcamera/MainFragment.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/SettingsFragment.kt b/Android/app/src/main/java/com/ipcamera/SettingsFragment.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/SettingsFragment.kt rename to Android/app/src/main/java/com/ipcamera/SettingsFragment.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/SettingsPreferences.kt b/Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/SettingsPreferences.kt rename to Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt diff --git a/Andorid/app/src/main/java/com/ipcamera/StreamActivity.kt b/Android/app/src/main/java/com/ipcamera/StreamActivity.kt similarity index 100% rename from Andorid/app/src/main/java/com/ipcamera/StreamActivity.kt rename to Android/app/src/main/java/com/ipcamera/StreamActivity.kt diff --git a/Andorid/app/src/main/res/drawable/ic_camera.xml b/Android/app/src/main/res/drawable/ic_camera.xml similarity index 100% rename from Andorid/app/src/main/res/drawable/ic_camera.xml rename to Android/app/src/main/res/drawable/ic_camera.xml diff --git a/Andorid/app/src/main/res/drawable/ic_launcher_foreground.xml b/Android/app/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from Andorid/app/src/main/res/drawable/ic_launcher_foreground.xml rename to Android/app/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/Andorid/app/src/main/res/layout/camera_permission_activity.xml b/Android/app/src/main/res/layout/camera_permission_activity.xml similarity index 100% rename from Andorid/app/src/main/res/layout/camera_permission_activity.xml rename to Android/app/src/main/res/layout/camera_permission_activity.xml diff --git a/Andorid/app/src/main/res/layout/main_activity.xml b/Android/app/src/main/res/layout/main_activity.xml similarity index 100% rename from Andorid/app/src/main/res/layout/main_activity.xml rename to Android/app/src/main/res/layout/main_activity.xml diff --git a/Andorid/app/src/main/res/layout/main_fragment.xml b/Android/app/src/main/res/layout/main_fragment.xml similarity index 100% rename from Andorid/app/src/main/res/layout/main_fragment.xml rename to Android/app/src/main/res/layout/main_fragment.xml diff --git a/Andorid/app/src/main/res/layout/settings_fragment.xml b/Android/app/src/main/res/layout/settings_fragment.xml similarity index 100% rename from Andorid/app/src/main/res/layout/settings_fragment.xml rename to Android/app/src/main/res/layout/settings_fragment.xml diff --git a/Andorid/app/src/main/res/layout/stream_activity.xml b/Android/app/src/main/res/layout/stream_activity.xml similarity index 100% rename from Andorid/app/src/main/res/layout/stream_activity.xml rename to Android/app/src/main/res/layout/stream_activity.xml diff --git a/Andorid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from Andorid/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/Andorid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from Andorid/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to Android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/Andorid/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to Android/app/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/Andorid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to Android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/Andorid/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to Android/app/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/Andorid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to Android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/Andorid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to Android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/Andorid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to Android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/Andorid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to Android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/Andorid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to Android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/Andorid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/Andorid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from Andorid/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to Android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/Andorid/app/src/main/res/values/colors.xml b/Android/app/src/main/res/values/colors.xml similarity index 100% rename from Andorid/app/src/main/res/values/colors.xml rename to Android/app/src/main/res/values/colors.xml diff --git a/Andorid/app/src/main/res/values/dimens.xml b/Android/app/src/main/res/values/dimens.xml similarity index 100% rename from Andorid/app/src/main/res/values/dimens.xml rename to Android/app/src/main/res/values/dimens.xml diff --git a/Andorid/app/src/main/res/values/ic_launcher_background.xml b/Android/app/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from Andorid/app/src/main/res/values/ic_launcher_background.xml rename to Android/app/src/main/res/values/ic_launcher_background.xml diff --git a/Andorid/app/src/main/res/values/strings.xml b/Android/app/src/main/res/values/strings.xml similarity index 100% rename from Andorid/app/src/main/res/values/strings.xml rename to Android/app/src/main/res/values/strings.xml diff --git a/Andorid/app/src/main/res/values/themes.xml b/Android/app/src/main/res/values/themes.xml similarity index 100% rename from Andorid/app/src/main/res/values/themes.xml rename to Android/app/src/main/res/values/themes.xml diff --git a/Andorid/app/src/main/res/xml/backup_rules.xml b/Android/app/src/main/res/xml/backup_rules.xml similarity index 100% rename from Andorid/app/src/main/res/xml/backup_rules.xml rename to Android/app/src/main/res/xml/backup_rules.xml diff --git a/Andorid/app/src/main/res/xml/data_extraction_rules.xml b/Android/app/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from Andorid/app/src/main/res/xml/data_extraction_rules.xml rename to Android/app/src/main/res/xml/data_extraction_rules.xml diff --git a/Andorid/app/src/test/java/com/ipcamera/ExampleUnitTest.kt b/Android/app/src/test/java/com/ipcamera/ExampleUnitTest.kt similarity index 100% rename from Andorid/app/src/test/java/com/ipcamera/ExampleUnitTest.kt rename to Android/app/src/test/java/com/ipcamera/ExampleUnitTest.kt diff --git a/Andorid/build.gradle b/Android/build.gradle similarity index 100% rename from Andorid/build.gradle rename to Android/build.gradle diff --git a/Andorid/gradle.properties b/Android/gradle.properties similarity index 100% rename from Andorid/gradle.properties rename to Android/gradle.properties diff --git a/Andorid/gradle/wrapper/gradle-wrapper.jar b/Android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from Andorid/gradle/wrapper/gradle-wrapper.jar rename to Android/gradle/wrapper/gradle-wrapper.jar diff --git a/Andorid/gradle/wrapper/gradle-wrapper.properties b/Android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from Andorid/gradle/wrapper/gradle-wrapper.properties rename to Android/gradle/wrapper/gradle-wrapper.properties diff --git a/Andorid/gradlew b/Android/gradlew similarity index 100% rename from Andorid/gradlew rename to Android/gradlew diff --git a/Andorid/gradlew.bat b/Android/gradlew.bat similarity index 100% rename from Andorid/gradlew.bat rename to Android/gradlew.bat diff --git a/Andorid/settings.gradle b/Android/settings.gradle similarity index 100% rename from Andorid/settings.gradle rename to Android/settings.gradle From fbf7ff8902fe6a5bd648471e86bd09734354454d Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 15:01:30 +0200 Subject: [PATCH 07/21] Fixed a Typo in the settings widget --- Android/app/src/main/res/layout/settings_fragment.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Android/app/src/main/res/layout/settings_fragment.xml b/Android/app/src/main/res/layout/settings_fragment.xml index 73ab6df..8a268a7 100644 --- a/Android/app/src/main/res/layout/settings_fragment.xml +++ b/Android/app/src/main/res/layout/settings_fragment.xml @@ -22,7 +22,7 @@ android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" - app:helperText="Format: 192.168.101:4321" + app:helperText="Format: 192.168.168.x:4321" android:layout_marginHorizontal="@dimen/horizontal_margin" > @@ -46,4 +46,4 @@ android:text="Save" /> - \ No newline at end of file + From 3f33442819915ea785ca80da2a77e1e9b4786c4c Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 15:06:23 +0200 Subject: [PATCH 08/21] Fixed typos and generally improved the README.md --- README.md | 63 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 5e6b7a0..b0a93c2 100644 --- a/README.md +++ b/README.md @@ -6,50 +6,55 @@ ## Overview ![Overview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/high_level_overview.png?raw=true) -## How to use +## How to Use You can either watch this video or follow the steps below. -### How to start live streaming -1. Start the Video server. By default the Video server launches 3 sockets, each acting as a server: -- WebSocket Server (runs on port 1234). -- MJPEG Server (runs on port 4444). -- Camera Server (runs on port 4321). + +### How to Start Live Streaming +1. Start the Video Server. By default, the Video Server launches 3 sockets, each acting as a server: + - WebSocket Server (runs on port 1234) + - MJPEG Server (runs on port 4444) + - Camera Server (runs on port 4321) 2. Install the app on your phone. -3. Navigate to app's settings screen and setup your Camera's server IP. For example `192.168.0.101:4321`. -4. Open the stream screen and click the Start streaming button. -5. Now your phone sends video data to your Camera Server. +3. Navigate to the app's settings screen and set up your camera server's IP. For example: `192.168.0.101:4321` +4. Open the stream screen and click the "Start streaming" button. +5. Your phone is now sending video data to your Camera Server. + --- -### Watching the stream -The stream can be watched from either your browser, the Web App or apps like VLC media player. + +### Watching the Stream +The stream can be watched from either your browser, the Web App, or apps like VLC Media Player. ### Browser -Open your favorite web browser and navigate to your MJPEG server's IP address. For example `http://192.168.0.101:4444` +Open your favorite web browser and navigate to your MJPEG Server's IP address. For example: +`http://192.168.0.101:4444` ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true) -### VLC meida player -Open the VLC media player, File -> Open Network -> Network and write your MJPEG's server IP address. For example `http://192.168.0.101:4444/` +### VLC Media Player +Open VLC Media Player. Go to **File → Open Network → Network** and enter your MJPEG Server's IP address. For example: +`http://192.168.0.101:4444/` ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true) + ### The Web App -1. Navigate to the Web app root directory and in your terminal execute `webpack serve`. -2. Open your browser and navigate to `http://localhost:8080/`. -3. Go to settings and enter your WebSocket server ip address. For example `192.168.0.101:1234`. -4. Go to the streaming page `http://localhost:8080/stream.html` and click the connect button. +1. Navigate to the Web App's root directory and execute `webpack serve` in your terminal. +2. Open your browser and go to `http://localhost:8080/`. +3. Go to the settings page and enter your WebSocket Server's IP address. For example: `192.168.0.101:1234` +4. Navigate to the streaming page at `http://localhost:8080/stream.html` and click the "Connect" button. ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true) -### Configuring the Web App's server -Note: This section is required only if you'd like to be able to take screenshots from the Web App. +### Configuring the Web App's Server +> **Note:** This section is only required if you'd like to take screenshots from the Web App. -1. Open the Web App Server project -2. Open index.js and edit the connection object to match your MySQL credentials. -3. Create the required tables by executing the SQL query located in `user.sql` -4. At the root directory execute `node index.js` in your terminal -5. You may have to update the IP that the Web App connects to. You can edit this IP in Web app's `stream.html` file (`BACKEND_URL` const variable) -6. Create a user through the Web App from `http://localhost:8080/register.html` -7. Take screenshots from `http://localhost:8080/stream.html` -8. View your screenshots at `http://localhost:8080/gallery.html` +1. Open the Web App's server project. +2. Open `index.js` and edit the connection object to match your MySQL credentials. +3. Create the required tables by executing the SQL query located in `user.sql`. +4. From the root directory, run `node index.js` in your terminal. +5. You may have to update the IP that the Web App connects to. You can edit this IP in the Web App's `stream.html` file (see the `BACKEND_URL` constant). +6. Create a user through the Web App at `http://localhost:8080/register.html`. +7. Take screenshots from `http://localhost:8080/stream.html`. +8. View your screenshots at `http://localhost:8080/gallery.html`. ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp_gallery.gif?raw=true) ---- From fc3050717ef8df4baf25fa9b55676f1850df99ae Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 15:16:43 +0200 Subject: [PATCH 09/21] Fixed some more Typos --- .../app/src/main/java/com/ipcamera/SettingsPreferences.kt | 4 ++-- Android/app/src/main/res/layout/settings_fragment.xml | 4 ++-- README.md | 8 ++++---- VideoServer/src/main/kotlin/CameraServer.kt | 8 ++++---- VideoServer/src/main/kotlin/MJpegServer.kt | 2 +- VideoServer/src/main/kotlin/ViewerWebSocketServer.kt | 4 ++-- WebApp/public/common.js | 6 +++--- WebApp/public/settings.html | 4 ++-- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt b/Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt index 94f5430..3370932 100644 --- a/Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt +++ b/Android/app/src/main/java/com/ipcamera/SettingsPreferences.kt @@ -19,6 +19,6 @@ class SettingsPreferences(context: Context) { } fun getIpAddress() : String? { - return sharedPreferences.getString(IP_KEY, "192.168.0.101:4321") + return sharedPreferences.getString(IP_KEY, "192.168.178.101:4321") } -} \ No newline at end of file +} diff --git a/Android/app/src/main/res/layout/settings_fragment.xml b/Android/app/src/main/res/layout/settings_fragment.xml index 8a268a7..565b592 100644 --- a/Android/app/src/main/res/layout/settings_fragment.xml +++ b/Android/app/src/main/res/layout/settings_fragment.xml @@ -22,7 +22,7 @@ android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" - app:helperText="Format: 192.168.168.x:4321" + app:helperText="Example: 192.168.168.x:4321" android:layout_marginHorizontal="@dimen/horizontal_margin" > @@ -30,7 +30,7 @@ android:id="@+id/edit_text_ip" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="Server IP Address" + android:hint="Enter server IP address" android:inputType="textNoSuggestions" /> diff --git a/README.md b/README.md index b0a93c2..a5bcb5a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You can either watch this video or follow the steps below. - Camera Server (runs on port 4321) 2. Install the app on your phone. -3. Navigate to the app's settings screen and set up your camera server's IP. For example: `192.168.0.101:4321` +3. Navigate to the app's settings screen and set up your camera server's IP. For example: `192.168.178.101:4321` 4. Open the stream screen and click the "Start streaming" button. 5. Your phone is now sending video data to your Camera Server. @@ -27,20 +27,20 @@ The stream can be watched from either your browser, the Web App, or apps like VL ### Browser Open your favorite web browser and navigate to your MJPEG Server's IP address. For example: -`http://192.168.0.101:4444` +`http://192.168.178.101:4444` ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true) ### VLC Media Player Open VLC Media Player. Go to **File → Open Network → Network** and enter your MJPEG Server's IP address. For example: -`http://192.168.0.101:4444/` +`http://192.168.178.101:4444/` ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true) ### The Web App 1. Navigate to the Web App's root directory and execute `webpack serve` in your terminal. 2. Open your browser and go to `http://localhost:8080/`. -3. Go to the settings page and enter your WebSocket Server's IP address. For example: `192.168.0.101:1234` +3. Go to the settings page and enter your WebSocket Server's IP address. For example: `192.168.178.101:1234` 4. Navigate to the streaming page at `http://localhost:8080/stream.html` and click the "Connect" button. ![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true) diff --git a/VideoServer/src/main/kotlin/CameraServer.kt b/VideoServer/src/main/kotlin/CameraServer.kt index 99ae31f..11e8574 100644 --- a/VideoServer/src/main/kotlin/CameraServer.kt +++ b/VideoServer/src/main/kotlin/CameraServer.kt @@ -44,9 +44,9 @@ class CameraServer { createFrameDelegatorThread().start() val thread = Thread() { - println("Camera server: Starting camera server") + println("CameraServer: Starting the cameraserver") - println("Camera server: Waiting for clients...") + println("CameraServer: Waiting for clients...") val client = server.accept() deviceConnected = true val connectionTime = System.currentTimeMillis() @@ -104,7 +104,7 @@ class CameraServer { iterator.forEach { listener -> listener.onAvailable(deviceOfflineImage) } - println("Device offline, sending \"Device offline image\"") + println("Device is offline - sending fallback image") Thread.sleep(1000 / 24) } continue @@ -140,4 +140,4 @@ class CameraServer { file.writeText(existingLines) } -} \ No newline at end of file +} diff --git a/VideoServer/src/main/kotlin/MJpegServer.kt b/VideoServer/src/main/kotlin/MJpegServer.kt index 285df72..c4d8015 100644 --- a/VideoServer/src/main/kotlin/MJpegServer.kt +++ b/VideoServer/src/main/kotlin/MJpegServer.kt @@ -68,4 +68,4 @@ class MJpegServer(private val viewerListener: ViewerConnectionListener) { val onFrameAvailable = clients.remove(client)!! viewerListener.onDisconnect(onFrameAvailable) } -} \ No newline at end of file +} diff --git a/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt b/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt index 6170b20..e4d3a20 100644 --- a/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt +++ b/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt @@ -16,7 +16,7 @@ class ViewerWebSocketServer(val connectionListener: Listener) { private val server = object: WebSocketServer(InetSocketAddress(1234)) { override fun onOpen(conn: WebSocket?, handshake: ClientHandshake?) { - println("ViewerWebSocketServer: onOpen") + println("ViewerWebSocketServer: New connection opened from ${conn?.remoteSocketAddress}") val listener = object: CameraServer.OnFrameAvailable { override fun onAvailable(frame: ByteArray) { @@ -54,4 +54,4 @@ class ViewerWebSocketServer(val connectionListener: Listener) { server.start() } -} \ No newline at end of file +} diff --git a/WebApp/public/common.js b/WebApp/public/common.js index e83dcbb..228e867 100644 --- a/WebApp/public/common.js +++ b/WebApp/public/common.js @@ -1,4 +1,4 @@ -const BACKEND_URL = 'http://192.168.0.101:3000'; +const BACKEND_URL = 'http://192.168.178.101:3000'; let username = '' let password = '' @@ -13,7 +13,7 @@ function getServerIp() { const storedIp = localStorage.getItem("server_ip"); if (storedIp == null) { - return "192.168.0.101:1234"; + return "192.168.178.101:1234"; } else { return storedIp } @@ -40,4 +40,4 @@ function setAuthorization(username, password) { function clearAuthorization() { localStorage.setItem('auth', '') -} \ No newline at end of file +} diff --git a/WebApp/public/settings.html b/WebApp/public/settings.html index ecd343c..3982291 100644 --- a/WebApp/public/settings.html +++ b/WebApp/public/settings.html @@ -28,7 +28,7 @@

IP Camera

IP Camera - \ No newline at end of file + From 01d48812aac401e4990e40760e17035c17d59bbb Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 15:23:38 +0200 Subject: [PATCH 10/21] Reformated the README.md --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5bcb5a..fc85620 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ # IP Camera -![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/preview.gif?raw=true) -[Fullscreen](https://youtu.be/NtQ_Al-56Qs) +[![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/preview.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs) ## Overview + ![Overview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/high_level_overview.png?raw=true) ## How to Use -You can either watch this video or follow the steps below. + +You can either watch the video above or follow the steps below. ### How to Start Live Streaming + 1. Start the Video Server. By default, the Video Server launches 3 sockets, each acting as a server: - WebSocket Server (runs on port 1234) - MJPEG Server (runs on port 4444) - Camera Server (runs on port 4321) - 2. Install the app on your phone. 3. Navigate to the app's settings screen and set up your camera server's IP. For example: `192.168.178.101:4321` 4. Open the stream screen and click the "Start streaming" button. @@ -23,29 +24,34 @@ You can either watch this video or follow the steps below. --- ### Watching the Stream + The stream can be watched from either your browser, the Web App, or apps like VLC Media Player. ### Browser + Open your favorite web browser and navigate to your MJPEG Server's IP address. For example: `http://192.168.178.101:4444` -![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true) +[![Browser Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/browser.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs) ### VLC Media Player + Open VLC Media Player. Go to **File → Open Network → Network** and enter your MJPEG Server's IP address. For example: `http://192.168.178.101:4444/` -![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true) +[![VLC Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/vlc.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs) ### The Web App + 1. Navigate to the Web App's root directory and execute `webpack serve` in your terminal. 2. Open your browser and go to `http://localhost:8080/`. 3. Go to the settings page and enter your WebSocket Server's IP address. For example: `192.168.178.101:1234` 4. Navigate to the streaming page at `http://localhost:8080/stream.html` and click the "Connect" button. -![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true) +[![Web App Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs) ### Configuring the Web App's Server + > **Note:** This section is only required if you'd like to take screenshots from the Web App. 1. Open the Web App's server project. @@ -57,4 +63,5 @@ Open VLC Media Player. Go to **File → Open Network → Network** and enter you 7. Take screenshots from `http://localhost:8080/stream.html`. 8. View your screenshots at `http://localhost:8080/gallery.html`. -![Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp_gallery.gif?raw=true) +[![Web App Gallery Preview](https://github.com/BalioFVFX/IP-Camera/blob/main/media/webapp_gallery.gif?raw=true)](https://youtu.be/NtQ_Al-56Qs) + From 76b92a332f0cf67a43462d55b5351838fdeca5e9 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:18:51 +0200 Subject: [PATCH 11/21] Security Patch based on BalioFVFX/IP-Camera#5 --- WebAppServer/index.js | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/WebAppServer/index.js b/WebAppServer/index.js index 5ba2eb2..8d3745a 100644 --- a/WebAppServer/index.js +++ b/WebAppServer/index.js @@ -16,6 +16,14 @@ const connection = mysql.createConnection({ app.use(express.json()); app.use(cors()); +app.use(helmet()); + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100 +}); + +app.use(limiter); app.post('/register', async function (req, res) { res.setHeader('Access-Control-Allow-Origin', '*') @@ -25,14 +33,14 @@ app.post('/register', async function (req, res) { return } - const username = mysql.escape(req.body.username); + const username = req.body.username; const password = req.body.password; - const hashPassword = mysql.escape(await bcrypt.hash(password, 10)); + const hashPassword = await bcrypt.hash(password, 10); - const sql = "INSERT INTO user (username, password) VALUES (" + username + "," + hashPassword + ")"; + const sql = "INSERT INTO user (username, password) VALUES (?, ?)"; - connection.query(sql, function (err, sqlRes) { + connection.query(sql, [username, hashPassword], function (err, sqlRes) { if (err == null) { res.sendStatus(201); } else { @@ -63,10 +71,12 @@ app.post('/screenshot', async (req, res) => { return; } - const username = mysql.escape(getUsernameFromAuth(req)); + const username = getUsernameFromAuth(req); - fs.mkdir(username, function () { - const stream = fs.createWriteStream(username + '/' + Date.now().toString() + '.jpeg') + const sanitizedUsername = username.replace(/[^a-zA-Z0-9]/g, ''); + + fs.mkdir(sanitizedUsername, { recursive: true }, function () { + const stream = fs.createWriteStream(sanitizedUsername + '/' + Date.now().toString() + '.jpeg') stream.write(new Uint8Array(req.body)) }); @@ -81,9 +91,11 @@ app.get('/gallery', async (req, res) => { return; } - const username = mysql.escape(getUsernameFromAuth(req)); + const username = getUsernameFromAuth(req); + + const sanitizedUsername = username.replace(/[^a-zA-Z0-9]/g, ''); - fs.readdir(username, (err, files) => { + fs.readdir(sanitizedUsername, (err, files) => { if (err) { res.sendStatus(404); return @@ -92,7 +104,7 @@ app.get('/gallery', async (req, res) => { const arr = []; files.forEach(file => { - const data = fs.readFileSync(username + '/' + file) + const data = fs.readFileSync(sanitizedUsername + '/' + file) arr.push([...data]); }); @@ -115,7 +127,7 @@ async function isAuthorized(req) { } login = mysql.escape(login); - connection.query('select password from user where username = ' + login, async function (err, res) { + connection.query('select password from user where username = ?', [login], async function (err, res) { if (err == null) { const dbPassword = res[0].password; @@ -137,4 +149,4 @@ async function isAuthorized(req) { function getUsernameFromAuth(req) { const b64auth = (req.headers.authorization || '').split(' ')[1] || '' return Buffer.from(b64auth, 'base64').toString().split(':')[0]; -} \ No newline at end of file +} From b208a585b4394487000ef61774a8939e8a35cde9 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:28:29 +0200 Subject: [PATCH 12/21] Added Docker Support by BalioFVFX/IP-Camera#3 --- .../gradle/wrapper/gradle-wrapper.properties | 6 +-- VideoServer/Dockerfile | 51 +++++++++++++++++++ VideoServer/build.gradle | 34 ++++++------- .../gradle/wrapper/gradle-wrapper.properties | 2 + 4 files changed, 71 insertions(+), 22 deletions(-) create mode 100644 VideoServer/Dockerfile diff --git a/Android/gradle/wrapper/gradle-wrapper.properties b/Android/gradle/wrapper/gradle-wrapper.properties index e2847c8..b5c11a6 100644 --- a/Android/gradle/wrapper/gradle-wrapper.properties +++ b/Android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME +distributionPath=wrapper/dists zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/VideoServer/Dockerfile b/VideoServer/Dockerfile new file mode 100644 index 0000000..7c8ee80 --- /dev/null +++ b/VideoServer/Dockerfile @@ -0,0 +1,51 @@ +# Stage 1: Build the application using Eclipse Temurin JDK 22 +FROM eclipse-temurin:22-jdk AS build + +# Set working directory inside the build container +WORKDIR /app + +# Copy Gradle wrapper scripts and related files for build setup +COPY gradlew /app/gradlew +COPY gradlew.bat /app/gradlew.bat +COPY gradle/wrapper/gradle-wrapper.jar /app/gradle/wrapper/gradle-wrapper.jar +COPY gradle/wrapper/gradle-wrapper.properties /app/gradle/wrapper/gradle-wrapper.properties + +# Copy Gradle build files (build.gradle, settings.gradle) +COPY build.gradle settings.gradle /app/ + +# Copy application source code to container +COPY src /app/src + +# Ensure Gradle wrapper has execute permission +RUN chmod +x gradlew + +# Execute Gradle build to clean and create a shadow (fat) JAR +RUN ./gradlew clean shadowJar + +# Stage 2: Prepare runtime image +FROM eclipse-temurin:22-jdk + +# Set working directory inside the runtime container +WORKDIR /app + +# Copy the built shadow JAR from the build stage +COPY --from=build /app/build/libs/VideoServer-1.0-SNAPSHOT.jar /app/VideoServer.jar + +# Copy static asset required by the application +COPY device_offline.jpg /app/device_offline.jpg + +# Define container start command to run the JAR +ENTRYPOINT ["java", "-jar", "/app/VideoServer.jar"] + +# Expose ports used by the application services +# 1234: WebSocket Server +# 4444: MJPEG Streaming Server +# 4321: Camera Server +EXPOSE 1234 +EXPOSE 4444 +EXPOSE 4321 + +# Docker usage instructions: +# Build image: sudo docker build -t videoserver:latest . +# Run container detached with port mappings: +# sudo docker run -d -p 1234:1234 -p 4444:4444 -p 4321:4321 videoserver diff --git a/VideoServer/build.gradle b/VideoServer/build.gradle index aed851a..50afe7d 100644 --- a/VideoServer/build.gradle +++ b/VideoServer/build.gradle @@ -1,9 +1,9 @@ plugins { - id 'org.jetbrains.kotlin.jvm' version '1.7.21' + id 'org.jetbrains.kotlin.jvm' version '2.0.0' id 'com.github.johnrengelman.shadow' version '8.1.1' } -group = 'org.example' +group = 'com.videoserver' version = '1.0-SNAPSHOT' repositories { @@ -16,31 +16,29 @@ dependencies { testImplementation 'org.jetbrains.kotlin:kotlin-test' } -test { - useJUnitPlatform() -} - -compileKotlin { - kotlinOptions.jvmTarget = '1.8' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(22) + } } -compileTestKotlin { - kotlinOptions.jvmTarget = '1.8' +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + jvmTarget = '22' + } } -jar { - manifest { - attributes( - 'Main-Class': 'MainKt' - ) - } +test { + useJUnitPlatform() } shadowJar { archiveBaseName.set('VideoServer') - archiveClassifier.set('') archiveVersion.set('1.0-SNAPSHOT') + archiveClassifier.set('') manifest { - attributes('Main-Class': 'MainKt') + attributes 'Main-Class': 'com.videoserver.MainKt' } } + +build.dependsOn(shadowJar) diff --git a/VideoServer/gradle/wrapper/gradle-wrapper.properties b/VideoServer/gradle/wrapper/gradle-wrapper.properties index 81aa1c0..e2847c8 100644 --- a/VideoServer/gradle/wrapper/gradle-wrapper.properties +++ b/VideoServer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 232661cee362a6245e6cc330b34807278d13558c Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:31:32 +0200 Subject: [PATCH 13/21] Tried to make it more optimized --- .../main/java/com/ipcamera/StreamActivity.kt | 369 +++++++++--------- 1 file changed, 179 insertions(+), 190 deletions(-) diff --git a/Android/app/src/main/java/com/ipcamera/StreamActivity.kt b/Android/app/src/main/java/com/ipcamera/StreamActivity.kt index 67ebb84..ee8b624 100644 --- a/Android/app/src/main/java/com/ipcamera/StreamActivity.kt +++ b/Android/app/src/main/java/com/ipcamera/StreamActivity.kt @@ -1,31 +1,35 @@ package com.ipcamera import android.annotation.SuppressLint -import android.graphics.ImageFormat +import android.graphics.SurfaceTexture import android.hardware.camera2.* -import android.media.ImageReader +import android.media.MediaCodec +import android.media.MediaCodecInfo +import android.media.MediaFormat import android.os.Bundle import android.os.Handler -import android.os.Looper +import android.os.HandlerThread import android.util.Log import android.util.Range -import android.view.SurfaceHolder +import android.view.Surface import android.widget.Toast import androidx.appcompat.app.AppCompatActivity -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.view.updateLayoutParams import com.ipcamera.databinding.StreamActivityBinding import java.io.DataOutputStream import java.net.Socket -import java.util.concurrent.ConcurrentLinkedQueue +import java.nio.ByteBuffer import java.util.concurrent.Executors - class StreamActivity : AppCompatActivity() { private lateinit var binding: StreamActivityBinding - private val TAG = "StreamTag" - private lateinit var imageReader: ImageReader + private val TAG = "StreamActivity" + + private var cameraDevice: CameraDevice? = null + private var captureSession: CameraCaptureSession? = null + + private var mediaCodec: MediaCodec? = null + private var encoderSurface: Surface? = null @Volatile private var isStreaming = false @@ -33,234 +37,219 @@ class StreamActivity : AppCompatActivity() { @Volatile private var socket: Socket? = null - private val executor = Executors.newSingleThreadExecutor() + private val socketExecutor = Executors.newSingleThreadExecutor() - @SuppressLint("MissingPermission") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + private lateinit var cameraHandler: Handler + private lateinit var cameraThread: HandlerThread - EdgeToEdge.setDecorFitsSystemWindows( - window = window, - fitSystemWindows = false, - ) + private val TIMEOUT_US = 10000L - EdgeToEdge.enableImmersiveMode(window = window) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) binding = StreamActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - EdgeToEdge.setInsetsHandler( - root = binding.root, - handler = StreamActivityInsetsHandler { systemBarInsets -> + startCameraThread() - binding.btnSave.updateLayoutParams { - bottomMargin += systemBarInsets.bottom - } - - binding.tvStatus.updateLayoutParams { - topMargin += systemBarInsets.top - } + binding.btnSave.setOnClickListener { + if (isStreaming) { + stopStreaming() + } else { + startStreaming() } - ) - - val cameraManager = getSystemService(CameraManager::class.java) - - val cameraId = cameraManager.cameraIdList[0] - - val surfaceView = binding.surfaceView + } + } - val mainHandler = Handler(Looper.getMainLooper()) + private fun startCameraThread() { + cameraThread = HandlerThread("CameraBackground").also { it.start() } + cameraHandler = Handler(cameraThread.looper) + } - imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 3) + private fun stopCameraThread() { + cameraThread.quitSafely() + try { + cameraThread.join() + } catch (e: InterruptedException) { + e.printStackTrace() + } + } - val queue = ConcurrentLinkedQueue() + @SuppressLint("MissingPermission") + private fun startStreaming() { + val ipAddress = SettingsPreferences(this.applicationContext).getIpAddress() + if (ipAddress.isNullOrBlank()) { + Toast.makeText(this, "Invalid IP address", Toast.LENGTH_SHORT).show() + return + } - surfaceView.holder.setFixedSize(1920, 1080) + isStreaming = true + binding.tvStatus.text = "Connecting..." - val ipAddress = SettingsPreferences(this.applicationContext).getIpAddress()!! + setupEncoder() - binding.btnSave.setOnClickListener { - if (isStreaming) { - isStreaming = !isStreaming + openCameraAndStartSession() - executor.execute { - socket?.close() - socket = null + socketExecutor.execute { + try { + val (ip, portStr) = ipAddress.split(":") + val port = portStr.toInt() + socket = Socket(ip, port) + val socketWriter = DataOutputStream(socket!!.getOutputStream()) - mainHandler.post { - binding.tvStatus.text = "Status: Disconnected" - binding.btnSave.text = "Start streaming" - } + runOnUiThread { + binding.tvStatus.text = "Streaming to: $ipAddress" + binding.btnSave.text = "Stop streaming" } + val codec = mediaCodec ?: throw IllegalStateException("Encoder not initialized") + val bufferInfo = MediaCodec.BufferInfo() - } else { - binding.tvStatus.text = "Connecting..." + while (isStreaming) { + val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US) + if (outputBufferIndex >= 0) { + val encodedBuffer = codec.getOutputBuffer(outputBufferIndex) + encodedBuffer?.let { + val outData = ByteArray(bufferInfo.size) + it.get(outData) + it.clear() - executor.execute { - try { - val ip = ipAddress.split(":")[0] - val port = ipAddress.split(":")[1] - - socket = Socket(ip, port.toInt()) + socketWriter.writeInt(outData.size) + socketWriter.write(outData) + socketWriter.flush() - mainHandler.post { - binding.tvStatus.text = "Streaming to: $ipAddress" - binding.btnSave.text = "Stop streaming" + Log.d(TAG, "Sent frame size=${outData.size} bytes") } + codec.releaseOutputBuffer(outputBufferIndex, false) + } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + val newFormat = codec.outputFormat + Log.d(TAG, "Encoder output format changed: $newFormat") + } else { - isStreaming = !isStreaming - - val socketWriter = DataOutputStream(socket!!.getOutputStream()) - val stack = ArrayDeque(10) - var size = 0 - var start = 0L - - while (isStreaming) { - val frame = try { - queue.remove() - } catch (ex: java.util.NoSuchElementException) { - Log.d(TAG, "Empty queue") - continue - } - - Log.d(TAG, "Buffer size: ${queue.size}") - start = System.currentTimeMillis() - - socketWriter.writeInt(frame.size) - socketWriter.write(frame) + } + } - socketWriter.flush() - Log.d(TAG, "Sent to server: ${frame.size} bytes") - Log.d(TAG, "Elapsed to send: ${System.currentTimeMillis() - start}") - } + socketWriter.close() + socket?.close() + socket = null - } catch (exception: java.lang.Exception) { - exception.printStackTrace() - - socket?.close() - socket = null - isStreaming = false - - mainHandler.post { - Toast.makeText( - this, - "Could not connect to: $ipAddress", - Toast.LENGTH_LONG - ) - .show() - binding.tvStatus.text = "Status: Disconnected" - binding.btnSave.text = "Start streaming" - } - } + } catch (e: Exception) { + e.printStackTrace() + runOnUiThread { + Toast.makeText(this, "Could not connect or stream: $e", Toast.LENGTH_LONG).show() + binding.tvStatus.text = "Status: Disconnected" + binding.btnSave.text = "Start streaming" } + stopStreaming() } } + } - surfaceView.holder.addCallback(object : SurfaceHolder.Callback { - override fun surfaceCreated(holder: SurfaceHolder) { - Log.d(TAG, "surfaceCreated: ") - - imageReader.setOnImageAvailableListener(object : - ImageReader.OnImageAvailableListener { - override fun onImageAvailable(reader: ImageReader?) { - - val image = reader?.acquireNextImage() ?: return - - if (!isStreaming) { - image.close() - return - } - - val buffer = image.planes[0].buffer - - if (buffer.hasArray()) { - queue.add(buffer.array()) - } else { - val array = ByteArray(buffer.remaining()) - buffer.get(array) - - queue.add(array) - } - - image.close() - } - }, null) - - cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { - override fun onOpened(camera: CameraDevice) { - Log.d(TAG, "onOpened") + private fun stopStreaming() { + isStreaming = false - val captureRequest = - camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + socketExecutor.execute { + try { + socket?.close() + socket = null + } catch (e: Exception) { + e.printStackTrace() + } + } - captureRequest.set(CaptureRequest.JPEG_QUALITY, 20) - val range = Range(24, 24) + closeCamera() + releaseEncoder() - captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, range) + runOnUiThread { + binding.tvStatus.text = "Status: Disconnected" + binding.btnSave.text = "Start streaming" + } + } - val callback = object : CameraCaptureSession.CaptureCallback() { + @SuppressLint("MissingPermission") + private fun openCameraAndStartSession() { + val cameraManager = getSystemService(CameraManager::class.java) ?: return + val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return - override fun onCaptureProgressed( - session: CameraCaptureSession, - request: CaptureRequest, - partialResult: CaptureResult, - ) { - super.onCaptureProgressed(session, request, partialResult) - } - } + cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + cameraDevice = camera - val captureSession = object : CameraCaptureSession.StateCallback() { - override fun onConfigured(session: CameraCaptureSession) { - captureRequest.addTarget(imageReader.surface) - captureRequest.addTarget(surfaceView.holder.surface) - session.setRepeatingRequest( - captureRequest.build(), - callback, - mainHandler - ) - } + val captureRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { + encoderSurface?.let { addTarget(it) } - override fun onConfigureFailed(session: CameraCaptureSession) { + set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(60, 60)) + } - } + camera.createCaptureSession(listOfNotNull(encoderSurface), object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + captureSession = session + try { + session.setRepeatingRequest(captureRequestBuilder.build(), null, cameraHandler) + } catch (e: CameraAccessException) { + e.printStackTrace() } - - - camera.createCaptureSession( - listOf( - surfaceView.holder.surface, - imageReader.surface - ), captureSession, mainHandler - ) - } - override fun onDisconnected(camera: CameraDevice) { - - } - - override fun onError(camera: CameraDevice, error: Int) { - + override fun onConfigureFailed(session: CameraCaptureSession) { + Log.e(TAG, "Camera capture session configuration failed") } - - }, mainHandler) + }, cameraHandler) } - override fun surfaceChanged( - holder: SurfaceHolder, - format: Int, - width: Int, - height: Int - ) { + override fun onDisconnected(camera: CameraDevice) { + camera.close() + cameraDevice = null + } + override fun onError(camera: CameraDevice, error: Int) { + Log.e(TAG, "Camera error: $error") + camera.close() + cameraDevice = null } + }, cameraHandler) + } - override fun surfaceDestroyed(holder: SurfaceHolder) { + private fun closeCamera() { + captureSession?.close() + captureSession = null + cameraDevice?.close() + cameraDevice = null + } + private fun setupEncoder() { + try { + val format = MediaFormat.createVideoFormat("video/avc", 1920, 1080).apply { + setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) + setInteger(MediaFormat.KEY_BIT_RATE, 5_000_000) + setInteger(MediaFormat.KEY_FRAME_RATE, 60) + setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) } + mediaCodec = MediaCodec.createEncoderByType("video/avc") + mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) + encoderSurface = mediaCodec?.createInputSurface() + mediaCodec?.start() + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(this, "Encoder initialization failed", Toast.LENGTH_LONG).show() + } + } + + private fun releaseEncoder() { + try { + mediaCodec?.stop() + mediaCodec?.release() + mediaCodec = null + encoderSurface?.release() + encoderSurface = null + } catch (e: Exception) { + e.printStackTrace() + } + } - }) + override fun onDestroy() { + super.onDestroy() + stopStreaming() + stopCameraThread() } -} \ No newline at end of file +} From da72870e806803af58f2a6c17d40c70834245721 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:33:17 +0200 Subject: [PATCH 14/21] Switched from JDK 17 to JDK 22 for VideoServer building --- .github/workflows/android-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index f636bbf..352e636 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -31,7 +31,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'temurin' - java-version: '17' + java-version: '22' - name: Make gradlew executable (VideoServer) run: chmod +x gradlew From a75dfea5fb00efa64c2af5c232b411a6404d1c55 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:35:28 +0200 Subject: [PATCH 15/21] Optimized Workflow File --- .github/workflows/android-build.yml | 93 +++++++++++++++++------------ 1 file changed, 56 insertions(+), 37 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 352e636..bbc9c23 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -6,43 +6,62 @@ on: - '**' jobs: - build: + build-android: runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '22' + + - name: Cache Android Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + Android/.gradle + key: ${{ runner.os }}-android-gradle-${{ hashFiles('Android/gradlew', 'Android/build.gradle', 'Android/app/build.gradle') }} + + - name: Build APK + run: chmod +x gradlew && ./gradlew assembleDebug + working-directory: ./Android + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: debug-apk + path: Android/app/build/outputs/apk/debug/app-debug.apk + build-videoserver: + runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Make gradlew executable (Android) - run: chmod +x gradlew - working-directory: ./Android - - - name: Build APK - run: ./gradlew assembleDebug - working-directory: ./Android - - - name: Upload APK artifact - uses: actions/upload-artifact@v4 - with: - name: debug-apk - path: Android/app/build/outputs/apk/debug/app-debug.apk - - - name: Set up JDK - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '22' - - - name: Make gradlew executable (VideoServer) - run: chmod +x gradlew - working-directory: ./VideoServer - - - name: Build VideoServer - run: ./gradlew build - working-directory: ./VideoServer - - - name: Upload VideoServer jar artifact - uses: actions/upload-artifact@v4 - with: - name: video-server-jar - path: VideoServer/build/libs/*.jar + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '22' + + - name: Cache VideoServer Gradle + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + VideoServer/.gradle + key: ${{ runner.os }}-videoserver-gradle-${{ hashFiles('VideoServer/gradlew', 'VideoServer/build.gradle') }} + + - name: Build VideoServer + run: chmod +x gradlew && ./gradlew build + working-directory: ./VideoServer + + - name: Upload VideoServer jar artifact + uses: actions/upload-artifact@v4 + with: + name: video-server-jar + path: VideoServer/build/libs/*.jar From 0a1fd4d000ffc344eada1ed7aedaf071ff402e58 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 17:46:42 +0200 Subject: [PATCH 16/21] Modified workflow to build shadowjar --- .github/workflows/android-build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index bbc9c23..880f9b0 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -56,12 +56,12 @@ jobs: VideoServer/.gradle key: ${{ runner.os }}-videoserver-gradle-${{ hashFiles('VideoServer/gradlew', 'VideoServer/build.gradle') }} - - name: Build VideoServer - run: chmod +x gradlew && ./gradlew build + - name: Build VideoServer Shadow JAR + run: chmod +x gradlew && ./gradlew shadowJar working-directory: ./VideoServer - - name: Upload VideoServer jar artifact + - name: Upload VideoServer shadow JAR artifact uses: actions/upload-artifact@v4 with: - name: video-server-jar - path: VideoServer/build/libs/*.jar + name: video-server-shadow-jar + path: VideoServer/build/libs/*-all.jar From 0db3c59436bbc533db1b1de733d7cb1195479cf2 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 18:11:30 +0200 Subject: [PATCH 17/21] Updated the workflow to work again --- VideoServer/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VideoServer/build.gradle b/VideoServer/build.gradle index 50afe7d..4e82bdd 100644 --- a/VideoServer/build.gradle +++ b/VideoServer/build.gradle @@ -28,7 +28,7 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { } } -test { +tasks.named('test') { useJUnitPlatform() } From 96a601c2f992b634a1c37a5be935d5abaefabcb0 Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 18:14:44 +0200 Subject: [PATCH 18/21] Updated the workflow to work again --- .github/workflows/android-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml index 880f9b0..6e1def6 100644 --- a/.github/workflows/android-build.yml +++ b/.github/workflows/android-build.yml @@ -64,4 +64,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: video-server-shadow-jar - path: VideoServer/build/libs/*-all.jar + path: VideoServer/build/libs/*-SNAPSHOT.jar From 7ccf71f1700b539abe39525f035c00eccc0fb80f Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 18:29:47 +0200 Subject: [PATCH 19/21] QoL updates --- .../java/com/ipcamera/SettingsFragment.kt | 2 +- .../main/java/com/ipcamera/StreamActivity.kt | 358 +++++++++--------- VideoServer/src/main/kotlin/CameraServer.kt | 2 + VideoServer/src/main/kotlin/Frame.kt | 4 +- VideoServer/src/main/kotlin/MJpegServer.kt | 2 + VideoServer/src/main/kotlin/Main.kt | 8 +- .../main/kotlin/ViewerConnectionListener.kt | 4 +- .../src/main/kotlin/ViewerWebSocketServer.kt | 2 + 8 files changed, 200 insertions(+), 182 deletions(-) diff --git a/Android/app/src/main/java/com/ipcamera/SettingsFragment.kt b/Android/app/src/main/java/com/ipcamera/SettingsFragment.kt index 54d7f34..587320a 100644 --- a/Android/app/src/main/java/com/ipcamera/SettingsFragment.kt +++ b/Android/app/src/main/java/com/ipcamera/SettingsFragment.kt @@ -57,4 +57,4 @@ class SettingsFragment : Fragment() { activity?.onBackPressed() } } -} \ No newline at end of file +} diff --git a/Android/app/src/main/java/com/ipcamera/StreamActivity.kt b/Android/app/src/main/java/com/ipcamera/StreamActivity.kt index ee8b624..14de285 100644 --- a/Android/app/src/main/java/com/ipcamera/StreamActivity.kt +++ b/Android/app/src/main/java/com/ipcamera/StreamActivity.kt @@ -1,35 +1,29 @@ package com.ipcamera import android.annotation.SuppressLint -import android.graphics.SurfaceTexture +import android.graphics.ImageFormat import android.hardware.camera2.* -import android.media.MediaCodec -import android.media.MediaCodecInfo -import android.media.MediaFormat +import android.media.ImageReader import android.os.Bundle import android.os.Handler -import android.os.HandlerThread +import android.os.Looper import android.util.Log import android.util.Range -import android.view.Surface +import android.view.SurfaceHolder import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import com.ipcamera.databinding.StreamActivityBinding import java.io.DataOutputStream import java.net.Socket -import java.nio.ByteBuffer +import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.Executors + class StreamActivity : AppCompatActivity() { private lateinit var binding: StreamActivityBinding - private val TAG = "StreamActivity" - - private var cameraDevice: CameraDevice? = null - private var captureSession: CameraCaptureSession? = null - - private var mediaCodec: MediaCodec? = null - private var encoderSurface: Surface? = null + private val TAG = "StreamTag" + private lateinit var imageReader: ImageReader @Volatile private var isStreaming = false @@ -37,219 +31,227 @@ class StreamActivity : AppCompatActivity() { @Volatile private var socket: Socket? = null - private val socketExecutor = Executors.newSingleThreadExecutor() - - private lateinit var cameraHandler: Handler - private lateinit var cameraThread: HandlerThread - - private val TIMEOUT_US = 10000L + private val executor = Executors.newSingleThreadExecutor() + @SuppressLint("MissingPermission") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = StreamActivityBinding.inflate(layoutInflater) + setContentView(binding.root) - startCameraThread() + val cameraManager = getSystemService(CameraManager::class.java) + + val cameraId = cameraManager.cameraIdList[0] + + val surfaceView = binding.surfaceView + + val mainHandler = Handler(Looper.getMainLooper()) + + imageReader = ImageReader.newInstance(1280, 720, ImageFormat.JPEG, 3) + + val queue = ConcurrentLinkedQueue() + + surfaceView.holder.setFixedSize(1920, 1080) + + val ipAddress = SettingsPreferences(this.applicationContext).getIpAddress()!! binding.btnSave.setOnClickListener { if (isStreaming) { - stopStreaming() + isStreaming = !isStreaming + + executor.execute { + socket?.close() + socket = null + + mainHandler.post { + binding.tvStatus.text = "Status: Disconnected" + binding.btnSave.text = "Start streaming" + } + } + + } else { - startStreaming() - } - } - } + binding.tvStatus.text = "Connecting..." - private fun startCameraThread() { - cameraThread = HandlerThread("CameraBackground").also { it.start() } - cameraHandler = Handler(cameraThread.looper) - } + executor.execute { + try { + val ip = ipAddress.split(":")[0] + val port = ipAddress.split(":")[1] - private fun stopCameraThread() { - cameraThread.quitSafely() - try { - cameraThread.join() - } catch (e: InterruptedException) { - e.printStackTrace() - } - } + socket = Socket(ip, port.toInt()) + socket?.sendBufferSize = 900000000 + socket?.receiveBufferSize = 900000000 - @SuppressLint("MissingPermission") - private fun startStreaming() { - val ipAddress = SettingsPreferences(this.applicationContext).getIpAddress() - if (ipAddress.isNullOrBlank()) { - Toast.makeText(this, "Invalid IP address", Toast.LENGTH_SHORT).show() - return - } + mainHandler.post { + binding.tvStatus.text = "Streaming to: $ipAddress" + binding.btnSave.text = "Stop streaming" + } - isStreaming = true - binding.tvStatus.text = "Connecting..." + isStreaming = !isStreaming - setupEncoder() + val socketWriter = DataOutputStream(socket!!.getOutputStream()) + val stack = ArrayDeque(10) + var size = 0 + var start = 0L - openCameraAndStartSession() + while (isStreaming) { + val frame = try { + queue.remove() + } catch (ex: java.util.NoSuchElementException) { + Log.d(TAG, "Empty queue") + continue + } - socketExecutor.execute { - try { - val (ip, portStr) = ipAddress.split(":") - val port = portStr.toInt() - socket = Socket(ip, port) - val socketWriter = DataOutputStream(socket!!.getOutputStream()) + Log.d(TAG, "Buffer size: ${queue.size}") + start = System.currentTimeMillis() + size = frame.size - runOnUiThread { - binding.tvStatus.text = "Streaming to: $ipAddress" - binding.btnSave.text = "Stop streaming" - } + while (size > 0) { + stack.addLast(size % 10) + size /= 10 + } - val codec = mediaCodec ?: throw IllegalStateException("Encoder not initialized") - val bufferInfo = MediaCodec.BufferInfo() + socketWriter.writeByte(stack.size) - while (isStreaming) { - val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US) - if (outputBufferIndex >= 0) { - val encodedBuffer = codec.getOutputBuffer(outputBufferIndex) - encodedBuffer?.let { - val outData = ByteArray(bufferInfo.size) - it.get(outData) - it.clear() + while (stack.isNotEmpty()) { + socketWriter.writeByte(stack.removeLast()) + } - socketWriter.writeInt(outData.size) - socketWriter.write(outData) - socketWriter.flush() + socketWriter.write(frame) - Log.d(TAG, "Sent frame size=${outData.size} bytes") + socketWriter.flush() + Log.d(TAG, "Sent to server: ${frame.size} bytes") + Log.d(TAG, "Elapsed to send: ${System.currentTimeMillis() - start}") } - codec.releaseOutputBuffer(outputBufferIndex, false) - } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - val newFormat = codec.outputFormat - Log.d(TAG, "Encoder output format changed: $newFormat") - } else { + } catch (exception: java.lang.Exception) { + exception.printStackTrace() + + socket?.close() + socket = null + isStreaming = false + + mainHandler.post { + Toast.makeText( + this, + "Could not connect to: $ipAddress", + Toast.LENGTH_LONG + ) + .show() + binding.tvStatus.text = "Status: Disconnected" + binding.btnSave.text = "Start streaming" + } } } - - socketWriter.close() - socket?.close() - socket = null - - } catch (e: Exception) { - e.printStackTrace() - runOnUiThread { - Toast.makeText(this, "Could not connect or stream: $e", Toast.LENGTH_LONG).show() - binding.tvStatus.text = "Status: Disconnected" - binding.btnSave.text = "Start streaming" - } - stopStreaming() } } - } - private fun stopStreaming() { - isStreaming = false + surfaceView.holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + Log.d(TAG, "surfaceCreated: ") - socketExecutor.execute { - try { - socket?.close() - socket = null - } catch (e: Exception) { - e.printStackTrace() - } - } + imageReader.setOnImageAvailableListener(object : + ImageReader.OnImageAvailableListener { + override fun onImageAvailable(reader: ImageReader?) { - closeCamera() - releaseEncoder() + val image = reader?.acquireNextImage() ?: return - runOnUiThread { - binding.tvStatus.text = "Status: Disconnected" - binding.btnSave.text = "Start streaming" - } - } + if (!isStreaming) { + image.close() + return + } - @SuppressLint("MissingPermission") - private fun openCameraAndStartSession() { - val cameraManager = getSystemService(CameraManager::class.java) ?: return - val cameraId = cameraManager.cameraIdList.firstOrNull() ?: return + val buffer = image.planes[0].buffer + buffer.rewind() - cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { - override fun onOpened(camera: CameraDevice) { - cameraDevice = camera + val arr = ByteArray(buffer.capacity()) - val captureRequestBuilder = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply { - encoderSurface?.let { addTarget(it) } + var i = 0 + while (buffer.hasRemaining()) { + arr[i++] = buffer.get() + } - set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, Range(60, 60)) - } + image.close() + queue.add(arr) + } + }, null) + + cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + Log.d(TAG, "onOpened") + + val captureRequest = + camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + + captureRequest.set(CaptureRequest.JPEG_QUALITY, 20) + val range = Range(24, 24) + + captureRequest.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, range) - camera.createCaptureSession(listOfNotNull(encoderSurface), object : CameraCaptureSession.StateCallback() { - override fun onConfigured(session: CameraCaptureSession) { - captureSession = session - try { - session.setRepeatingRequest(captureRequestBuilder.build(), null, cameraHandler) - } catch (e: CameraAccessException) { - e.printStackTrace() + val callback = object : CameraCaptureSession.CaptureCallback() { + + override fun onCaptureProgressed( + session: CameraCaptureSession, + request: CaptureRequest, + partialResult: CaptureResult + ) { + super.onCaptureProgressed(session, request, partialResult) + Log.d(TAG, "onCaptureProgressed: ") + } } + + val captureSession = object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + captureRequest.addTarget(imageReader.surface) + captureRequest.addTarget(surfaceView.holder.surface) + session.setRepeatingRequest( + captureRequest.build(), + callback, + mainHandler + ) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + + } + } + + + camera.createCaptureSession( + listOf( + surfaceView.holder.surface, + imageReader.surface + ), captureSession, mainHandler + ) + } - override fun onConfigureFailed(session: CameraCaptureSession) { - Log.e(TAG, "Camera capture session configuration failed") + override fun onDisconnected(camera: CameraDevice) { + } - }, cameraHandler) - } - override fun onDisconnected(camera: CameraDevice) { - camera.close() - cameraDevice = null - } + override fun onError(camera: CameraDevice, error: Int) { + + } - override fun onError(camera: CameraDevice, error: Int) { - Log.e(TAG, "Camera error: $error") - camera.close() - cameraDevice = null + }, mainHandler) } - }, cameraHandler) - } - private fun closeCamera() { - captureSession?.close() - captureSession = null - cameraDevice?.close() - cameraDevice = null - } + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { - private fun setupEncoder() { - try { - val format = MediaFormat.createVideoFormat("video/avc", 1920, 1080).apply { - setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface) - setInteger(MediaFormat.KEY_BIT_RATE, 5_000_000) - setInteger(MediaFormat.KEY_FRAME_RATE, 60) - setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) } - mediaCodec = MediaCodec.createEncoderByType("video/avc") - mediaCodec?.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) - encoderSurface = mediaCodec?.createInputSurface() - mediaCodec?.start() - } catch (e: Exception) { - e.printStackTrace() - Toast.makeText(this, "Encoder initialization failed", Toast.LENGTH_LONG).show() - } - } - private fun releaseEncoder() { - try { - mediaCodec?.stop() - mediaCodec?.release() - mediaCodec = null - encoderSurface?.release() - encoderSurface = null - } catch (e: Exception) { - e.printStackTrace() - } - } + override fun surfaceDestroyed(holder: SurfaceHolder) { + + } - override fun onDestroy() { - super.onDestroy() - stopStreaming() - stopCameraThread() + }) } } diff --git a/VideoServer/src/main/kotlin/CameraServer.kt b/VideoServer/src/main/kotlin/CameraServer.kt index 11e8574..fd0c949 100644 --- a/VideoServer/src/main/kotlin/CameraServer.kt +++ b/VideoServer/src/main/kotlin/CameraServer.kt @@ -1,3 +1,5 @@ +package com.videoserver + import java.io.DataInputStream import java.io.EOFException import java.io.File diff --git a/VideoServer/src/main/kotlin/Frame.kt b/VideoServer/src/main/kotlin/Frame.kt index 002ba4d..84b1d51 100644 --- a/VideoServer/src/main/kotlin/Frame.kt +++ b/VideoServer/src/main/kotlin/Frame.kt @@ -1 +1,3 @@ -data class Frame(val data: List) \ No newline at end of file +package com.videoserver + +data class Frame(val data: List) diff --git a/VideoServer/src/main/kotlin/MJpegServer.kt b/VideoServer/src/main/kotlin/MJpegServer.kt index c4d8015..ad34aea 100644 --- a/VideoServer/src/main/kotlin/MJpegServer.kt +++ b/VideoServer/src/main/kotlin/MJpegServer.kt @@ -1,3 +1,5 @@ +package com.videoserver + import java.io.IOException import java.net.ServerSocket import java.net.Socket diff --git a/VideoServer/src/main/kotlin/Main.kt b/VideoServer/src/main/kotlin/Main.kt index ddd869f..b23ee2d 100644 --- a/VideoServer/src/main/kotlin/Main.kt +++ b/VideoServer/src/main/kotlin/Main.kt @@ -1,3 +1,9 @@ +package com.videoserver + +import com.videoserver.CameraServer +import com.videoserver.ViewerWebSocketServer +import com.videoserver.MJpegServer + fun main(args: Array) { val cameraServer = CameraServer() @@ -27,4 +33,4 @@ fun main(args: Array) { viewerServer.start() mjpegServer.start() cameraServer.start() -} \ No newline at end of file +} diff --git a/VideoServer/src/main/kotlin/ViewerConnectionListener.kt b/VideoServer/src/main/kotlin/ViewerConnectionListener.kt index 4eed5a9..ae3b136 100644 --- a/VideoServer/src/main/kotlin/ViewerConnectionListener.kt +++ b/VideoServer/src/main/kotlin/ViewerConnectionListener.kt @@ -1,4 +1,6 @@ +package com.videoserver + interface ViewerConnectionListener { fun onConnect(onFrameAvailable: CameraServer.OnFrameAvailable) fun onDisconnect(onFrameAvailable: CameraServer.OnFrameAvailable) -} \ No newline at end of file +} diff --git a/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt b/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt index e4d3a20..c03f71b 100644 --- a/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt +++ b/VideoServer/src/main/kotlin/ViewerWebSocketServer.kt @@ -1,3 +1,5 @@ +package com.videoserver + import org.java_websocket.WebSocket import org.java_websocket.handshake.ClientHandshake import org.java_websocket.server.WebSocketServer From a66131fff15d3d2689096130b91168cba085e13e Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 18:34:50 +0200 Subject: [PATCH 20/21] Moved the offline image to make it work for packaged installs --- VideoServer/Dockerfile | 32 ++---------------- .../main/resources}/device_offline.jpg | Bin 2 files changed, 2 insertions(+), 30 deletions(-) rename VideoServer/{ => src/main/resources}/device_offline.jpg (100%) diff --git a/VideoServer/Dockerfile b/VideoServer/Dockerfile index 7c8ee80..f1bb481 100644 --- a/VideoServer/Dockerfile +++ b/VideoServer/Dockerfile @@ -1,51 +1,23 @@ -# Stage 1: Build the application using Eclipse Temurin JDK 22 FROM eclipse-temurin:22-jdk AS build -# Set working directory inside the build container WORKDIR /app -# Copy Gradle wrapper scripts and related files for build setup -COPY gradlew /app/gradlew -COPY gradlew.bat /app/gradlew.bat -COPY gradle/wrapper/gradle-wrapper.jar /app/gradle/wrapper/gradle-wrapper.jar -COPY gradle/wrapper/gradle-wrapper.properties /app/gradle/wrapper/gradle-wrapper.properties +COPY gradlew gradlew.bat settings.gradle build.gradle /app/ +COPY gradle /app/gradle -# Copy Gradle build files (build.gradle, settings.gradle) -COPY build.gradle settings.gradle /app/ - -# Copy application source code to container COPY src /app/src -# Ensure Gradle wrapper has execute permission RUN chmod +x gradlew - -# Execute Gradle build to clean and create a shadow (fat) JAR RUN ./gradlew clean shadowJar -# Stage 2: Prepare runtime image FROM eclipse-temurin:22-jdk -# Set working directory inside the runtime container WORKDIR /app -# Copy the built shadow JAR from the build stage COPY --from=build /app/build/libs/VideoServer-1.0-SNAPSHOT.jar /app/VideoServer.jar -# Copy static asset required by the application -COPY device_offline.jpg /app/device_offline.jpg - -# Define container start command to run the JAR ENTRYPOINT ["java", "-jar", "/app/VideoServer.jar"] -# Expose ports used by the application services -# 1234: WebSocket Server -# 4444: MJPEG Streaming Server -# 4321: Camera Server EXPOSE 1234 EXPOSE 4444 EXPOSE 4321 - -# Docker usage instructions: -# Build image: sudo docker build -t videoserver:latest . -# Run container detached with port mappings: -# sudo docker run -d -p 1234:1234 -p 4444:4444 -p 4321:4321 videoserver diff --git a/VideoServer/device_offline.jpg b/VideoServer/src/main/resources/device_offline.jpg similarity index 100% rename from VideoServer/device_offline.jpg rename to VideoServer/src/main/resources/device_offline.jpg From 1146f6d98e40135237f8fca3bb4cd41ba06bd12b Mon Sep 17 00:00:00 2001 From: AuthBypass Date: Sat, 2 Aug 2025 18:38:17 +0200 Subject: [PATCH 21/21] Github workflows doing problems --- VideoServer/src/main/kotlin/CameraServer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/VideoServer/src/main/kotlin/CameraServer.kt b/VideoServer/src/main/kotlin/CameraServer.kt index fd0c949..d19c3a2 100644 --- a/VideoServer/src/main/kotlin/CameraServer.kt +++ b/VideoServer/src/main/kotlin/CameraServer.kt @@ -15,7 +15,8 @@ class CameraServer { fun onAvailable(frame: ByteArray) } - private val deviceOfflineImage: ByteArray = File("device_offline.jpg").readBytes() + private val deviceOfflineImage = object {}.javaClass.getResource("/device_offline.jpg")?.readBytes() + ?: error("device_offline.jpg not found in resources!") private val server = ServerSocket(4321)