From fe535117b095035105dee9606ac9d9afc7eebf95 Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 17:46:08 +0100 Subject: [PATCH 1/6] Prepare for API 36 --- android-versions.toml | 4 +-- app/build.gradle.kts | 5 ++- .../be/ugent/zeus/hydra/MainActivity.java | 11 ++++--- .../zeus/hydra/common/ui/WebViewActivity.java | 8 +++++ .../zeus/hydra/resto/salad/SaladActivity.java | 8 +++++ .../urgent/player/BecomingNoisyReceiver.java | 3 +- .../wpi/account/ApiKeyManagementActivity.java | 10 +++++- .../hydra/wpi/tab/create/FormActivity.java | 8 +++++ .../main/res/layout/activity_resto_salad.xml | 1 + app/src/main/res/layout/activity_webview.xml | 1 + flake.nix | 2 +- gradle/libs.versions.toml | 1 - material-intro/build.gradle.kts | 2 +- .../materialintro/app/IntroActivity.java | 31 ++++++++++--------- 14 files changed, 67 insertions(+), 28 deletions(-) diff --git a/android-versions.toml b/android-versions.toml index cc17e5fbf..a45d5858a 100644 --- a/android-versions.toml +++ b/android-versions.toml @@ -5,5 +5,5 @@ # Since properties strings are not quoted, we unquote the values in Gradle. cmdLineToolsVersion = "16.0-rc01" platformToolsVersion = "35.0.2" -buildToolsVersions = "35.0.0-rc4" -platformVersions = "35" +buildToolsVersions = "36.0.0" +platformVersions = "36" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2e38920eb..fbad19c96 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -45,7 +45,7 @@ android { compileSdk = versions.getProperty("platformVersions").toInt() applicationId = "be.ugent.zeus.hydra" minSdk = 21 - targetSdk = 35 + targetSdk = 36 versionCode = 37400 versionName = "3.7.4" vectorDrawables.useSupportLibrary = true @@ -167,7 +167,7 @@ android { lint { disable += listOf( - "RtlSymmetry", "VectorPath", "Overdraw", "GradleDependency", "NotificationPermission", "OldTargetApi", "AndroidGradlePluginVersion" + "RtlSymmetry", "VectorPath", "Overdraw", "GradleDependency", "NotificationPermission", "AndroidGradlePluginVersion" ) showAll = true warningsAsErrors = true @@ -200,7 +200,6 @@ dependencies { implementation(project(":material-intro")) implementation(libs.once) implementation(libs.materialvalues) - implementation(libs.insetter) implementation(libs.ipcam) annotationProcessor(libs.recordbuilder.processor) diff --git a/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java b/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java index 9093885d5..f0cf6dc2d 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/MainActivity.java @@ -65,7 +65,7 @@ import com.google.android.material.navigation.NavigationView; import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.tabs.TabLayout; -import dev.chrisbanes.insetter.Insetter; +import androidx.core.view.ViewCompat; import jonathanfinerty.once.Once; import org.jetbrains.annotations.NotNull; @@ -233,9 +233,12 @@ protected void onCreate(Bundle savedInstanceState) { // Prevent navigation drawer from extending behind the notification bar. // It doesn't work to just set fitsSystemWindows=true, since this adds the padding // for the navigation bar to the header, which is not what you want. - Insetter.builder() - .padding(WindowInsetsCompat.Type.statusBars()) - .applyToView(this.binding.navigationView.getHeaderView(0)); + ViewCompat.setOnApplyWindowInsetsListener(this.binding.navigationView.getHeaderView(0), (v, insets) -> { + androidx.core.graphics.Insets statusInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars()); + v.setPadding(statusInsets.left, statusInsets.top, statusInsets.right, statusInsets.bottom); + return insets; + }); + ViewCompat.requestApplyInsets(this.binding.navigationView.getHeaderView(0)); if (savedInstanceState != null) { isOnboardingOpen = savedInstanceState.getBoolean(STATE_IS_ONBOARDING_OPEN, false); diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java index cb5584d98..0cef1e709 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/WebViewActivity.java @@ -29,6 +29,8 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.ProgressBar; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import be.ugent.zeus.hydra.databinding.ActivityWebviewBinding; @@ -47,6 +49,12 @@ public class WebViewActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(ActivityWebviewBinding::inflate); + ViewCompat.setOnApplyWindowInsetsListener(binding.webContainer, (v, insets) -> { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.webContainer); binding.webView.getSettings().setJavaScriptEnabled(true); binding.webView.setWebViewClient(new ProgressClient(binding.progressBar.progressBar)); diff --git a/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java b/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java index cda06802d..add8d699b 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/resto/salad/SaladActivity.java @@ -26,6 +26,8 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import be.ugent.zeus.hydra.R; @@ -54,6 +56,12 @@ public class SaladActivity extends BaseActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(ActivityRestoSaladBinding::inflate); + ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView, (v, insets) -> { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.recyclerView); final ViewModelProvider provider = new ViewModelProvider(this); viewModel = provider.get(SaladViewModel.class); diff --git a/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java b/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java index 00b91b712..522da1af0 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java +++ b/app/src/main/java/be/ugent/zeus/hydra/urgent/player/BecomingNoisyReceiver.java @@ -26,6 +26,7 @@ import android.media.AudioManager; import android.support.v4.media.session.MediaControllerCompat; import android.support.v4.media.session.MediaSessionCompat; +import androidx.core.content.ContextCompat; /** * Helper class for listening for when headphones are unplugged (or the audio @@ -46,7 +47,7 @@ class BecomingNoisyReceiver extends BroadcastReceiver { void register() { if (!registered) { - context.registerReceiver(this, noisyIntentFilter); + ContextCompat.registerReceiver(context, this, noisyIntentFilter, ContextCompat.RECEIVER_NOT_EXPORTED); registered = true; } } diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java index ecb064c96..d86993cd5 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/account/ApiKeyManagementActivity.java @@ -27,6 +27,8 @@ import android.text.Editable; import android.text.TextWatcher; import androidx.annotation.Nullable; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import java.util.Objects; @@ -47,7 +49,13 @@ public class ApiKeyManagementActivity extends BaseActivity { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.scroll); + binding.apiTab.setText(AccountManager.getTabKey(this)); binding.apiTap.setText(AccountManager.getTapKey(this)); binding.apiUsername.setText(AccountManager.getUsername(this)); diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java index 05f6cdab3..a9a5e1485 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/tab/create/FormActivity.java @@ -30,6 +30,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.os.BundleCompat; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import java.math.BigDecimal; @@ -65,6 +67,12 @@ public class FormActivity extends BaseActivity { + int bottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom; + v.setPadding(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), bottom); + return insets; + }); + ViewCompat.requestApplyInsets(binding.scroll); if (savedInstanceState != null && savedInstanceState.containsKey(KEY_FORM_OBJECT)) { formObject = BundleCompat.getParcelable(savedInstanceState, KEY_FORM_OBJECT, TransactionForm.class); diff --git a/app/src/main/res/layout/activity_resto_salad.xml b/app/src/main/res/layout/activity_resto_salad.xml index 380a33aee..abc9cfabd 100644 --- a/app/src/main/res/layout/activity_resto_salad.xml +++ b/app/src/main/res/layout/activity_resto_salad.xml @@ -53,6 +53,7 @@ android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" + android:clipToPadding="false" android:scrollbars="none" app:layoutManager="@string/app_layout_manager_linear" tools:listitem="@layout/item_salad_bowl" /> diff --git a/app/src/main/res/layout/activity_webview.xml b/app/src/main/res/layout/activity_webview.xml index 1f841275c..cdc5e903d 100644 --- a/app/src/main/res/layout/activity_webview.xml +++ b/app/src/main/res/layout/activity_webview.xml @@ -38,6 +38,7 @@ 0) { + previousSlide(); + } else { + Intent returnIntent = onSendActivityResult(RESULT_CANCELED); + if (returnIntent != null) + setResult(RESULT_CANCELED, returnIntent); + else + setResult(RESULT_CANCELED); + finish(); + } + } + }); } @Override @@ -220,20 +237,6 @@ protected void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(KEY_BUTTON_CTA_VISIBLE, buttonCtaVisible); } - @Override - public void onBackPressed() { - if (position > 0) { - previousSlide(); - return; - } - Intent returnIntent = onSendActivityResult(RESULT_CANCELED); - if (returnIntent != null) - setResult(RESULT_CANCELED, returnIntent); - else - setResult(RESULT_CANCELED); - super.onBackPressed(); - } - public Intent onSendActivityResult(int result) { return null; } From 5459ca910cb0237d863535cef22a4d7e40e8a58c Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 17:48:45 +0100 Subject: [PATCH 2/6] Bump Android build tool --- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c1a38d9a8..64a3815b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ livedata = '2.9.2' okhttp3 = '4.12.0' recordbuilder = '52' room = '2.6.1' -buildtool = "8.7.3" +buildtool = "8.12.0" [libraries] android-build-tool = { module = 'com.android.tools.build:gradle', version.ref = 'buildtool' } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 79eb9d003..ed4c299ad 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 68c528696450425dd36a47982073f0746e5a8398 Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 17:55:12 +0100 Subject: [PATCH 3/6] Migrate deprecated build option --- app/build.gradle.kts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fbad19c96..a6969e24e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,7 +50,6 @@ android { versionName = "3.7.4" vectorDrawables.useSupportLibrary = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - resourceConfigurations += listOf("nl", "en") // For a description of what these do, see the config.properties file. buildConfigField("boolean", "DEBUG_HOME_STREAM_PRIORITY", props.getProperty("hydra.debug.home.stream.priority")) @@ -67,6 +66,12 @@ android { } } } + + androidResources { + // Typical for Android, the old way is deprecated and the new way not stable yet. + @Suppress("UnstableApiUsage") + localeFilters += listOf("nl", "en") + } if (props.getProperty("signing").toBoolean()) { From 434628649b84a12615276fc547f937072f270c43 Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 19:05:14 +0100 Subject: [PATCH 4/6] Vendor ipcam view --- app/build.gradle.kts | 3 +- .../hydra/common/ui/widgets/MenuTable.java | 7 +- .../feed/cards/library/LibraryViewHolder.java | 7 +- app/src/main/res/layout/item_tab_request.xml | 4 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/dimens.xml | 20 + gradle/libs.versions.toml | 3 +- ipcam-view/README.md | 137 +++++ ipcam-view/build.gradle.kts | 77 +++ ipcam-view/proguard-rules.pro | 17 + ipcam-view/src/main/AndroidManifest.xml | 1 + .../github/niqdev/mjpeg/AbstractMjpegView.kt | 20 + .../com/github/niqdev/mjpeg/DisplayMode.kt | 5 + .../java/com/github/niqdev/mjpeg/Mjpeg.java | 173 ++++++ .../github/niqdev/mjpeg/MjpegInputStream.kt | 6 + .../niqdev/mjpeg/MjpegInputStreamDefault.java | 103 ++++ .../niqdev/mjpeg/MjpegInputStreamNative.java | 174 ++++++ .../niqdev/mjpeg/MjpegRecordingHandler.kt | 118 ++++ .../github/niqdev/mjpeg/MjpegSurfaceView.java | 194 +++++++ .../java/com/github/niqdev/mjpeg/MjpegView.kt | 25 + .../github/niqdev/mjpeg/MjpegViewDefault.java | 508 ++++++++++++++++++ .../github/niqdev/mjpeg/MjpegViewNative.java | 462 ++++++++++++++++ .../niqdev/mjpeg/OnFrameCapturedListener.kt | 10 + ipcam-view/src/main/res/values/attrs.xml | 13 + material-intro/build.gradle.kts | 2 +- settings.gradle.kts | 3 +- 26 files changed, 2084 insertions(+), 12 deletions(-) create mode 100644 ipcam-view/README.md create mode 100644 ipcam-view/build.gradle.kts create mode 100644 ipcam-view/proguard-rules.pro create mode 100644 ipcam-view/src/main/AndroidManifest.xml create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/DisplayMode.kt create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java create mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt create mode 100644 ipcam-view/src/main/res/values/attrs.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a6969e24e..def6fee51 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -203,9 +203,8 @@ dependencies { implementation(libs.picasso) implementation(libs.cachapa) implementation(project(":material-intro")) + implementation(project(":ipcam-view")) implementation(libs.once) - implementation(libs.materialvalues) - implementation(libs.ipcam) annotationProcessor(libs.recordbuilder.processor) compileOnly(libs.recordbuilder.core) diff --git a/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java b/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java index a720adc50..e8d8c0eb7 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java +++ b/app/src/main/java/be/ugent/zeus/hydra/common/ui/widgets/MenuTable.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import android.util.TypedValue; import android.widget.TableLayout; import android.widget.TableRow; import android.widget.TextView; @@ -103,7 +104,11 @@ private void init(Context context, @Nullable AttributeSet attrs) { */ private void createText(String text, boolean isTitle, boolean isHtml) { - final int rowPadding = getContext().getResources().getDimensionPixelSize(R.dimen.material_baseline_grid_1x); + final int rowPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + getContext().getResources().getDisplayMetrics() + ); TableRow tr = new TableRow(getContext()); TableRow.LayoutParams lp = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT); diff --git a/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java b/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java index e398ef5f2..40601dc7b 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java +++ b/app/src/main/java/be/ugent/zeus/hydra/feed/cards/library/LibraryViewHolder.java @@ -25,6 +25,7 @@ import android.content.Context; import android.content.Intent; import android.util.Pair; +import android.util.TypedValue; import android.view.View; import android.widget.LinearLayout; import android.widget.TableRow; @@ -62,7 +63,11 @@ public LibraryViewHolder(View itemView, HomeFeedAdapter adapter) { Context c = itemView.getContext(); styleName = ViewUtils.getAttr(c, R.attr.textAppearanceBodyMedium); styleHours = ViewUtils.getAttr(c, R.attr.textAppearanceCaption); - rowPadding = c.getResources().getDimensionPixelSize(R.dimen.material_baseline_grid_1x); + rowPadding = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 8f, + c.getResources().getDisplayMetrics() + ); itemView.setOnClickListener(v -> { Intent intent = new Intent(v.getContext(), MainActivity.class); diff --git a/app/src/main/res/layout/item_tab_request.xml b/app/src/main/res/layout/item_tab_request.xml index 6a2d55e56..ef18d8f09 100644 --- a/app/src/main/res/layout/item_tab_request.xml +++ b/app/src/main/res/layout/item_tab_request.xml @@ -61,8 +61,8 @@ android:layout_width="wrap_content" app:icon="@drawable/ic_cancel" android:id="@+id/decline_button" - android:backgroundTint="@color/material_color_red_600" - android:background="@color/material_color_red_600" + android:backgroundTint="?attr/colorError" + android:background="?attr/colorError" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" /> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 78d3f66c3..05dc0ef36 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -83,14 +83,14 @@ #095BBF - @color/material_color_white + #FFFFFF - @color/material_color_grey_100 + #F5F5F5 @color/md_theme_light_primary @color/wpi_theme_light_primary diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e6162d93b..cafbd1d70 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -84,4 +84,24 @@ 64dp + + + 8dp + 6dp + 56dp + 40dp + 8dp + 16dp + 20sp + 56dp + 16dp + 24dp + 16dp + 24dp + 24dp + 16dp + 12dp + 16sp + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64a3815b9..e26abe495 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ androidxTest = '1.7.0' espresso = '3.7.0' livedata = '2.9.2' +mjpeg = "3.0.0" okhttp3 = '4.12.0' recordbuilder = '52' room = '2.6.1' @@ -41,13 +42,11 @@ equalsverifier = { module = 'nl.jqno.equalsverifier:equalsverifier', version = ' firebase-analytics = { module = 'com.google.firebase:firebase-analytics', version = '23.0.0' } firebase-crashlytics = { module = 'com.google.firebase:firebase-crashlytics', version = '19.4.1' } guava = { module = 'com.google.guava:guava', version = '33.4.8-jre' } -ipcam = { module = 'com.github.niqdev:ipcam-view', version = '2.4.1' } jetbrains-annotations = { module = 'org.jetbrains:annotations', version = '26.1.0' } jsonassert = { module = 'org.skyscreamer:jsonassert', version = '1.5.3' } junit = { module = 'junit:junit', version = '4.13.2' } leakcanary = { module = 'com.squareup.leakcanary:leakcanary-android', version = '2.14' } material = { module = 'com.google.android.material:material', version = '1.13.0' } -materialvalues = { module = 'com.github.esnaultdev:MaterialValues', version = 'v1.1.1' } mockito-core = { module = 'org.mockito:mockito-core', version = '5.22.0' } mockito-inline = { module = 'org.mockito:mockito-inline', version = '5.2.0' } moshi = { module = 'com.squareup.moshi:moshi', version = '1.15.2' } diff --git a/ipcam-view/README.md b/ipcam-view/README.md new file mode 100644 index 000000000..48e3dd20a --- /dev/null +++ b/ipcam-view/README.md @@ -0,0 +1,137 @@ +Vendored version from ipcam view 3.0.0. + +We just include the `mjpeg-view` folder. + + +# ipcam-view ![ipcam-view](images/logo.png) + +[![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-ipcam--view-brightgreen.svg?style=flat)](http://android-arsenal.com/details/1/3358) +[![JitPack](https://jitpack.io/v/niqdev/ipcam-view.svg)](https://jitpack.io/#niqdev/ipcam-view) + +Android MJPEG video streaming made simple! + +A wrapper library around the well known [SimpleMjpegView](https://bitbucket.org/neuralassembly/simplemjpegview) and [android-camera-axis](https://code.google.com/archive/p/android-camera-axis/) projects. + +If you have problem to identify your IpCam url, please follow this [link](https://github.com/niqdev/ipcam-view/wiki) + +### Features +- [x] Default support by `android-camera-axis` +- [ ] Native support by `SimpleMjpegView` +- [x] Handle credentials and cookies +- [x] Multiple camera in one activity +- [x] Snapshot +- [x] Flip and rotate image +- [x] Video recording +- [x] Custom appearance + +### Gradle dependency +```java +repositories { + maven { url 'https://jitpack.io' } +} +dependencies { + implementation 'com.github.niqdev:ipcam-view:' +} +``` + +### Demo app + +main default + +two-camera snapshot + +video custom-appearance + +settings + +[](https://f-droid.org/packages/com.github.niqdev.ipcam/) +[](https://play.google.com/store/apps/details?id=com.github.niqdev.ipcam) + +### Usage + +Add to your layout: [example](app/src/main/res/layout/activity_ipcam_default.xml) +```java + + + + + + +``` + +Read stream in your activity/fragment: [example](app/src/main/java/com/github/niqdev/ipcam/IpCamDefaultActivity.kt) +```java +int TIMEOUT = 5; //seconds + +Mjpeg.newInstance() + .credential("USERNAME", "PASSWORD") + .open("IPCAM_URL.mjpg", TIMEOUT) + .subscribe(inputStream -> { + mjpegView.setSource(inputStream); + mjpegView.setDisplayMode(DisplayMode.BEST_FIT); + mjpegView.showFps(true); + }); +``` + +### Customize appearance + +To get a transparent background for the surface itself (while stream is loading) as well as for the stream background +```java +mjpegView.setTransparentBackground(); +// OR +stream:transparentBackground="true" +``` + +To hide the MjpegView later, you might need to reset the transparency due to internal behaviour of applying transparency +```java +mjpegView.resetTransparentBackground(); +``` + +To set other colors than transparent, be aware that they will only be applied on a running stream i.e. you can't change the color of the surface itself which you will see while the stream is loading + +Note that it only works when `transparentBackground` is not set to `true` and that you are not able to directly set transparent background color here +```java +mjpegView.setCustomBackgroundColor(Color.DKGRAY); +// OR +stream:backgroundColor="@android:color/darker_gray" +``` + +To change the colors of the fps overlay +```java +mjpegView.setFpsOverlayBackgroundColor(Color.DKGRAY); +mjpegView.setFpsOverlayTextColor(Color.WHITE); +``` + +To clear the last frame since the canvas keeps the current image even if you stop the stream, e.g. hide/show +```java +mjpegView.clearStream(); +``` + +To flip the image +```java +mjpegView.flipHorizontal(true); +mjpegView.flipVertical(true); +``` + +To rotate the image +```java +mjpegView.setRotate(90); // degrees +``` + +### Apps that use this library +* [OpenWebNet Android](https://github.com/openwebnet/openwebnet-android) +* [TankDroid](https://github.com/bmachek/TankDroid) + +You are welcome to add your app to the list! diff --git a/ipcam-view/build.gradle.kts b/ipcam-view/build.gradle.kts new file mode 100644 index 000000000..745b594e8 --- /dev/null +++ b/ipcam-view/build.gradle.kts @@ -0,0 +1,77 @@ +import java.io.FileInputStream +import java.util.* + +plugins { + id(libs.plugins.android.build.library.get().pluginId) + id("org.jetbrains.kotlin.android") version "2.3.10" +} + +val versions = loadAndroidVersions() + +android { + namespace = "com.github.niqdev.mjpeg" + + // We need this for Nix flakes + buildToolsVersion = versions.getProperty("buildToolsVersions") + + defaultConfig { + compileSdk = versions.getProperty("platformVersions").toInt() + minSdk = 21 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") + } + } + lint { + disable += listOf("Overdraw", "OldTargetApi", "GradleDependency") + showAll = true + warningsAsErrors = true + targetSdk = versions.getProperty("platformVersions").toInt() + } + + packaging { + resources { + excludes.add("META-INF/services/javax.annotation.processing.Processor") + } + } + + useLibrary("org.apache.http.legacy") + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } + } +} + +dependencies { + implementation(libs.androidx.appcompat) + api("io.reactivex:rxjava:1.3.8") // it"s obsolete + api("io.reactivex:rxandroid:1.2.1") + implementation("androidx.core:core-ktx:1.17.0") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.22") +} + +// TODO: extract this duplicate code... +/** + * Loads the default properties, and the user properties. This will also load the + * secret keys. + */ +fun loadAndroidVersions(): Properties { + val defaultProps = Properties() + defaultProps.load(FileInputStream(file("../android-versions.toml"))) + + val strippedProps = Properties() + defaultProps.forEach { k, v -> + strippedProps.setProperty(k.toString(), v.toString().replace("\"", "")) + } + + return strippedProps +} diff --git a/ipcam-view/proguard-rules.pro b/ipcam-view/proguard-rules.pro new file mode 100644 index 000000000..06d16ae7a --- /dev/null +++ b/ipcam-view/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/niqdev/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/ipcam-view/src/main/AndroidManifest.xml b/ipcam-view/src/main/AndroidManifest.xml new file mode 100644 index 000000000..cc947c567 --- /dev/null +++ b/ipcam-view/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt new file mode 100644 index 000000000..ecb2ce634 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/AbstractMjpegView.kt @@ -0,0 +1,20 @@ +package com.github.niqdev.mjpeg + +import android.view.SurfaceHolder + +abstract class AbstractMjpegView : MjpegView { + abstract fun onSurfaceCreated(holder: SurfaceHolder) + abstract fun onSurfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) + abstract fun onSurfaceDestroyed(holder: SurfaceHolder) + + companion object { + protected const val POSITION_UPPER_LEFT = 9 + protected const val POSITION_UPPER_RIGHT = 3 + protected const val POSITION_LOWER_LEFT = 12 + protected const val POSITION_LOWER_RIGHT = 6 + protected const val SIZE_STANDARD = 1 + protected const val SIZE_BEST_FIT = 4 + protected const val SIZE_SCALE_FIT = 16 + protected const val SIZE_FULLSCREEN = 8 + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/DisplayMode.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/DisplayMode.kt new file mode 100644 index 000000000..73afb8274 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/DisplayMode.kt @@ -0,0 +1,5 @@ +package com.github.niqdev.mjpeg + +enum class DisplayMode(val value: Int) { + STANDARD(1), BEST_FIT(4), SCALE_FIT(16), FULLSCREEN(8); +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java new file mode 100644 index 000000000..42ef9e4fb --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/Mjpeg.java @@ -0,0 +1,173 @@ +package com.github.niqdev.mjpeg; + +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Authenticator; +import java.net.CookieManager; +import java.net.HttpCookie; +import java.net.HttpURLConnection; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.util.concurrent.TimeUnit; + +import androidx.annotation.NonNull; +import rx.Observable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; + +/** + * A library wrapper for handle mjpeg streams. + * + *
  • simplemjpegview
  • + *
  • android-camera-axis
  • + * + */ +public class Mjpeg { + private static final String TAG = Mjpeg.class.getSimpleName(); + + private static final CookieManager msCookieManager = new CookieManager(); + private final Type type; + private boolean sendConnectionCloseHeader = false; + + private Mjpeg(Type type) { + if (type == null) { + throw new IllegalArgumentException("null type not allowed"); + } + this.type = type; + } + + /** + * Uses {@link Type#DEFAULT} implementation. + * + * @return Mjpeg instance + */ + public static Mjpeg newInstance() { + return new Mjpeg(Type.DEFAULT); + } + + /** + * Choose among {@link com.github.niqdev.mjpeg.Mjpeg.Type} implementations. + * + * @return Mjpeg instance + */ + public static Mjpeg newInstance(Type type) { + return new Mjpeg(type); + } + + /** + * Configure authentication. + * + * @param username credential + * @param password credential + * @return Mjpeg instance + */ + public Mjpeg credential(String username, String password) { + if (!TextUtils.isEmpty(username) && !TextUtils.isEmpty(password)) { + Authenticator.setDefault(new Authenticator() { + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password.toCharArray()); + } + }); + } + return this; + } + + /** + * Configure cookies. + * + * @param cookie cookie string + * @return Mjpeg instance + */ + public Mjpeg addCookie(String cookie) { + if (!TextUtils.isEmpty(cookie)) { + msCookieManager.getCookieStore().add(null, HttpCookie.parse(cookie).get(0)); + } + return this; + } + + /** + * Send a "Connection: close" header to fix + * java.net.ProtocolException: Unexpected status line + * + * @return Observable Mjpeg stream + */ + public Mjpeg sendConnectionCloseHeader() { + sendConnectionCloseHeader = true; + return this; + } + + @NonNull + private Observable connect(String url) { + return Observable.defer(() -> { + try { + HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); + loadConnectionProperties(urlConnection); + InputStream inputStream = urlConnection.getInputStream(); + switch (type) { + // handle multiple implementations + case DEFAULT: + return Observable.just(new MjpegInputStreamDefault(inputStream)); + case NATIVE: + return Observable.just(new MjpegInputStreamNative(inputStream)); + } + throw new IllegalStateException("invalid type"); + } catch (IOException e) { + Log.e(TAG, "error during connection", e); + return Observable.error(e); + } + }); + } + + /** + * Connect to a Mjpeg stream. + * + * @param url source + * @return Observable Mjpeg stream + */ + public Observable open(String url) { + return connect(url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * Connect to a Mjpeg stream. + * + * @param url source + * @param timeout in seconds + * @return Observable Mjpeg stream + */ + public Observable open(String url, int timeout) { + return connect(url) + .timeout(timeout, TimeUnit.SECONDS) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + /** + * Configure request properties + * + * @param urlConnection the url connection to add properties and cookies to + */ + private void loadConnectionProperties(HttpURLConnection urlConnection) { + urlConnection.setRequestProperty("Cache-Control", "no-cache"); + if (sendConnectionCloseHeader) { + urlConnection.setRequestProperty("Connection", "close"); + } + + if (!msCookieManager.getCookieStore().getCookies().isEmpty()) { + urlConnection.setRequestProperty("Cookie", + TextUtils.join(";", msCookieManager.getCookieStore().getCookies())); + } + } + + /** + * Library implementation type + */ + public enum Type { + DEFAULT, NATIVE + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt new file mode 100644 index 000000000..1fcd2094a --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt @@ -0,0 +1,6 @@ +package com.github.niqdev.mjpeg + +import java.io.DataInputStream +import java.io.InputStream + +abstract class MjpegInputStream(insputStream: InputStream) : DataInputStream(insputStream) \ No newline at end of file diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java new file mode 100644 index 000000000..9850b1f77 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java @@ -0,0 +1,103 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://code.google.com/archive/p/android-camera-axis + */ +public class MjpegInputStreamDefault extends MjpegInputStream { + private final static int HEADER_MAX_LENGTH = 100; + private final static int FRAME_MAX_LENGTH = 200000 + HEADER_MAX_LENGTH; + private final byte[] SOI_MARKER = {(byte) 0xFF, (byte) 0xD8}; + private final byte[] EOF_MARKER = {(byte) 0xFF, (byte) 0xD9}; + private final String CONTENT_LENGTH = "Content-Length"; + private int mContentLength = -1; + + // no more accessible + MjpegInputStreamDefault(InputStream in) { + super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); + } + + private int getEndOfSequence(DataInputStream in, byte[] sequence) throws IOException { + int seqIndex = 0; + byte c; + for (int i = 0; i < FRAME_MAX_LENGTH; i++) { + c = (byte) in.readUnsignedByte(); + if (c == sequence[seqIndex]) { + seqIndex++; + if (seqIndex == sequence.length) { + return i + 1; + } + } else { + seqIndex = 0; + } + } + return -1; + } + + private int getStartOfSequence(DataInputStream in, byte[] sequence) throws IOException { + int end = getEndOfSequence(in, sequence); + return (end < 0) ? (-1) : (end - sequence.length); + } + + private int parseContentLength(byte[] headerBytes) throws IOException, IllegalArgumentException { + ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); + Properties props = new Properties(); + props.load(headerIn); + return Integer.parseInt(props.getProperty(CONTENT_LENGTH)); + } + + byte[] readHeader() throws IOException { + mark(FRAME_MAX_LENGTH); + int headerLen = getStartOfSequence(this, SOI_MARKER); + reset(); + byte[] header = new byte[headerLen]; + readFully(header); + return header; + } + + // no more accessible + byte[] readMjpegFrame(byte[] header) throws IOException { + try { + mContentLength = parseContentLength(header); + } catch (IllegalArgumentException iae) { + mContentLength = getEndOfSequence(this, EOF_MARKER); + } + reset(); + byte[] frameData = new byte[mContentLength]; + skipBytes(header.length); + readFully(frameData); + return frameData; + } + + // no more accessible + Bitmap readMjpegFrame() throws IOException { + mark(FRAME_MAX_LENGTH); + int headerLen = getStartOfSequence(this, SOI_MARKER); + reset(); + byte[] header = new byte[headerLen]; + readFully(header); + try { + mContentLength = parseContentLength(header); + } catch (IllegalArgumentException iae) { + mContentLength = getEndOfSequence(this, EOF_MARKER); + } + reset(); + byte[] frameData = new byte[mContentLength]; + skipBytes(headerLen); + readFully(frameData); + return BitmapFactory.decodeStream(new ByteArrayInputStream(frameData)); + } + +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java new file mode 100644 index 000000000..5ebf86c4d --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java @@ -0,0 +1,174 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.Bitmap; +import android.util.Log; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://bitbucket.org/neuralassembly/simplemjpegview + */ +public class MjpegInputStreamNative extends MjpegInputStream { + + //private final static int FRAME_MAX_LENGTH = 40000 + HEADER_MAX_LENGTH; + private final static int FRAME_MAX_LENGTH = 200000; + private static final String TAG = "MJPEG"; + private static final boolean DEBUG = false; + + static { + System.loadLibrary("ImageProc"); + } + + private final byte[] SOI_MARKER = {(byte) 0xFF, (byte) 0xD8}; + private final byte[] EOF_MARKER = {(byte) 0xFF, (byte) 0xD9}; + private final String CONTENT_LENGTH = "Content-Length"; + byte[] header = null; + byte[] frameData = null; + int headerLen = -1; + int headerLenPrev = -1; + int skip = 1; + int count = 0; + private int mContentLength = -1; + + // no more accessible + MjpegInputStreamNative(InputStream in) { + super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); + } + + public native int pixeltobmp(byte[] jp, int l, Bitmap bmp); + + public native void freeCameraMemory(); + + private int getEndOfSeqeunce(DataInputStream in, byte[] sequence) + throws IOException { + + int seqIndex = 0; + byte c; + for (int i = 0; i < FRAME_MAX_LENGTH; i++) { + c = (byte) in.readUnsignedByte(); + if (c == sequence[seqIndex]) { + seqIndex++; + if (seqIndex == sequence.length) { + + return i + 1; + } + } else seqIndex = 0; + } + + return -1; + } + + private int getStartOfSequence(DataInputStream in, byte[] sequence) + throws IOException { + int end = getEndOfSeqeunce(in, sequence); + return (end < 0) ? (-1) : (end - sequence.length); + } + + private int getEndOfSeqeunceSimplified(DataInputStream in, byte[] sequence) + throws IOException { + int startPos = mContentLength / 2; + int endPos = 3 * mContentLength / 2; + + skipBytes(headerLen + startPos); + + int seqIndex = 0; + byte c; + for (int i = 0; i < endPos - startPos; i++) { + c = (byte) in.readUnsignedByte(); + if (c == sequence[seqIndex]) { + seqIndex++; + if (seqIndex == sequence.length) { + + return headerLen + startPos + i + 1; + } + } else seqIndex = 0; + } + + return -1; + } + + private int parseContentLength(byte[] headerBytes) + throws IOException, IllegalArgumentException { + ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); + Properties props = new Properties(); + props.load(headerIn); + return Integer.parseInt(props.getProperty(CONTENT_LENGTH)); + } + + // no more accessible + int readMjpegFrame(Bitmap bmp) throws IOException { + mark(FRAME_MAX_LENGTH); + int headerLen; + try { + headerLen = getStartOfSequence(this, SOI_MARKER); + } catch (IOException e) { + if (DEBUG) Log.d(TAG, "IOException in betting headerLen."); + reset(); + return -1; + } + reset(); + + if (header == null || headerLen != headerLenPrev) { + header = new byte[headerLen]; + if (DEBUG) Log.d(TAG, "header renewed " + headerLenPrev + " -> " + headerLen); + } + headerLenPrev = headerLen; + readFully(header); + + int ContentLengthNew; + try { + ContentLengthNew = parseContentLength(header); + } catch (NumberFormatException nfe) { + ContentLengthNew = getEndOfSeqeunceSimplified(this, EOF_MARKER); + + if (ContentLengthNew < 0) { + if (DEBUG) Log.d(TAG, "Worst case for finding EOF_MARKER"); + reset(); + ContentLengthNew = getEndOfSeqeunce(this, EOF_MARKER); + } + } catch (IllegalArgumentException e) { + if (DEBUG) Log.d(TAG, "IllegalArgumentException in parseContentLength"); + ContentLengthNew = getEndOfSeqeunceSimplified(this, EOF_MARKER); + + if (ContentLengthNew < 0) { + if (DEBUG) Log.d(TAG, "Worst case for finding EOF_MARKER"); + reset(); + ContentLengthNew = getEndOfSeqeunce(this, EOF_MARKER); + } + } catch (IOException e) { + if (DEBUG) Log.d(TAG, "IOException in parseContentLength"); + reset(); + return -1; + } + mContentLength = ContentLengthNew; + reset(); + + if (frameData == null || mContentLength > frameData.length) { + frameData = new byte[mContentLength]; // + HEADER_MAX_LENGTH]; + if (DEBUG) Log.d(TAG, "frameData renewed cl=" + mContentLength); + } + + skipBytes(headerLen); + + readFully(frameData, 0, mContentLength); + + if (count++ % skip == 0) { + return pixeltobmp(frameData, mContentLength, bmp); + } else { + return 0; + } + } + + // no more accessible + void setSkip(int s) { + skip = s; + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt new file mode 100644 index 000000000..46a651f14 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt @@ -0,0 +1,118 @@ +package com.github.niqdev.mjpeg + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.util.Log +import android.widget.Toast +import java.io.* +import java.text.SimpleDateFormat +import java.util.* + +class MjpegRecordingHandler(private val context: Context) : OnFrameCapturedListener { + private var bos: BufferedOutputStream? = null + var isRecording = false + var lastBitmap: Bitmap? = null + private set + + /** + * start recording the live image + */ + fun startRecording() { + try { + val mjpegFilePath = createMjpegFile()!!.absolutePath + val fos = FileOutputStream(mjpegFilePath) + bos = BufferedOutputStream(fos) + Toast.makeText(context, "start recording, file path is:$mjpegFilePath", Toast.LENGTH_LONG).show() + isRecording = true + } catch (e: FileNotFoundException) { + Log.e(TAG, e.message.toString()) + } + } + + /** + * stop recording the live image + */ + fun stopRecording() { + isRecording = false + } + + /** + * save the last acquired bitmap into jpg file. + */ + fun saveBitmapToFile() { + val fos: FileOutputStream + val bos: BufferedOutputStream + val imagePath = createJpgFile()!!.absolutePath + try { + fos = FileOutputStream(imagePath) + bos = BufferedOutputStream(fos) + val jpegByteArrayOutputStream = ByteArrayOutputStream() + lastBitmap?.compress(Bitmap.CompressFormat.JPEG, 75, jpegByteArrayOutputStream) + val jpegByteArray = jpegByteArrayOutputStream.toByteArray() + bos.write(jpegByteArray) + bos.flush() + Toast.makeText(context, "saved image:$imagePath", Toast.LENGTH_LONG).show() + } catch (e: IOException) { + Log.e(TAG, e.message.toString()) + } + } + + /** + * Create jpg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files + * + * @return File + */ + private fun createJpgFile(): File? { + return createSavingFile("photo", "jpg") + } + + private fun createSavingFile(prefix: String, extension: String): File? { + val date = Date() + + @SuppressLint("SimpleDateFormat") + val sdf = SimpleDateFormat("yyyyMMddHHmmss") + val szFileName = prefix + "-" + sdf.format(date) + try { + val path = context.getExternalFilesDir(null)!!.path + "/" + szFileName + "." + extension + val file = File(path) + if (!file.exists()) { + file.createNewFile() + } + Log.d(TAG, "file path is " + file.absolutePath) + return file + } catch (e: IOException) { + Log.e(TAG, e.message.toString()) + } + return null + } + + /** + * Create mjpeg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files + * + * @return File + */ + private fun createMjpegFile(): File? { + return createSavingFile("video", "mjpeg") + } + + override fun onFrameCaptured(bitmap: Bitmap) { + lastBitmap = bitmap + } + + override fun onFrameCapturedWithHeader(bitmap: ByteArray, header: ByteArray) { + if (isRecording) { + try { + bos!!.write(header) + bos!!.write(bitmap) + bos!!.flush() + } catch (e: IOException) { + Log.e(TAG, e.message!!) + } + } + } + + companion object { + private const val TAG = "MjpegRecordingHandler" + } +} \ No newline at end of file diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java new file mode 100644 index 000000000..e635f6883 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java @@ -0,0 +1,194 @@ +package com.github.niqdev.mjpeg; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.PixelFormat; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import androidx.annotation.NonNull; +import androidx.annotation.StyleableRes; + +public class MjpegSurfaceView extends SurfaceView implements SurfaceHolder.Callback, MjpegView { + + private static final int DEFAULT_TYPE = 0; + // issue in attrs.xml - verify reserved keywords + private static final SparseArray TYPE; + + static { + TYPE = new SparseArray<>(); + TYPE.put(0, Mjpeg.Type.DEFAULT); + TYPE.put(1, Mjpeg.Type.NATIVE); + } + + private MjpegView mMjpegView; + + public MjpegSurfaceView(Context context, AttributeSet attrs) { + super(context, attrs); + boolean transparentBackground = getPropertyBoolean(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_transparentBackground); + int backgroundColor = getPropertyColor(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_backgroundColor); + + if (transparentBackground) { + setZOrderOnTop(true); + getHolder().setFormat(PixelFormat.TRANSPARENT); + } + + switch (getPropertyType(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_type)) { + case DEFAULT: + mMjpegView = new MjpegViewDefault(this, this, transparentBackground); + break; + case NATIVE: + mMjpegView = new MjpegViewNative(this, this, transparentBackground); + break; + } + + if (mMjpegView != null && backgroundColor != -1) { + this.setCustomBackgroundColor(backgroundColor); + } + } + + public Mjpeg.Type getPropertyType(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { + TypedArray typedArray = getContext().getTheme() + .obtainStyledAttributes(attributeSet, attrs, 0, 0); + try { + int typeIndex = typedArray.getInt(attrIndex, DEFAULT_TYPE); + Mjpeg.Type type = TYPE.get(typeIndex); + return type != null ? type : TYPE.get(DEFAULT_TYPE); + } finally { + typedArray.recycle(); + } + } + + public boolean getPropertyBoolean(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { + TypedArray typedArray = getContext().getTheme() + .obtainStyledAttributes(attributeSet, attrs, 0, 0); + try { + return typedArray.getBoolean(attrIndex, false); + } finally { + typedArray.recycle(); + } + } + + public int getPropertyColor(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { + TypedArray typedArray = getContext().getTheme() + .obtainStyledAttributes(attributeSet, attrs, 0, 0); + try { + return typedArray.getColor(attrIndex, -1); + } finally { + typedArray.recycle(); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + ((AbstractMjpegView) mMjpegView).onSurfaceCreated(holder); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + ((AbstractMjpegView) mMjpegView).onSurfaceChanged(holder, format, width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + ((AbstractMjpegView) mMjpegView).onSurfaceDestroyed(holder); + } + + @Override + public void setSource(@NonNull MjpegInputStream stream) { + mMjpegView.setSource(stream); + } + + @Override + public void setDisplayMode(@NonNull DisplayMode mode) { + mMjpegView.setDisplayMode(mode); + } + + @Override + public void showFps(boolean show) { + mMjpegView.showFps(show); + } + + @Override + public void flipSource(boolean flip) { + mMjpegView.flipSource(flip); + } + + @Override + public void flipHorizontal(boolean flip) { + mMjpegView.flipHorizontal(flip); + } + + @Override + public void flipVertical(boolean flip) { + mMjpegView.flipVertical(flip); + } + + @Override + public void setRotate(float degrees) { + mMjpegView.setRotate(degrees); + } + + @Override + public void stopPlayback() { + mMjpegView.stopPlayback(); + } + + @Override + public boolean isStreaming() { + return mMjpegView.isStreaming(); + } + + @Override + public void setResolution(int width, int height) { + mMjpegView.setResolution(width, height); + } + + @Override + public void freeCameraMemory() { + mMjpegView.freeCameraMemory(); + } + + @Override + public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { + mMjpegView.setOnFrameCapturedListener(onFrameCapturedListener); + } + + @Override + public void setCustomBackgroundColor(int backgroundColor) { + mMjpegView.setCustomBackgroundColor(backgroundColor); + } + + @Override + public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { + mMjpegView.setFpsOverlayBackgroundColor(overlayBackgroundColor); + } + + @Override + public void setFpsOverlayTextColor(int overlayTextColor) { + mMjpegView.setFpsOverlayTextColor(overlayTextColor); + } + + @NonNull + @Override + public SurfaceView getSurfaceView() { + return this; + } + + @Override + public void resetTransparentBackground() { + mMjpegView.resetTransparentBackground(); + } + + @Override + public void setTransparentBackground() { + mMjpegView.setTransparentBackground(); + } + + @Override + public void clearStream() { + mMjpegView.clearStream(); + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt new file mode 100644 index 000000000..bc92a16da --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt @@ -0,0 +1,25 @@ +package com.github.niqdev.mjpeg + +import android.view.SurfaceView + +interface MjpegView { + fun setSource(stream: MjpegInputStream) + fun setDisplayMode(mode: DisplayMode) + fun showFps(show: Boolean) + fun flipSource(flip: Boolean) + fun flipHorizontal(flip: Boolean) + fun flipVertical(flip: Boolean) + fun setRotate(degrees: Float) + fun stopPlayback() + val isStreaming: Boolean + fun setResolution(width: Int, height: Int) + fun freeCameraMemory() + fun setOnFrameCapturedListener(onFrameCapturedListener: OnFrameCapturedListener) + fun setCustomBackgroundColor(backgroundColor: Int) + fun setFpsOverlayBackgroundColor(overlayBackgroundColor: Int) + fun setFpsOverlayTextColor(overlayTextColor: Int) + val surfaceView: SurfaceView + fun resetTransparentBackground() + fun setTransparentBackground() + fun clearStream() +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java new file mode 100644 index 000000000..6f4d488c3 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java @@ -0,0 +1,508 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import androidx.annotation.NonNull; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://code.google.com/archive/p/android-camera-axis + */ +public class MjpegViewDefault extends AbstractMjpegView { + private static final String TAG = MjpegViewDefault.class.getSimpleName(); + + private final SurfaceHolder.Callback mSurfaceHolderCallback; + private final SurfaceView mSurfaceView; + private final boolean transparentBackground; + + private MjpegViewThread thread; + private MjpegInputStreamDefault mIn = null; + private boolean showFps = false; + private boolean flipHorizontal = false; + private boolean flipVertical = false; + private float rotateDegrees = 0; + private volatile boolean mRun = false; + private volatile boolean surfaceDone = false; + private Paint overlayPaint; + private int overlayTextColor; + private int overlayBackgroundColor; + private int backgroundColor; + private int ovlPos; + private int dispWidth; + private int dispHeight; + private int displayMode; + private boolean resume = false; + + private OnFrameCapturedListener onFrameCapturedListener; + + MjpegViewDefault(SurfaceView surfaceView, SurfaceHolder.Callback callback, boolean transparentBackground) { + this.mSurfaceView = surfaceView; + this.mSurfaceHolderCallback = callback; + this.transparentBackground = transparentBackground; + init(); + } + + Bitmap flip(Bitmap src) { + Matrix m = new Matrix(); + float sx = flipHorizontal ? -1 : 1; + float sy = flipVertical ? -1 : 1; + m.preScale(sx, sy); + Bitmap dst = Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, false); + dst.setDensity(DisplayMetrics.DENSITY_DEFAULT); + return dst; + } + + Bitmap rotate(Bitmap src, float degrees) { + Matrix m = new Matrix(); + m.setRotate(degrees); + return Bitmap.createBitmap(src, 0, 0, src.getWidth(), src.getHeight(), m, false); + } + + private void init() { + + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.addCallback(mSurfaceHolderCallback); + thread = new MjpegViewThread(holder); + mSurfaceView.setFocusable(true); + if (!resume) { + resume = true; + overlayPaint = new Paint(); + overlayPaint.setTextAlign(Paint.Align.LEFT); + overlayPaint.setTextSize(12); + overlayPaint.setTypeface(Typeface.DEFAULT); + overlayTextColor = Color.WHITE; + overlayBackgroundColor = Color.BLACK; + backgroundColor = Color.BLACK; + ovlPos = MjpegViewDefault.POSITION_LOWER_RIGHT; + displayMode = MjpegViewDefault.SIZE_STANDARD; + dispWidth = mSurfaceView.getWidth(); + dispHeight = mSurfaceView.getHeight(); + } + } + + /* all methods/constructors below are no more accessible */ + + void _startPlayback() { + if (mIn != null && thread != null) { + mRun = true; + /* + * clear canvas cache + * @see https://github.com/niqdev/ipcam-view/issues/14 + */ + mSurfaceView.destroyDrawingCache(); + thread.start(); + } + } + + void _resumePlayback() { + mRun = true; + init(); + thread.start(); + } + + /* + * @see https://github.com/niqdev/ipcam-view/issues/14 + */ + synchronized void _stopPlayback() { + mRun = false; + boolean retry = true; + while (retry) { + try { + // make sure the thread is not null + if (thread != null) { + thread.join(500); + } + retry = false; + } catch (InterruptedException e) { + Log.e(TAG, "error stopping playback thread", e); + } + } + + // close the connection + if (mIn != null) { + try { + mIn.close(); + } catch (IOException e) { + Log.e(TAG, "error closing input stream", e); + } + mIn = null; + } + } + + void _surfaceChanged(int w, int h) { + if (thread != null) { + thread.setSurfaceSize(w, h); + } + } + + void _surfaceDestroyed() { + surfaceDone = false; + _stopPlayback(); + if (thread != null) { + thread = null; + } + } + + void _frameCapturedWithByteData(byte[] imageByte, byte[] header) { + if (onFrameCapturedListener != null) { + onFrameCapturedListener.onFrameCapturedWithHeader(imageByte, header); + } + } + + void _frameCapturedWithBitmap(Bitmap bitmap) { + if (onFrameCapturedListener != null) { + onFrameCapturedListener.onFrameCaptured(bitmap); + } + } + + void _surfaceCreated() { + surfaceDone = true; + } + + void _showFps(boolean b) { + showFps = b; + } + + void _flipHorizontal(boolean b) { + flipHorizontal = b; + } + + void _flipVertical(boolean b) { + flipVertical = b; + } + + /* + * @see https://github.com/niqdev/ipcam-view/issues/14 + */ + void _setSource(MjpegInputStreamDefault source) { + mIn = source; + // make sure resume is calling _resumePlayback() + if (!resume) { + _startPlayback(); + } else { + _resumePlayback(); + } + } + + void setOverlayPaint(Paint p) { + overlayPaint = p; + } + + void setOverlayTextColor(int c) { + overlayTextColor = c; + } + + void setOverlayBackgroundColor(int c) { + overlayBackgroundColor = c; + } + + void setOverlayPosition(int p) { + ovlPos = p; + } + + void setDisplayMode(int s) { + displayMode = s; + } + + @Override + public void onSurfaceCreated(@NonNull SurfaceHolder holder) { + _surfaceCreated(); + } + + /* override methods */ + + @Override + public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + _surfaceChanged(width, height); + } + + @Override + public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { + _surfaceDestroyed(); + } + + @Override + public void setSource(@NonNull MjpegInputStream stream) { + if (!(stream instanceof MjpegInputStreamDefault)) { + throw new IllegalArgumentException("stream must be an instance of MjpegInputStreamDefault"); + } + _setSource((MjpegInputStreamDefault) stream); + } + + @Override + public void setDisplayMode(DisplayMode mode) { + setDisplayMode(mode.getValue()); + } + + @Override + public void showFps(boolean show) { + _showFps(show); + } + + @Override + public void flipSource(boolean flip) { + _flipHorizontal(flip); + } + + @Override + public void flipHorizontal(boolean flip) { + _flipHorizontal(flip); + } + + @Override + public void flipVertical(boolean flip) { + _flipVertical(flip); + } + + @Override + public void setRotate(float degrees) { + rotateDegrees = degrees; + } + + @Override + public void stopPlayback() { + _stopPlayback(); + } + + @Override + public boolean isStreaming() { + return mRun; + } + + @Override + public void setResolution(int width, int height) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void freeCameraMemory() { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { + this.onFrameCapturedListener = onFrameCapturedListener; + } + + @Override + public void setCustomBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + } + + @Override + public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { + this.overlayBackgroundColor = overlayBackgroundColor; + } + + @Override + public void setFpsOverlayTextColor(int overlayTextColor) { + this.overlayTextColor = overlayTextColor; + } + + @NonNull + @Override + public SurfaceView getSurfaceView() { + return mSurfaceView; + } + + @Override + public void resetTransparentBackground() { + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.getHolder().setFormat(PixelFormat.OPAQUE); + } + + @Override + public void setTransparentBackground() { + mSurfaceView.setZOrderOnTop(true); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); + } + + @Override + public void clearStream() { + Canvas c = null; + + try { + c = mSurfaceView.getHolder().lockCanvas(); + c.drawColor(0, PorterDuff.Mode.CLEAR); + } finally { + if (c != null) { + mSurfaceView.getHolder().unlockCanvasAndPost(c); + } else { + Log.w(TAG, "couldn't unlock surface canvas"); + } + } + } + + // no more accessible + class MjpegViewThread extends Thread { + private final SurfaceHolder mSurfaceHolder; + private int frameCounter = 0; + private Bitmap ovl; + + // no more accessible + MjpegViewThread(SurfaceHolder surfaceHolder) { + mSurfaceHolder = surfaceHolder; + } + + private Rect destRect(int bmw, int bmh) { + + int tempx; + int tempy; + if (displayMode == MjpegViewDefault.SIZE_STANDARD) { + tempx = (dispWidth / 2) - (bmw / 2); + tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); + } + if (displayMode == MjpegViewDefault.SIZE_BEST_FIT) { + float bmasp = (float) bmw / (float) bmh; + bmw = dispWidth; + bmh = (int) (dispWidth / bmasp); + if (bmh > dispHeight) { + bmh = dispHeight; + bmw = (int) (dispHeight * bmasp); + } + tempx = (dispWidth / 2) - (bmw / 2); + tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); + } + if (displayMode == MjpegViewDefault.SIZE_SCALE_FIT) { + float bmasp = ((float) bmw / (float) bmh); + tempx = 0; + tempy = 0; + if (bmw < dispWidth) { + bmw = dispWidth; + // cross-multiplication using aspect ratio + bmh = (int) (dispWidth / bmasp); + // set it to the center height + tempy = (dispHeight - bmh) / 4; + } + return new Rect(tempx, tempy, bmw, bmh + tempy); + } + if (displayMode == MjpegViewDefault.SIZE_FULLSCREEN) + return new Rect(0, 0, dispWidth, dispHeight); + return null; + } + + // no more accessible + void setSurfaceSize(int width, int height) { + synchronized (mSurfaceHolder) { + dispWidth = width; + dispHeight = height; + } + } + + private Bitmap makeFpsOverlay(Paint p, String text) { + Rect b = new Rect(); + p.getTextBounds(text, 0, text.length(), b); + int bwidth = b.width() + 2; + int bheight = b.height() + 2; + Bitmap bm = Bitmap.createBitmap(bwidth, bheight, + Bitmap.Config.ARGB_8888); + Canvas c = new Canvas(bm); + p.setColor(overlayBackgroundColor); + c.drawRect(0, 0, bwidth, bheight, p); + p.setColor(overlayTextColor); + c.drawText(text, -b.left + 1, + (bheight / 2) - ((p.ascent() + p.descent()) / 2) + 1, p); + return bm; + } + + public void run() { + long start = System.currentTimeMillis(); + PorterDuffXfermode mode = new PorterDuffXfermode( + PorterDuff.Mode.DST_OVER); + Bitmap bm; + int width; + int height; + Rect destRect; + Canvas c = null; + Paint p = new Paint(); + String fps; + while (mRun) { + if (surfaceDone) { + try { + c = mSurfaceHolder.lockCanvas(); + + if (c == null) { + Log.w(TAG, "null canvas, skipping render"); + continue; + } + synchronized (mSurfaceHolder) { + try { + byte[] header = mIn.readHeader(); + byte[] imageData = mIn.readMjpegFrame(header); + bm = BitmapFactory.decodeStream(new ByteArrayInputStream(imageData)); + if (flipHorizontal || flipVertical) + bm = flip(bm); + if (rotateDegrees != 0) + bm = rotate(bm, rotateDegrees); + + _frameCapturedWithByteData(imageData, header); + _frameCapturedWithBitmap(bm); + destRect = destRect(bm.getWidth(), + bm.getHeight()); + + if (transparentBackground) { + c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } else { + c.drawColor(backgroundColor); + } + + c.drawBitmap(bm, null, destRect, p); + + if (showFps) { + p.setXfermode(mode); + if (ovl != null) { + height = ((ovlPos & 1) == 1) ? destRect.top + : destRect.bottom + - ovl.getHeight(); + width = ((ovlPos & 8) == 8) ? destRect.left + : destRect.right + - ovl.getWidth(); + c.drawBitmap(ovl, width, height, null); + } + p.setXfermode(null); + frameCounter++; + if ((System.currentTimeMillis() - start) >= 1000) { + fps = frameCounter + + "fps"; + frameCounter = 0; + start = System.currentTimeMillis(); + ovl = makeFpsOverlay(overlayPaint, fps); + } + } + } catch (IOException e) { + Log.e(TAG, "encountered exception during render", e); + } + } + } finally { + if (c != null) { + mSurfaceHolder.unlockCanvasAndPost(c); + } else { + Log.w(TAG, "couldn't unlock surface canvas"); + } + } + } + } + } + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java new file mode 100644 index 000000000..fe66f6d6a --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java @@ -0,0 +1,462 @@ +package com.github.niqdev.mjpeg; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; + +import java.io.IOException; + +import androidx.annotation.NonNull; + +/* + * I don't really understand and want to know what the hell it does! + * Maybe one day I will refactor it ;-) + *

    + * https://bitbucket.org/neuralassembly/simplemjpegview + */ +public class MjpegViewNative extends AbstractMjpegView { + private static final String TAG = MjpegViewDefault.class.getSimpleName(); + + private final SurfaceHolder.Callback mSurfaceHolderCallback; + private final SurfaceView mSurfaceView; + private final boolean transparentBackground; + + private SurfaceHolder holder; + + private MjpegViewThread thread; + private MjpegInputStreamNative mIn = null; + private boolean showFps = false; + private boolean mRun = false; + private boolean surfaceDone = false; + + private Paint overlayPaint; + private int overlayTextColor; + private int overlayBackgroundColor; + private int backgroundColor; + private int ovlPos; + private int dispWidth; + private int dispHeight; + private int displayMode; + + private boolean suspending = false; + + private Bitmap bmp = null; + + private int IMG_WIDTH = 640; + private int IMG_HEIGHT = 480; + + MjpegViewNative(SurfaceView surfaceView, SurfaceHolder.Callback callback, boolean transparentBackground) { + this.mSurfaceView = surfaceView; + this.mSurfaceHolderCallback = callback; + this.transparentBackground = transparentBackground; + init(); + } + + private void init() { + + //SurfaceHolder holder = getHolder(); + holder = mSurfaceView.getHolder(); + holder.addCallback(mSurfaceHolderCallback); + thread = new MjpegViewThread(holder); + mSurfaceView.setFocusable(true); + overlayPaint = new Paint(); + overlayPaint.setTextAlign(Paint.Align.LEFT); + overlayPaint.setTextSize(12); + overlayPaint.setTypeface(Typeface.DEFAULT); + overlayTextColor = Color.WHITE; + overlayBackgroundColor = Color.BLACK; + backgroundColor = Color.BLACK; + ovlPos = MjpegViewNative.POSITION_LOWER_RIGHT; + displayMode = MjpegViewNative.SIZE_STANDARD; + dispWidth = mSurfaceView.getWidth(); + dispHeight = mSurfaceView.getHeight(); + } + + /* all methods/constructors below are no more accessible */ + + void _startPlayback() { + if (mIn != null) { + mRun = true; + if (thread == null) { + thread = new MjpegViewThread(holder); + } + thread.start(); + } + } + + void _resumePlayback() { + if (suspending) { + if (mIn != null) { + mRun = true; + SurfaceHolder holder = mSurfaceView.getHolder(); + holder.addCallback(mSurfaceHolderCallback); + thread = new MjpegViewThread(holder); + thread.start(); + suspending = false; + } + } + } + + void _stopPlayback() { + if (mRun) { + suspending = true; + } + mRun = false; + if (thread != null) { + boolean retry = true; + while (retry) { + try { + thread.join(); + retry = false; + } catch (InterruptedException ignored) { + } + } + thread = null; + } + if (mIn != null) { + try { + mIn.close(); + } catch (IOException ignored) { + } + mIn = null; + } + + } + + void _freeCameraMemory() { + if (mIn != null) { + mIn.freeCameraMemory(); + } + } + + void _surfaceChanged(int w, int h) { + if (thread != null) { + thread.setSurfaceSize(w, h); + } + } + + void _surfaceDestroyed() { + surfaceDone = false; + _stopPlayback(); + if (thread != null) { + thread = null; + } + } + + void _surfaceCreated() { + surfaceDone = true; + } + + void _showFps(boolean b) { + showFps = b; + } + + void _setSource(MjpegInputStreamNative source) { + mIn = source; + if (!suspending) { + _startPlayback(); + } else { + _resumePlayback(); + } + } + + void _setOverlayPaint(Paint p) { + overlayPaint = p; + } + + void _setOverlayTextColor(int c) { + overlayTextColor = c; + } + + void _setOverlayBackgroundColor(int c) { + overlayBackgroundColor = c; + } + + void _setOverlayPosition(int p) { + ovlPos = p; + } + + void _setDisplayMode(int s) { + displayMode = s; + } + + void _setResolution(int w, int h) { + IMG_WIDTH = w; + IMG_HEIGHT = h; + } + + boolean _isStreaming() { + return mRun; + } + + @Override + public void onSurfaceCreated(@NonNull SurfaceHolder holder) { + _surfaceCreated(); + } + + /* override methods */ + + @Override + public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + _surfaceChanged(width, height); + } + + @Override + public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { + _surfaceDestroyed(); + } + + @Override + public void setSource(@NonNull MjpegInputStream stream) { + if (!(stream instanceof MjpegInputStreamNative)) { + throw new IllegalArgumentException("stream must be an instance of MjpegInputStreamNative"); + } + _setSource((MjpegInputStreamNative) stream); + } + + @Override + public void setDisplayMode(DisplayMode mode) { + _setDisplayMode(mode.getValue()); + } + + @Override + public void showFps(boolean show) { + _showFps(show); + } + + @Override + public void flipSource(boolean flip) { + flipHorizontal(flip); + } + + @Override + public void flipHorizontal(boolean flip) { + + } + + @Override + public void flipVertical(boolean flip) { + + } + + @Override + public void setRotate(float degrees) { + + } + + @Override + public void stopPlayback() { + _stopPlayback(); + } + + @Override + public boolean isStreaming() { + return _isStreaming(); + } + + @Override + public void setResolution(int width, int height) { + _setResolution(width, height); + } + + @Override + public void freeCameraMemory() { + _freeCameraMemory(); + } + + @Override + public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { + throw new UnsupportedOperationException("Not implemented yet!"); + } + + @Override + public void setCustomBackgroundColor(int backgroundColor) { + this.backgroundColor = backgroundColor; + } + + @Override + public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { + this.overlayBackgroundColor = overlayBackgroundColor; + } + + @Override + public void setFpsOverlayTextColor(int overlayTextColor) { + this.overlayTextColor = overlayTextColor; + } + + @NonNull + @Override + public SurfaceView getSurfaceView() { + return mSurfaceView; + } + + @Override + public void resetTransparentBackground() { + mSurfaceView.setZOrderOnTop(false); + mSurfaceView.getHolder().setFormat(PixelFormat.OPAQUE); + } + + @Override + public void setTransparentBackground() { + mSurfaceView.setZOrderOnTop(true); + mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); + } + + @Override + public void clearStream() { + Canvas c = null; + + try { + c = mSurfaceView.getHolder().lockCanvas(); + c.drawColor(0, PorterDuff.Mode.CLEAR); + } finally { + if (c != null) { + mSurfaceView.getHolder().unlockCanvasAndPost(c); + } else { + Log.w(TAG, "couldn't unlock surface canvas"); + } + } + } + + // no more accessible + class MjpegViewThread extends Thread { + private final SurfaceHolder mSurfaceHolder; + private int frameCounter = 0; + private String fps = ""; + + // no more accessible + MjpegViewThread(SurfaceHolder surfaceHolder) { + mSurfaceHolder = surfaceHolder; + } + + private Rect destRect(int bmw, int bmh) { + int tempx; + int tempy; + if (displayMode == MjpegViewNative.SIZE_STANDARD) { + tempx = (dispWidth / 2) - (bmw / 2); + tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); + } + if (displayMode == MjpegViewNative.SIZE_BEST_FIT) { + float bmasp = (float) bmw / (float) bmh; + bmw = dispWidth; + bmh = (int) (dispWidth / bmasp); + if (bmh > dispHeight) { + bmh = dispHeight; + bmw = (int) (dispHeight * bmasp); + } + tempx = (dispWidth / 2) - (bmw / 2); + tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); + } + if (displayMode == MjpegViewNative.SIZE_FULLSCREEN) + return new Rect(0, 0, dispWidth, dispHeight); + return null; + } + + // no more accessible + void setSurfaceSize(int width, int height) { + synchronized (mSurfaceHolder) { + dispWidth = width; + dispHeight = height; + } + } + + private Bitmap makeFpsOverlay(Paint p) { + Rect b = new Rect(); + p.getTextBounds(fps, 0, fps.length(), b); + + // false indentation to fix forum layout + Bitmap bm = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); + + Canvas c = new Canvas(bm); + p.setColor(overlayBackgroundColor); + c.drawRect(0, 0, b.width(), b.height(), p); + p.setColor(overlayTextColor); + c.drawText(fps, -b.left, b.bottom - b.top - p.descent(), p); + return bm; + } + + public void run() { + long start = System.currentTimeMillis(); + PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.DST_OVER); + + int width; + int height; + Paint p = new Paint(); + Bitmap ovl = null; + + while (mRun) { + + Rect destRect; + Canvas c = null; + + if (surfaceDone) { + try { + if (bmp == null) { + bmp = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888); + } + int ret = mIn.readMjpegFrame(bmp); + + if (ret == -1) { + // TODO error + //((MjpegActivity) saved_context).setImageError(); + return; + } + + destRect = destRect(bmp.getWidth(), bmp.getHeight()); + + c = mSurfaceHolder.lockCanvas(); + synchronized (mSurfaceHolder) { + if (transparentBackground) { + c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + } else { + c.drawColor(backgroundColor); + } + + c.drawBitmap(bmp, null, destRect, p); + + if (showFps) { + p.setXfermode(mode); + if (ovl != null) { + + // false indentation to fix forum layout + height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom - ovl.getHeight(); + width = ((ovlPos & 8) == 8) ? destRect.left : destRect.right - ovl.getWidth(); + + c.drawBitmap(ovl, width, height, null); + } + p.setXfermode(null); + frameCounter++; + if ((System.currentTimeMillis() - start) >= 1000) { + fps = frameCounter + "fps"; + frameCounter = 0; + start = System.currentTimeMillis(); + if (ovl != null) ovl.recycle(); + + ovl = makeFpsOverlay(overlayPaint); + } + } + + + } + + } catch (IOException ignored) { + + } finally { + if (c != null) mSurfaceHolder.unlockCanvasAndPost(c); + } + } + } + } + } +} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt new file mode 100644 index 000000000..dec78d044 --- /dev/null +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt @@ -0,0 +1,10 @@ +package com.github.niqdev.mjpeg + +import android.graphics.Bitmap + +interface OnFrameCapturedListener { + fun onFrameCaptured(bitmap: Bitmap) + + //return the byte data for recording + fun onFrameCapturedWithHeader(bitmap: ByteArray, header: ByteArray) +} diff --git a/ipcam-view/src/main/res/values/attrs.xml b/ipcam-view/src/main/res/values/attrs.xml new file mode 100644 index 000000000..32d131998 --- /dev/null +++ b/ipcam-view/src/main/res/values/attrs.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/material-intro/build.gradle.kts b/material-intro/build.gradle.kts index 4cd66e51a..a9f1bf6dd 100644 --- a/material-intro/build.gradle.kts +++ b/material-intro/build.gradle.kts @@ -51,7 +51,7 @@ android { disable += listOf("Overdraw", "OldTargetApi", "GradleDependency") showAll = true warningsAsErrors = true - targetSdk = 36 + targetSdk = versions.getProperty("platformVersions").toInt() } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 5b6101bcd..589182d17 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,4 +39,5 @@ dependencyResolutionManagement { } include(":app") -include(":material-intro") \ No newline at end of file +include(":material-intro") +include(":ipcam-view") From a925e6ac15c4981e60b4f4f621638b1679a798b3 Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 19:30:09 +0100 Subject: [PATCH 5/6] Further clean up of ipcam view --- .../zeus/hydra/wpi/cammie/CammieActivity.java | 2 - .../wpi/cammie/FullScreenCammieActivity.java | 2 - .../res/layout-land/activity_wpi_cammie.xml | 3 +- .../main/res/layout/activity_wpi_cammie.xml | 3 +- .../activity_wpi_full_screen_cammie.xml | 3 +- .../com/github/niqdev/mjpeg/DisplayMode.kt | 5 - .../java/com/github/niqdev/mjpeg/Mjpeg.java | 98 +--- ...reamDefault.java => MjpegInputStream.java} | 14 +- .../github/niqdev/mjpeg/MjpegInputStream.kt | 6 - .../niqdev/mjpeg/MjpegInputStreamNative.java | 174 ------- .../niqdev/mjpeg/MjpegRecordingHandler.kt | 118 ----- .../github/niqdev/mjpeg/MjpegSurfaceView.java | 131 +---- .../java/com/github/niqdev/mjpeg/MjpegView.kt | 10 - .../github/niqdev/mjpeg/MjpegViewDefault.java | 337 +++---------- .../github/niqdev/mjpeg/MjpegViewNative.java | 462 ------------------ .../niqdev/mjpeg/OnFrameCapturedListener.kt | 10 - ipcam-view/src/main/res/values/attrs.xml | 11 +- 17 files changed, 86 insertions(+), 1303 deletions(-) delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/DisplayMode.kt rename ipcam-view/src/main/java/com/github/niqdev/mjpeg/{MjpegInputStreamDefault.java => MjpegInputStream.java} (87%) delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java delete mode 100644 ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java index d4e6fbd5c..af607d999 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/CammieActivity.java @@ -38,7 +38,6 @@ import be.ugent.zeus.hydra.common.network.NetworkState; import be.ugent.zeus.hydra.common.ui.BaseActivity; import be.ugent.zeus.hydra.databinding.ActivityWpiCammieBinding; -import com.github.niqdev.mjpeg.DisplayMode; import com.github.niqdev.mjpeg.Mjpeg; /** @@ -118,7 +117,6 @@ private void loadMjpeg() { .open(Endpoints.CAMMIE, 5) .subscribe(inputStream -> { binding.cammieViewer.setSource(inputStream); - binding.cammieViewer.setDisplayMode(DisplayMode.BEST_FIT); binding.cammieViewer.showFps(true); }); } diff --git a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java index 51931c433..6f6ef87ef 100644 --- a/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java +++ b/app/src/main/java/be/ugent/zeus/hydra/wpi/cammie/FullScreenCammieActivity.java @@ -31,7 +31,6 @@ import be.ugent.zeus.hydra.common.network.Endpoints; import be.ugent.zeus.hydra.common.ui.BaseActivity; import be.ugent.zeus.hydra.databinding.ActivityWpiFullScreenCammieBinding; -import com.github.niqdev.mjpeg.DisplayMode; import com.github.niqdev.mjpeg.Mjpeg; /** @@ -94,7 +93,6 @@ private void loadMjpeg() { .open(Endpoints.CAMMIE, 5) .subscribe(inputStream -> { binding.cammieViewer.setSource(inputStream); - binding.cammieViewer.setDisplayMode(DisplayMode.BEST_FIT); binding.cammieViewer.showFps(true); }); } diff --git a/app/src/main/res/layout-land/activity_wpi_cammie.xml b/app/src/main/res/layout-land/activity_wpi_cammie.xml index 040a8e0e4..58b3b4a93 100644 --- a/app/src/main/res/layout-land/activity_wpi_cammie.xml +++ b/app/src/main/res/layout-land/activity_wpi_cammie.xml @@ -53,8 +53,7 @@ app:layout_constraintDimensionRatio="H,20:11" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintWidth_percent="0.5" - stream:type="stream_default" /> + app:layout_constraintWidth_percent="0.5" /> + app:layout_constraintTop_toTopOf="parent" /> + android:layout_height="match_parent" /> java.net.ProtocolException: Unexpected status line - * - * @return Observable Mjpeg stream - */ - public Mjpeg sendConnectionCloseHeader() { - sendConnectionCloseHeader = true; - return this; + return new Mjpeg(); } @NonNull @@ -106,14 +34,7 @@ private Observable connect(String url) { HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); loadConnectionProperties(urlConnection); InputStream inputStream = urlConnection.getInputStream(); - switch (type) { - // handle multiple implementations - case DEFAULT: - return Observable.just(new MjpegInputStreamDefault(inputStream)); - case NATIVE: - return Observable.just(new MjpegInputStreamNative(inputStream)); - } - throw new IllegalStateException("invalid type"); + return Observable.just(new MjpegInputStream(inputStream)); } catch (IOException e) { Log.e(TAG, "error during connection", e); return Observable.error(e); @@ -154,20 +75,5 @@ public Observable open(String url, int timeout) { */ private void loadConnectionProperties(HttpURLConnection urlConnection) { urlConnection.setRequestProperty("Cache-Control", "no-cache"); - if (sendConnectionCloseHeader) { - urlConnection.setRequestProperty("Connection", "close"); - } - - if (!msCookieManager.getCookieStore().getCookies().isEmpty()) { - urlConnection.setRequestProperty("Cookie", - TextUtils.join(";", msCookieManager.getCookieStore().getCookies())); - } - } - - /** - * Library implementation type - */ - public enum Type { - DEFAULT, NATIVE } } diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java similarity index 87% rename from ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java rename to ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java index 9850b1f77..91b2d8f38 100644 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamDefault.java +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.java @@ -3,11 +3,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; +import java.io.*; import java.util.Properties; /* @@ -16,16 +12,14 @@ *

    * https://code.google.com/archive/p/android-camera-axis */ -public class MjpegInputStreamDefault extends MjpegInputStream { +public class MjpegInputStream extends DataInputStream { private final static int HEADER_MAX_LENGTH = 100; private final static int FRAME_MAX_LENGTH = 200000 + HEADER_MAX_LENGTH; private final byte[] SOI_MARKER = {(byte) 0xFF, (byte) 0xD8}; private final byte[] EOF_MARKER = {(byte) 0xFF, (byte) 0xD9}; - private final String CONTENT_LENGTH = "Content-Length"; private int mContentLength = -1; - // no more accessible - MjpegInputStreamDefault(InputStream in) { + MjpegInputStream(InputStream in) { super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); } @@ -55,7 +49,7 @@ private int parseContentLength(byte[] headerBytes) throws IOException, IllegalAr ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); Properties props = new Properties(); props.load(headerIn); - return Integer.parseInt(props.getProperty(CONTENT_LENGTH)); + return Integer.parseInt(props.getProperty("Content-Length")); } byte[] readHeader() throws IOException { diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt deleted file mode 100644 index 1fcd2094a..000000000 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStream.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.niqdev.mjpeg - -import java.io.DataInputStream -import java.io.InputStream - -abstract class MjpegInputStream(insputStream: InputStream) : DataInputStream(insputStream) \ No newline at end of file diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java deleted file mode 100644 index 5ebf86c4d..000000000 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegInputStreamNative.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.github.niqdev.mjpeg; - -import android.graphics.Bitmap; -import android.util.Log; - -import java.io.BufferedInputStream; -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -/* - * I don't really understand and want to know what the hell it does! - * Maybe one day I will refactor it ;-) - *

    - * https://bitbucket.org/neuralassembly/simplemjpegview - */ -public class MjpegInputStreamNative extends MjpegInputStream { - - //private final static int FRAME_MAX_LENGTH = 40000 + HEADER_MAX_LENGTH; - private final static int FRAME_MAX_LENGTH = 200000; - private static final String TAG = "MJPEG"; - private static final boolean DEBUG = false; - - static { - System.loadLibrary("ImageProc"); - } - - private final byte[] SOI_MARKER = {(byte) 0xFF, (byte) 0xD8}; - private final byte[] EOF_MARKER = {(byte) 0xFF, (byte) 0xD9}; - private final String CONTENT_LENGTH = "Content-Length"; - byte[] header = null; - byte[] frameData = null; - int headerLen = -1; - int headerLenPrev = -1; - int skip = 1; - int count = 0; - private int mContentLength = -1; - - // no more accessible - MjpegInputStreamNative(InputStream in) { - super(new BufferedInputStream(in, FRAME_MAX_LENGTH)); - } - - public native int pixeltobmp(byte[] jp, int l, Bitmap bmp); - - public native void freeCameraMemory(); - - private int getEndOfSeqeunce(DataInputStream in, byte[] sequence) - throws IOException { - - int seqIndex = 0; - byte c; - for (int i = 0; i < FRAME_MAX_LENGTH; i++) { - c = (byte) in.readUnsignedByte(); - if (c == sequence[seqIndex]) { - seqIndex++; - if (seqIndex == sequence.length) { - - return i + 1; - } - } else seqIndex = 0; - } - - return -1; - } - - private int getStartOfSequence(DataInputStream in, byte[] sequence) - throws IOException { - int end = getEndOfSeqeunce(in, sequence); - return (end < 0) ? (-1) : (end - sequence.length); - } - - private int getEndOfSeqeunceSimplified(DataInputStream in, byte[] sequence) - throws IOException { - int startPos = mContentLength / 2; - int endPos = 3 * mContentLength / 2; - - skipBytes(headerLen + startPos); - - int seqIndex = 0; - byte c; - for (int i = 0; i < endPos - startPos; i++) { - c = (byte) in.readUnsignedByte(); - if (c == sequence[seqIndex]) { - seqIndex++; - if (seqIndex == sequence.length) { - - return headerLen + startPos + i + 1; - } - } else seqIndex = 0; - } - - return -1; - } - - private int parseContentLength(byte[] headerBytes) - throws IOException, IllegalArgumentException { - ByteArrayInputStream headerIn = new ByteArrayInputStream(headerBytes); - Properties props = new Properties(); - props.load(headerIn); - return Integer.parseInt(props.getProperty(CONTENT_LENGTH)); - } - - // no more accessible - int readMjpegFrame(Bitmap bmp) throws IOException { - mark(FRAME_MAX_LENGTH); - int headerLen; - try { - headerLen = getStartOfSequence(this, SOI_MARKER); - } catch (IOException e) { - if (DEBUG) Log.d(TAG, "IOException in betting headerLen."); - reset(); - return -1; - } - reset(); - - if (header == null || headerLen != headerLenPrev) { - header = new byte[headerLen]; - if (DEBUG) Log.d(TAG, "header renewed " + headerLenPrev + " -> " + headerLen); - } - headerLenPrev = headerLen; - readFully(header); - - int ContentLengthNew; - try { - ContentLengthNew = parseContentLength(header); - } catch (NumberFormatException nfe) { - ContentLengthNew = getEndOfSeqeunceSimplified(this, EOF_MARKER); - - if (ContentLengthNew < 0) { - if (DEBUG) Log.d(TAG, "Worst case for finding EOF_MARKER"); - reset(); - ContentLengthNew = getEndOfSeqeunce(this, EOF_MARKER); - } - } catch (IllegalArgumentException e) { - if (DEBUG) Log.d(TAG, "IllegalArgumentException in parseContentLength"); - ContentLengthNew = getEndOfSeqeunceSimplified(this, EOF_MARKER); - - if (ContentLengthNew < 0) { - if (DEBUG) Log.d(TAG, "Worst case for finding EOF_MARKER"); - reset(); - ContentLengthNew = getEndOfSeqeunce(this, EOF_MARKER); - } - } catch (IOException e) { - if (DEBUG) Log.d(TAG, "IOException in parseContentLength"); - reset(); - return -1; - } - mContentLength = ContentLengthNew; - reset(); - - if (frameData == null || mContentLength > frameData.length) { - frameData = new byte[mContentLength]; // + HEADER_MAX_LENGTH]; - if (DEBUG) Log.d(TAG, "frameData renewed cl=" + mContentLength); - } - - skipBytes(headerLen); - - readFully(frameData, 0, mContentLength); - - if (count++ % skip == 0) { - return pixeltobmp(frameData, mContentLength, bmp); - } else { - return 0; - } - } - - // no more accessible - void setSkip(int s) { - skip = s; - } -} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt deleted file mode 100644 index 46a651f14..000000000 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegRecordingHandler.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.github.niqdev.mjpeg - -import android.annotation.SuppressLint -import android.content.Context -import android.graphics.Bitmap -import android.util.Log -import android.widget.Toast -import java.io.* -import java.text.SimpleDateFormat -import java.util.* - -class MjpegRecordingHandler(private val context: Context) : OnFrameCapturedListener { - private var bos: BufferedOutputStream? = null - var isRecording = false - var lastBitmap: Bitmap? = null - private set - - /** - * start recording the live image - */ - fun startRecording() { - try { - val mjpegFilePath = createMjpegFile()!!.absolutePath - val fos = FileOutputStream(mjpegFilePath) - bos = BufferedOutputStream(fos) - Toast.makeText(context, "start recording, file path is:$mjpegFilePath", Toast.LENGTH_LONG).show() - isRecording = true - } catch (e: FileNotFoundException) { - Log.e(TAG, e.message.toString()) - } - } - - /** - * stop recording the live image - */ - fun stopRecording() { - isRecording = false - } - - /** - * save the last acquired bitmap into jpg file. - */ - fun saveBitmapToFile() { - val fos: FileOutputStream - val bos: BufferedOutputStream - val imagePath = createJpgFile()!!.absolutePath - try { - fos = FileOutputStream(imagePath) - bos = BufferedOutputStream(fos) - val jpegByteArrayOutputStream = ByteArrayOutputStream() - lastBitmap?.compress(Bitmap.CompressFormat.JPEG, 75, jpegByteArrayOutputStream) - val jpegByteArray = jpegByteArrayOutputStream.toByteArray() - bos.write(jpegByteArray) - bos.flush() - Toast.makeText(context, "saved image:$imagePath", Toast.LENGTH_LONG).show() - } catch (e: IOException) { - Log.e(TAG, e.message.toString()) - } - } - - /** - * Create jpg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files - * - * @return File - */ - private fun createJpgFile(): File? { - return createSavingFile("photo", "jpg") - } - - private fun createSavingFile(prefix: String, extension: String): File? { - val date = Date() - - @SuppressLint("SimpleDateFormat") - val sdf = SimpleDateFormat("yyyyMMddHHmmss") - val szFileName = prefix + "-" + sdf.format(date) - try { - val path = context.getExternalFilesDir(null)!!.path + "/" + szFileName + "." + extension - val file = File(path) - if (!file.exists()) { - file.createNewFile() - } - Log.d(TAG, "file path is " + file.absolutePath) - return file - } catch (e: IOException) { - Log.e(TAG, e.message.toString()) - } - return null - } - - /** - * Create mjpeg file in app external cache directory. the directory path is /sdcard/Android/data/com.github.niqdev.ipcam/files - * - * @return File - */ - private fun createMjpegFile(): File? { - return createSavingFile("video", "mjpeg") - } - - override fun onFrameCaptured(bitmap: Bitmap) { - lastBitmap = bitmap - } - - override fun onFrameCapturedWithHeader(bitmap: ByteArray, header: ByteArray) { - if (isRecording) { - try { - bos!!.write(header) - bos!!.write(bitmap) - bos!!.flush() - } catch (e: IOException) { - Log.e(TAG, e.message!!) - } - } - } - - companion object { - private const val TAG = "MjpegRecordingHandler" - } -} \ No newline at end of file diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java index e635f6883..18de92834 100644 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegSurfaceView.java @@ -1,99 +1,34 @@ package com.github.niqdev.mjpeg; import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.PixelFormat; import android.util.AttributeSet; -import android.util.SparseArray; import android.view.SurfaceHolder; import android.view.SurfaceView; import androidx.annotation.NonNull; -import androidx.annotation.StyleableRes; public class MjpegSurfaceView extends SurfaceView implements SurfaceHolder.Callback, MjpegView { - private static final int DEFAULT_TYPE = 0; - // issue in attrs.xml - verify reserved keywords - private static final SparseArray TYPE; - - static { - TYPE = new SparseArray<>(); - TYPE.put(0, Mjpeg.Type.DEFAULT); - TYPE.put(1, Mjpeg.Type.NATIVE); - } - - private MjpegView mMjpegView; + private final AbstractMjpegView mMjpegView; public MjpegSurfaceView(Context context, AttributeSet attrs) { super(context, attrs); - boolean transparentBackground = getPropertyBoolean(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_transparentBackground); - int backgroundColor = getPropertyColor(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_backgroundColor); - - if (transparentBackground) { - setZOrderOnTop(true); - getHolder().setFormat(PixelFormat.TRANSPARENT); - } - - switch (getPropertyType(attrs, R.styleable.MjpegSurfaceView, R.styleable.MjpegSurfaceView_type)) { - case DEFAULT: - mMjpegView = new MjpegViewDefault(this, this, transparentBackground); - break; - case NATIVE: - mMjpegView = new MjpegViewNative(this, this, transparentBackground); - break; - } - - if (mMjpegView != null && backgroundColor != -1) { - this.setCustomBackgroundColor(backgroundColor); - } - } - - public Mjpeg.Type getPropertyType(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { - TypedArray typedArray = getContext().getTheme() - .obtainStyledAttributes(attributeSet, attrs, 0, 0); - try { - int typeIndex = typedArray.getInt(attrIndex, DEFAULT_TYPE); - Mjpeg.Type type = TYPE.get(typeIndex); - return type != null ? type : TYPE.get(DEFAULT_TYPE); - } finally { - typedArray.recycle(); - } - } - - public boolean getPropertyBoolean(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { - TypedArray typedArray = getContext().getTheme() - .obtainStyledAttributes(attributeSet, attrs, 0, 0); - try { - return typedArray.getBoolean(attrIndex, false); - } finally { - typedArray.recycle(); - } - } - - public int getPropertyColor(AttributeSet attributeSet, @StyleableRes int[] attrs, int attrIndex) { - TypedArray typedArray = getContext().getTheme() - .obtainStyledAttributes(attributeSet, attrs, 0, 0); - try { - return typedArray.getColor(attrIndex, -1); - } finally { - typedArray.recycle(); - } + mMjpegView = new MjpegViewDefault(this, this); } @Override - public void surfaceCreated(SurfaceHolder holder) { - ((AbstractMjpegView) mMjpegView).onSurfaceCreated(holder); + public void surfaceCreated(@NonNull SurfaceHolder holder) { + mMjpegView.onSurfaceCreated(holder); } @Override - public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { - ((AbstractMjpegView) mMjpegView).onSurfaceChanged(holder, format, width, height); + public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { + mMjpegView.onSurfaceChanged(holder, format, width, height); } @Override - public void surfaceDestroyed(SurfaceHolder holder) { - ((AbstractMjpegView) mMjpegView).onSurfaceDestroyed(holder); + public void surfaceDestroyed(@NonNull SurfaceHolder holder) { + mMjpegView.onSurfaceDestroyed(holder); } @Override @@ -101,11 +36,6 @@ public void setSource(@NonNull MjpegInputStream stream) { mMjpegView.setSource(stream); } - @Override - public void setDisplayMode(@NonNull DisplayMode mode) { - mMjpegView.setDisplayMode(mode); - } - @Override public void showFps(boolean show) { mMjpegView.showFps(show); @@ -141,54 +71,9 @@ public boolean isStreaming() { return mMjpegView.isStreaming(); } - @Override - public void setResolution(int width, int height) { - mMjpegView.setResolution(width, height); - } - - @Override - public void freeCameraMemory() { - mMjpegView.freeCameraMemory(); - } - - @Override - public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { - mMjpegView.setOnFrameCapturedListener(onFrameCapturedListener); - } - - @Override - public void setCustomBackgroundColor(int backgroundColor) { - mMjpegView.setCustomBackgroundColor(backgroundColor); - } - - @Override - public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { - mMjpegView.setFpsOverlayBackgroundColor(overlayBackgroundColor); - } - - @Override - public void setFpsOverlayTextColor(int overlayTextColor) { - mMjpegView.setFpsOverlayTextColor(overlayTextColor); - } - @NonNull @Override public SurfaceView getSurfaceView() { return this; } - - @Override - public void resetTransparentBackground() { - mMjpegView.resetTransparentBackground(); - } - - @Override - public void setTransparentBackground() { - mMjpegView.setTransparentBackground(); - } - - @Override - public void clearStream() { - mMjpegView.clearStream(); - } } diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt index bc92a16da..420bbba9a 100644 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegView.kt @@ -4,7 +4,6 @@ import android.view.SurfaceView interface MjpegView { fun setSource(stream: MjpegInputStream) - fun setDisplayMode(mode: DisplayMode) fun showFps(show: Boolean) fun flipSource(flip: Boolean) fun flipHorizontal(flip: Boolean) @@ -12,14 +11,5 @@ interface MjpegView { fun setRotate(degrees: Float) fun stopPlayback() val isStreaming: Boolean - fun setResolution(width: Int, height: Int) - fun freeCameraMemory() - fun setOnFrameCapturedListener(onFrameCapturedListener: OnFrameCapturedListener) - fun setCustomBackgroundColor(backgroundColor: Int) - fun setFpsOverlayBackgroundColor(overlayBackgroundColor: Int) - fun setFpsOverlayTextColor(overlayTextColor: Int) val surfaceView: SurfaceView - fun resetTransparentBackground() - fun setTransparentBackground() - fun clearStream() } diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java index 6f4d488c3..cb08430fc 100644 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java +++ b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewDefault.java @@ -1,26 +1,15 @@ package com.github.niqdev.mjpeg; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.Typeface; +import android.graphics.*; import android.util.DisplayMetrics; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; +import androidx.annotation.NonNull; import java.io.ByteArrayInputStream; import java.io.IOException; -import androidx.annotation.NonNull; - /* * I don't really understand and want to know what the hell it does! * Maybe one day I will refactor it ;-) @@ -32,10 +21,8 @@ public class MjpegViewDefault extends AbstractMjpegView { private final SurfaceHolder.Callback mSurfaceHolderCallback; private final SurfaceView mSurfaceView; - private final boolean transparentBackground; - private MjpegViewThread thread; - private MjpegInputStreamDefault mIn = null; + private MjpegInputStream mIn = null; private boolean showFps = false; private boolean flipHorizontal = false; private boolean flipVertical = false; @@ -43,21 +30,14 @@ public class MjpegViewDefault extends AbstractMjpegView { private volatile boolean mRun = false; private volatile boolean surfaceDone = false; private Paint overlayPaint; - private int overlayTextColor; - private int overlayBackgroundColor; - private int backgroundColor; private int ovlPos; private int dispWidth; private int dispHeight; - private int displayMode; private boolean resume = false; - private OnFrameCapturedListener onFrameCapturedListener; - - MjpegViewDefault(SurfaceView surfaceView, SurfaceHolder.Callback callback, boolean transparentBackground) { + MjpegViewDefault(SurfaceView surfaceView, SurfaceHolder.Callback callback) { this.mSurfaceView = surfaceView; this.mSurfaceHolderCallback = callback; - this.transparentBackground = transparentBackground; init(); } @@ -89,11 +69,7 @@ private void init() { overlayPaint.setTextAlign(Paint.Align.LEFT); overlayPaint.setTextSize(12); overlayPaint.setTypeface(Typeface.DEFAULT); - overlayTextColor = Color.WHITE; - overlayBackgroundColor = Color.BLACK; - backgroundColor = Color.BLACK; ovlPos = MjpegViewDefault.POSITION_LOWER_RIGHT; - displayMode = MjpegViewDefault.SIZE_STANDARD; dispWidth = mSurfaceView.getWidth(); dispHeight = mSurfaceView.getHeight(); } @@ -108,6 +84,7 @@ void _startPlayback() { * clear canvas cache * @see https://github.com/niqdev/ipcam-view/issues/14 */ + //noinspection deprecation mSurfaceView.destroyDrawingCache(); thread.start(); } @@ -119,82 +96,31 @@ void _resumePlayback() { thread.start(); } - /* - * @see https://github.com/niqdev/ipcam-view/issues/14 - */ - synchronized void _stopPlayback() { - mRun = false; - boolean retry = true; - while (retry) { - try { - // make sure the thread is not null - if (thread != null) { - thread.join(500); - } - retry = false; - } catch (InterruptedException e) { - Log.e(TAG, "error stopping playback thread", e); - } - } - - // close the connection - if (mIn != null) { - try { - mIn.close(); - } catch (IOException e) { - Log.e(TAG, "error closing input stream", e); - } - mIn = null; - } + @Override + public void onSurfaceCreated(@NonNull SurfaceHolder holder) { + surfaceDone = true; } - void _surfaceChanged(int w, int h) { + @Override + public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { if (thread != null) { - thread.setSurfaceSize(w, h); + thread.setSurfaceSize(width, height); } } - void _surfaceDestroyed() { + @Override + public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { surfaceDone = false; - _stopPlayback(); + stopPlayback(); if (thread != null) { thread = null; } } - void _frameCapturedWithByteData(byte[] imageByte, byte[] header) { - if (onFrameCapturedListener != null) { - onFrameCapturedListener.onFrameCapturedWithHeader(imageByte, header); - } - } - - void _frameCapturedWithBitmap(Bitmap bitmap) { - if (onFrameCapturedListener != null) { - onFrameCapturedListener.onFrameCaptured(bitmap); - } - } - - void _surfaceCreated() { - surfaceDone = true; - } - - void _showFps(boolean b) { - showFps = b; - } - - void _flipHorizontal(boolean b) { - flipHorizontal = b; - } - - void _flipVertical(boolean b) { - flipVertical = b; - } - - /* - * @see https://github.com/niqdev/ipcam-view/issues/14 - */ - void _setSource(MjpegInputStreamDefault source) { - mIn = source; + @Override + public void setSource(@NonNull MjpegInputStream stream) { + // see https://github.com/niqdev/ipcam-view/issues/14 + mIn = stream; // make sure resume is calling _resumePlayback() if (!resume) { _startPlayback(); @@ -203,74 +129,24 @@ void _setSource(MjpegInputStreamDefault source) { } } - void setOverlayPaint(Paint p) { - overlayPaint = p; - } - - void setOverlayTextColor(int c) { - overlayTextColor = c; - } - - void setOverlayBackgroundColor(int c) { - overlayBackgroundColor = c; - } - - void setOverlayPosition(int p) { - ovlPos = p; - } - - void setDisplayMode(int s) { - displayMode = s; - } - - @Override - public void onSurfaceCreated(@NonNull SurfaceHolder holder) { - _surfaceCreated(); - } - - /* override methods */ - - @Override - public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { - _surfaceChanged(width, height); - } - - @Override - public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { - _surfaceDestroyed(); - } - - @Override - public void setSource(@NonNull MjpegInputStream stream) { - if (!(stream instanceof MjpegInputStreamDefault)) { - throw new IllegalArgumentException("stream must be an instance of MjpegInputStreamDefault"); - } - _setSource((MjpegInputStreamDefault) stream); - } - - @Override - public void setDisplayMode(DisplayMode mode) { - setDisplayMode(mode.getValue()); - } - @Override public void showFps(boolean show) { - _showFps(show); + showFps = show; } @Override public void flipSource(boolean flip) { - _flipHorizontal(flip); + flipHorizontal(flip); } @Override public void flipHorizontal(boolean flip) { - _flipHorizontal(flip); + this.flipHorizontal = flip; } @Override public void flipVertical(boolean flip) { - _flipVertical(flip); + this.flipVertical = flip; } @Override @@ -279,8 +155,30 @@ public void setRotate(float degrees) { } @Override - public void stopPlayback() { - _stopPlayback(); + public synchronized void stopPlayback() { + mRun = false; + boolean retry = true; + while (retry) { + try { + // make sure the thread is not null + if (thread != null) { + thread.join(500); + } + retry = false; + } catch (InterruptedException e) { + Log.e(TAG, "error stopping playback thread", e); + } + } + + // close the connection + if (mIn != null) { + try { + mIn.close(); + } catch (IOException e) { + Log.e(TAG, "error closing input stream", e); + } + mIn = null; + } } @Override @@ -288,121 +186,34 @@ public boolean isStreaming() { return mRun; } - @Override - public void setResolution(int width, int height) { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public void freeCameraMemory() { - throw new UnsupportedOperationException("not implemented"); - } - - @Override - public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { - this.onFrameCapturedListener = onFrameCapturedListener; - } - - @Override - public void setCustomBackgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - } - - @Override - public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { - this.overlayBackgroundColor = overlayBackgroundColor; - } - - @Override - public void setFpsOverlayTextColor(int overlayTextColor) { - this.overlayTextColor = overlayTextColor; - } - @NonNull @Override public SurfaceView getSurfaceView() { return mSurfaceView; } - @Override - public void resetTransparentBackground() { - mSurfaceView.setZOrderOnTop(false); - mSurfaceView.getHolder().setFormat(PixelFormat.OPAQUE); - } - - @Override - public void setTransparentBackground() { - mSurfaceView.setZOrderOnTop(true); - mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); - } - - @Override - public void clearStream() { - Canvas c = null; - - try { - c = mSurfaceView.getHolder().lockCanvas(); - c.drawColor(0, PorterDuff.Mode.CLEAR); - } finally { - if (c != null) { - mSurfaceView.getHolder().unlockCanvasAndPost(c); - } else { - Log.w(TAG, "couldn't unlock surface canvas"); - } - } - } - - // no more accessible class MjpegViewThread extends Thread { private final SurfaceHolder mSurfaceHolder; private int frameCounter = 0; private Bitmap ovl; - // no more accessible MjpegViewThread(SurfaceHolder surfaceHolder) { mSurfaceHolder = surfaceHolder; } private Rect destRect(int bmw, int bmh) { - - int tempx; - int tempy; - if (displayMode == MjpegViewDefault.SIZE_STANDARD) { - tempx = (dispWidth / 2) - (bmw / 2); - tempy = (dispHeight / 2) - (bmh / 2); - return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); - } - if (displayMode == MjpegViewDefault.SIZE_BEST_FIT) { - float bmasp = (float) bmw / (float) bmh; - bmw = dispWidth; - bmh = (int) (dispWidth / bmasp); - if (bmh > dispHeight) { - bmh = dispHeight; - bmw = (int) (dispHeight * bmasp); - } - tempx = (dispWidth / 2) - (bmw / 2); - tempy = (dispHeight / 2) - (bmh / 2); - return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); - } - if (displayMode == MjpegViewDefault.SIZE_SCALE_FIT) { - float bmasp = ((float) bmw / (float) bmh); - tempx = 0; - tempy = 0; - if (bmw < dispWidth) { - bmw = dispWidth; - // cross-multiplication using aspect ratio - bmh = (int) (dispWidth / bmasp); - // set it to the center height - tempy = (dispHeight - bmh) / 4; - } - return new Rect(tempx, tempy, bmw, bmh + tempy); + float bmasp = (float) bmw / (float) bmh; + bmw = dispWidth; + bmh = (int) (dispWidth / bmasp); + if (bmh > dispHeight) { + bmh = dispHeight; + bmw = (int) (dispHeight * bmasp); } - if (displayMode == MjpegViewDefault.SIZE_FULLSCREEN) - return new Rect(0, 0, dispWidth, dispHeight); - return null; + int tempx = (dispWidth / 2) - (bmw / 2); + int tempy = (dispHeight / 2) - (bmh / 2); + return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); } - // no more accessible void setSurfaceSize(int width, int height) { synchronized (mSurfaceHolder) { dispWidth = width; @@ -415,14 +226,12 @@ private Bitmap makeFpsOverlay(Paint p, String text) { p.getTextBounds(text, 0, text.length(), b); int bwidth = b.width() + 2; int bheight = b.height() + 2; - Bitmap bm = Bitmap.createBitmap(bwidth, bheight, - Bitmap.Config.ARGB_8888); + Bitmap bm = Bitmap.createBitmap(bwidth, bheight, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(bm); - p.setColor(overlayBackgroundColor); + p.setColor(Color.BLACK); c.drawRect(0, 0, bwidth, bheight, p); - p.setColor(overlayTextColor); - c.drawText(text, -b.left + 1, - (bheight / 2) - ((p.ascent() + p.descent()) / 2) + 1, p); + p.setColor(Color.WHITE); + c.drawText(text, -b.left + 1, (bheight / 2) - ((p.ascent() + p.descent()) / 2) + 1, p); return bm; } @@ -451,40 +260,30 @@ public void run() { byte[] header = mIn.readHeader(); byte[] imageData = mIn.readMjpegFrame(header); bm = BitmapFactory.decodeStream(new ByteArrayInputStream(imageData)); - if (flipHorizontal || flipVertical) + if (flipHorizontal || flipVertical) { bm = flip(bm); - if (rotateDegrees != 0) + } + if (rotateDegrees != 0) { bm = rotate(bm, rotateDegrees); + } - _frameCapturedWithByteData(imageData, header); - _frameCapturedWithBitmap(bm); - destRect = destRect(bm.getWidth(), - bm.getHeight()); + destRect = destRect(bm.getWidth(), bm.getHeight()); - if (transparentBackground) { - c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - } else { - c.drawColor(backgroundColor); - } + c.drawColor(Color.BLACK); c.drawBitmap(bm, null, destRect, p); if (showFps) { p.setXfermode(mode); if (ovl != null) { - height = ((ovlPos & 1) == 1) ? destRect.top - : destRect.bottom - - ovl.getHeight(); - width = ((ovlPos & 8) == 8) ? destRect.left - : destRect.right - - ovl.getWidth(); + height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom - ovl.getHeight(); + width = ((ovlPos & 8) == 8) ? destRect.left : destRect.right - ovl.getWidth(); c.drawBitmap(ovl, width, height, null); } p.setXfermode(null); frameCounter++; if ((System.currentTimeMillis() - start) >= 1000) { - fps = frameCounter - + "fps"; + fps = frameCounter + "fps"; frameCounter = 0; start = System.currentTimeMillis(); ovl = makeFpsOverlay(overlayPaint, fps); diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java deleted file mode 100644 index fe66f6d6a..000000000 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/MjpegViewNative.java +++ /dev/null @@ -1,462 +0,0 @@ -package com.github.niqdev.mjpeg; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffXfermode; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.util.Log; -import android.view.SurfaceHolder; -import android.view.SurfaceView; - -import java.io.IOException; - -import androidx.annotation.NonNull; - -/* - * I don't really understand and want to know what the hell it does! - * Maybe one day I will refactor it ;-) - *

    - * https://bitbucket.org/neuralassembly/simplemjpegview - */ -public class MjpegViewNative extends AbstractMjpegView { - private static final String TAG = MjpegViewDefault.class.getSimpleName(); - - private final SurfaceHolder.Callback mSurfaceHolderCallback; - private final SurfaceView mSurfaceView; - private final boolean transparentBackground; - - private SurfaceHolder holder; - - private MjpegViewThread thread; - private MjpegInputStreamNative mIn = null; - private boolean showFps = false; - private boolean mRun = false; - private boolean surfaceDone = false; - - private Paint overlayPaint; - private int overlayTextColor; - private int overlayBackgroundColor; - private int backgroundColor; - private int ovlPos; - private int dispWidth; - private int dispHeight; - private int displayMode; - - private boolean suspending = false; - - private Bitmap bmp = null; - - private int IMG_WIDTH = 640; - private int IMG_HEIGHT = 480; - - MjpegViewNative(SurfaceView surfaceView, SurfaceHolder.Callback callback, boolean transparentBackground) { - this.mSurfaceView = surfaceView; - this.mSurfaceHolderCallback = callback; - this.transparentBackground = transparentBackground; - init(); - } - - private void init() { - - //SurfaceHolder holder = getHolder(); - holder = mSurfaceView.getHolder(); - holder.addCallback(mSurfaceHolderCallback); - thread = new MjpegViewThread(holder); - mSurfaceView.setFocusable(true); - overlayPaint = new Paint(); - overlayPaint.setTextAlign(Paint.Align.LEFT); - overlayPaint.setTextSize(12); - overlayPaint.setTypeface(Typeface.DEFAULT); - overlayTextColor = Color.WHITE; - overlayBackgroundColor = Color.BLACK; - backgroundColor = Color.BLACK; - ovlPos = MjpegViewNative.POSITION_LOWER_RIGHT; - displayMode = MjpegViewNative.SIZE_STANDARD; - dispWidth = mSurfaceView.getWidth(); - dispHeight = mSurfaceView.getHeight(); - } - - /* all methods/constructors below are no more accessible */ - - void _startPlayback() { - if (mIn != null) { - mRun = true; - if (thread == null) { - thread = new MjpegViewThread(holder); - } - thread.start(); - } - } - - void _resumePlayback() { - if (suspending) { - if (mIn != null) { - mRun = true; - SurfaceHolder holder = mSurfaceView.getHolder(); - holder.addCallback(mSurfaceHolderCallback); - thread = new MjpegViewThread(holder); - thread.start(); - suspending = false; - } - } - } - - void _stopPlayback() { - if (mRun) { - suspending = true; - } - mRun = false; - if (thread != null) { - boolean retry = true; - while (retry) { - try { - thread.join(); - retry = false; - } catch (InterruptedException ignored) { - } - } - thread = null; - } - if (mIn != null) { - try { - mIn.close(); - } catch (IOException ignored) { - } - mIn = null; - } - - } - - void _freeCameraMemory() { - if (mIn != null) { - mIn.freeCameraMemory(); - } - } - - void _surfaceChanged(int w, int h) { - if (thread != null) { - thread.setSurfaceSize(w, h); - } - } - - void _surfaceDestroyed() { - surfaceDone = false; - _stopPlayback(); - if (thread != null) { - thread = null; - } - } - - void _surfaceCreated() { - surfaceDone = true; - } - - void _showFps(boolean b) { - showFps = b; - } - - void _setSource(MjpegInputStreamNative source) { - mIn = source; - if (!suspending) { - _startPlayback(); - } else { - _resumePlayback(); - } - } - - void _setOverlayPaint(Paint p) { - overlayPaint = p; - } - - void _setOverlayTextColor(int c) { - overlayTextColor = c; - } - - void _setOverlayBackgroundColor(int c) { - overlayBackgroundColor = c; - } - - void _setOverlayPosition(int p) { - ovlPos = p; - } - - void _setDisplayMode(int s) { - displayMode = s; - } - - void _setResolution(int w, int h) { - IMG_WIDTH = w; - IMG_HEIGHT = h; - } - - boolean _isStreaming() { - return mRun; - } - - @Override - public void onSurfaceCreated(@NonNull SurfaceHolder holder) { - _surfaceCreated(); - } - - /* override methods */ - - @Override - public void onSurfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) { - _surfaceChanged(width, height); - } - - @Override - public void onSurfaceDestroyed(@NonNull SurfaceHolder holder) { - _surfaceDestroyed(); - } - - @Override - public void setSource(@NonNull MjpegInputStream stream) { - if (!(stream instanceof MjpegInputStreamNative)) { - throw new IllegalArgumentException("stream must be an instance of MjpegInputStreamNative"); - } - _setSource((MjpegInputStreamNative) stream); - } - - @Override - public void setDisplayMode(DisplayMode mode) { - _setDisplayMode(mode.getValue()); - } - - @Override - public void showFps(boolean show) { - _showFps(show); - } - - @Override - public void flipSource(boolean flip) { - flipHorizontal(flip); - } - - @Override - public void flipHorizontal(boolean flip) { - - } - - @Override - public void flipVertical(boolean flip) { - - } - - @Override - public void setRotate(float degrees) { - - } - - @Override - public void stopPlayback() { - _stopPlayback(); - } - - @Override - public boolean isStreaming() { - return _isStreaming(); - } - - @Override - public void setResolution(int width, int height) { - _setResolution(width, height); - } - - @Override - public void freeCameraMemory() { - _freeCameraMemory(); - } - - @Override - public void setOnFrameCapturedListener(@NonNull OnFrameCapturedListener onFrameCapturedListener) { - throw new UnsupportedOperationException("Not implemented yet!"); - } - - @Override - public void setCustomBackgroundColor(int backgroundColor) { - this.backgroundColor = backgroundColor; - } - - @Override - public void setFpsOverlayBackgroundColor(int overlayBackgroundColor) { - this.overlayBackgroundColor = overlayBackgroundColor; - } - - @Override - public void setFpsOverlayTextColor(int overlayTextColor) { - this.overlayTextColor = overlayTextColor; - } - - @NonNull - @Override - public SurfaceView getSurfaceView() { - return mSurfaceView; - } - - @Override - public void resetTransparentBackground() { - mSurfaceView.setZOrderOnTop(false); - mSurfaceView.getHolder().setFormat(PixelFormat.OPAQUE); - } - - @Override - public void setTransparentBackground() { - mSurfaceView.setZOrderOnTop(true); - mSurfaceView.getHolder().setFormat(PixelFormat.TRANSPARENT); - } - - @Override - public void clearStream() { - Canvas c = null; - - try { - c = mSurfaceView.getHolder().lockCanvas(); - c.drawColor(0, PorterDuff.Mode.CLEAR); - } finally { - if (c != null) { - mSurfaceView.getHolder().unlockCanvasAndPost(c); - } else { - Log.w(TAG, "couldn't unlock surface canvas"); - } - } - } - - // no more accessible - class MjpegViewThread extends Thread { - private final SurfaceHolder mSurfaceHolder; - private int frameCounter = 0; - private String fps = ""; - - // no more accessible - MjpegViewThread(SurfaceHolder surfaceHolder) { - mSurfaceHolder = surfaceHolder; - } - - private Rect destRect(int bmw, int bmh) { - int tempx; - int tempy; - if (displayMode == MjpegViewNative.SIZE_STANDARD) { - tempx = (dispWidth / 2) - (bmw / 2); - tempy = (dispHeight / 2) - (bmh / 2); - return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); - } - if (displayMode == MjpegViewNative.SIZE_BEST_FIT) { - float bmasp = (float) bmw / (float) bmh; - bmw = dispWidth; - bmh = (int) (dispWidth / bmasp); - if (bmh > dispHeight) { - bmh = dispHeight; - bmw = (int) (dispHeight * bmasp); - } - tempx = (dispWidth / 2) - (bmw / 2); - tempy = (dispHeight / 2) - (bmh / 2); - return new Rect(tempx, tempy, bmw + tempx, bmh + tempy); - } - if (displayMode == MjpegViewNative.SIZE_FULLSCREEN) - return new Rect(0, 0, dispWidth, dispHeight); - return null; - } - - // no more accessible - void setSurfaceSize(int width, int height) { - synchronized (mSurfaceHolder) { - dispWidth = width; - dispHeight = height; - } - } - - private Bitmap makeFpsOverlay(Paint p) { - Rect b = new Rect(); - p.getTextBounds(fps, 0, fps.length(), b); - - // false indentation to fix forum layout - Bitmap bm = Bitmap.createBitmap(b.width(), b.height(), Bitmap.Config.ARGB_8888); - - Canvas c = new Canvas(bm); - p.setColor(overlayBackgroundColor); - c.drawRect(0, 0, b.width(), b.height(), p); - p.setColor(overlayTextColor); - c.drawText(fps, -b.left, b.bottom - b.top - p.descent(), p); - return bm; - } - - public void run() { - long start = System.currentTimeMillis(); - PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.DST_OVER); - - int width; - int height; - Paint p = new Paint(); - Bitmap ovl = null; - - while (mRun) { - - Rect destRect; - Canvas c = null; - - if (surfaceDone) { - try { - if (bmp == null) { - bmp = Bitmap.createBitmap(IMG_WIDTH, IMG_HEIGHT, Bitmap.Config.ARGB_8888); - } - int ret = mIn.readMjpegFrame(bmp); - - if (ret == -1) { - // TODO error - //((MjpegActivity) saved_context).setImageError(); - return; - } - - destRect = destRect(bmp.getWidth(), bmp.getHeight()); - - c = mSurfaceHolder.lockCanvas(); - synchronized (mSurfaceHolder) { - if (transparentBackground) { - c.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); - } else { - c.drawColor(backgroundColor); - } - - c.drawBitmap(bmp, null, destRect, p); - - if (showFps) { - p.setXfermode(mode); - if (ovl != null) { - - // false indentation to fix forum layout - height = ((ovlPos & 1) == 1) ? destRect.top : destRect.bottom - ovl.getHeight(); - width = ((ovlPos & 8) == 8) ? destRect.left : destRect.right - ovl.getWidth(); - - c.drawBitmap(ovl, width, height, null); - } - p.setXfermode(null); - frameCounter++; - if ((System.currentTimeMillis() - start) >= 1000) { - fps = frameCounter + "fps"; - frameCounter = 0; - start = System.currentTimeMillis(); - if (ovl != null) ovl.recycle(); - - ovl = makeFpsOverlay(overlayPaint); - } - } - - - } - - } catch (IOException ignored) { - - } finally { - if (c != null) mSurfaceHolder.unlockCanvasAndPost(c); - } - } - } - } - } -} diff --git a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt b/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt deleted file mode 100644 index dec78d044..000000000 --- a/ipcam-view/src/main/java/com/github/niqdev/mjpeg/OnFrameCapturedListener.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.niqdev.mjpeg - -import android.graphics.Bitmap - -interface OnFrameCapturedListener { - fun onFrameCaptured(bitmap: Bitmap) - - //return the byte data for recording - fun onFrameCapturedWithHeader(bitmap: ByteArray, header: ByteArray) -} diff --git a/ipcam-view/src/main/res/values/attrs.xml b/ipcam-view/src/main/res/values/attrs.xml index 32d131998..6042ebd0a 100644 --- a/ipcam-view/src/main/res/values/attrs.xml +++ b/ipcam-view/src/main/res/values/attrs.xml @@ -1,13 +1,4 @@ - - - - - - - - - - + From 26ac439ca04ea2ddc79e677a3bbf3e5b9c7812fe Mon Sep 17 00:00:00 2001 From: Niko Strijbol Date: Sun, 8 Mar 2026 19:35:11 +0100 Subject: [PATCH 6/6] Fix activities in Android 36 --- app/src/main/res/layout/activity_extra_food.xml | 1 + app/src/main/res/layout/activity_info_sub_item.xml | 1 + app/src/main/res/layout/activity_preferences.xml | 1 + app/src/main/res/layout/activity_preferences_homefeed.xml | 1 + app/src/main/res/layout/activity_resto_history.xml | 1 + app/src/main/res/layout/activity_resto_salad.xml | 1 + app/src/main/res/layout/activity_webview.xml | 1 + app/src/main/res/layout/activity_wpi_api_key_management.xml | 1 + app/src/main/res/layout/activity_wpi_cammie.xml | 4 ++-- app/src/main/res/layout/activity_wpi_full_screen_cammie.xml | 1 - app/src/main/res/layout/activity_wpi_tab_transaction_form.xml | 1 + app/src/main/res/layout/activity_wpi_tap_cart.xml | 1 + 12 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/activity_extra_food.xml b/app/src/main/res/layout/activity_extra_food.xml index 77d86a20a..8334621ed 100644 --- a/app/src/main/res/layout/activity_extra_food.xml +++ b/app/src/main/res/layout/activity_extra_food.xml @@ -27,6 +27,7 @@ android:id="@+id/coordinator_layout" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".resto.extrafood.ExtraFoodActivity"> diff --git a/app/src/main/res/layout/activity_resto_salad.xml b/app/src/main/res/layout/activity_resto_salad.xml index abc9cfabd..c4496c06b 100644 --- a/app/src/main/res/layout/activity_resto_salad.xml +++ b/app/src/main/res/layout/activity_resto_salad.xml @@ -27,6 +27,7 @@ android:id="@+id/coordinator_layout" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context=".resto.salad.SaladActivity"> diff --git a/app/src/main/res/layout/activity_wpi_cammie.xml b/app/src/main/res/layout/activity_wpi_cammie.xml index 07c05dcea..ff48f27c6 100644 --- a/app/src/main/res/layout/activity_wpi_cammie.xml +++ b/app/src/main/res/layout/activity_wpi_cammie.xml @@ -22,12 +22,12 @@ + tools:context=".wpi.cammie.CammieActivity" + android:fitsSystemWindows="true"> diff --git a/app/src/main/res/layout/activity_wpi_tap_cart.xml b/app/src/main/res/layout/activity_wpi_tap_cart.xml index 3dd607889..420eee139 100644 --- a/app/src/main/res/layout/activity_wpi_tap_cart.xml +++ b/app/src/main/res/layout/activity_wpi_tap_cart.xml @@ -27,6 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".wpi.tab.create.FormActivity" + android:fitsSystemWindows="true" tools:menu="@menu/menu_resto">