Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<!-- TVs and set-top boxes have no touchscreen; without this Google Play hides the app there -->
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />

<!-- For Android 10+ -->
<application
Expand All @@ -16,7 +18,8 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true">
<activity android:name="Crash" android:exported="false" />
<!-- Separate process: it survives the crashed main process being killed -->
<activity android:name="Crash" android:exported="false" android:process=":crash" />
<activity
android:name="Decoder"
android:configChanges="orientation|screenSize|keyboardHidden"
Expand Down
17 changes: 12 additions & 5 deletions android/app/src/main/java/com/openipc/decoder/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import android.app.Application;
import android.content.Intent;
import android.os.Process;
import android.util.Log;

public class App extends Application {
Expand All @@ -20,11 +21,17 @@ public void onCreate() {
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
Log.e("OpenIPCDecoder", "Uncaught exception", throwable);

Intent intent = new Intent(getApplicationContext(), Crash.class);
intent.putExtra("error", Log.getStackTraceString(throwable));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
// Process will terminate naturally with the crash activity visible
try {
Intent intent = new Intent(getApplicationContext(), Crash.class);
intent.putExtra("error", Log.getStackTraceString(throwable));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
} finally {
// The app state is undefined after an uncaught exception (and the main looper
// may be dead), so end this process; Crash runs in its own ":crash" process.
Process.killProcess(Process.myPid());
System.exit(10);
}
});
}
}
62 changes: 34 additions & 28 deletions android/app/src/main/java/com/openipc/decoder/Decoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -391,9 +391,40 @@ private void saveSettings() {

// ====================== Status overlay ======================

// Delayed overlay updates after "connected"; cancelled on every status change
private final Runnable statusDetails = this::showConnectedDetails;
private final Runnable statusHide = () -> {
if (statusText != null) statusText.setVisibility(View.GONE);
};

private void showConnectedDetails() {
if (statusText == null || statusText.getVisibility() != View.VISIBLE) return;
String videoCodec = codecH265 ? "H.265" : "H.264";
String aCodec;
switch (audioCodec) {
case AudioConfig.AAC: aCodec = " • AAC"; break;
case AudioConfig.PCMU:
case AudioConfig.PCMA: aCodec = " • G.711"; break;
case AudioConfig.PCM: aCodec = " • PCM"; break;
default: aCodec = ""; break;
}
long elapsedNs = System.nanoTime() - statusBitrateNs;
String bps = "";
long bytes = rtspClient != null ? rtspClient.getStreamBytes() : 0;
if (elapsedNs > 500_000_000 && bytes > 0) {
double mbps = (bytes * 8.0 / 1_000_000)
/ (elapsedNs / 1_000_000_000.0);
bps = String.format(Locale.US, " • %.1f Mbps", mbps);
}
statusText.setText("Connected • " + videoCodec + aCodec + bps);
}

private void updateStatus(String status) {
runOnUiThread(() -> {
if (statusText == null) return;
// A stale timer from an earlier "connected" must not overwrite or hide this status
statusText.removeCallbacks(statusDetails);
statusText.removeCallbacks(statusHide);

switch (status) {
case "connecting":
Expand All @@ -405,34 +436,9 @@ private void updateStatus(String status) {
statusText.setVisibility(View.VISIBLE);
statusStreamBytes = rtspClient != null ? rtspClient.getStreamBytes() : 0;
statusBitrateNs = rtspClient != null ? rtspClient.getBitrateMarkNs() : System.nanoTime();
// After 1 second, refresh with codec + bitrate info
statusText.postDelayed(() -> {
if (statusText == null
|| statusText.getVisibility() != View.VISIBLE) return;
String videoCodec = codecH265 ? "H.265" : "H.264";
String aCodec;
switch (audioCodec) {
case AudioConfig.AAC: aCodec = " • AAC"; break;
case AudioConfig.PCMU:
case AudioConfig.PCMA: aCodec = " • G.711"; break;
case AudioConfig.PCM: aCodec = " • PCM"; break;
default: aCodec = ""; break;
}
long elapsedNs = System.nanoTime() - statusBitrateNs;
String bps = "";
long bytes = rtspClient != null ? rtspClient.getStreamBytes() : 0;
if (elapsedNs > 500_000_000 && bytes > 0) {
double mbps = (bytes * 8.0 / 1_000_000)
/ (elapsedNs / 1_000_000_000.0);
bps = String.format(Locale.US, " • %.1f Mbps", mbps);
}
statusText.setText("Connected • " + videoCodec + aCodec + bps);
}, 1000);
statusText.postDelayed(() -> {
if (statusText != null) {
statusText.setVisibility(View.GONE);
}
}, 5000);
// After 1 second, refresh with codec + bitrate info; hide after 5 seconds
statusText.postDelayed(statusDetails, 1000);
statusText.postDelayed(statusHide, 5000);
break;
case "disconnected":
statusText.setText(getString(R.string.status_disconnected));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ void startBrowser(String mHost) {
public void onReceivedHttpAuthRequest(
WebView v, HttpAuthHandler handler, String h, String realm) {
String userInfo = finalUri.getUserInfo();
if (userInfo != null) {
// Only the camera itself gets its credentials, never a host reached by a redirect or link
if (userInfo != null && link.equalsIgnoreCase(h)) {
String[] parts = userInfo.split(":", 2);
if (parts.length == 2) {
handler.proceed(parts[0], parts[1]);
Expand Down
Loading