Skip to content

Commit 5b6aeb7

Browse files
committed
fix: restore camera and Quad mode on resume, no thread leak or long pause
- loadSettings() reads back "active" and "quad_enabled", which were saved but never loaded; single view hides the Quad layout again. A shared link opens in single view (fixes #22). - startListener() shuts down the previous executor, so switching camera or saving a URL no longer leaks two idle threads (fixes #23). - onPause() disables the decoder and waits at most 300 ms for the audio thread instead of 2 s per executor, and Quad cells no longer block (fixes #32). - The frame that triggers decoder creation is passed to the new decoder. Tests: back-off delays and reset, the enable/disable gate, slot clamping.
1 parent 90002e5 commit 5b6aeb7

2 files changed

Lines changed: 115 additions & 18 deletions

File tree

‎android/app/src/main/java/com/openipc/decoder/Decoder.java‎

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -326,21 +326,27 @@ private void loadSettings() {
326326
edit.apply();
327327
}
328328

329-
mActive = 0;
330-
quadEnabled = false;
329+
// Restore the last camera and mode (#22); both are saved but were never read back
330+
mActive = clampSlot(pref.getInt("active", 0));
331+
quadEnabled = pref.getBoolean("quad_enabled", false);
331332
for (int i = 0; i < CAM_COUNT; i++) {
332333
mHosts[i] = pref.getString("host_" + i, DEFAULT_URL);
333334
}
334-
applyActiveCamera();
335335

336336
Intent intent = getIntent();
337337
String link = intent.getStringExtra(Intent.EXTRA_TEXT);
338338
if (link != null) {
339339
Log.d(TAG, "Link: " + Uri.parse(link).getHost());
340340
mHosts[mActive] = sanitizeUrl(link);
341-
mHost = mHosts[mActive];
341+
// A shared link is meant to be watched: show it in single view
342+
quadEnabled = false;
342343
intent.removeExtra(Intent.EXTRA_TEXT);
343344
}
345+
applyActiveCamera();
346+
}
347+
348+
static int clampSlot(int slot) {
349+
return slot >= 0 && slot < CAM_COUNT ? slot : 0;
344350
}
345351

346352
static String sanitizeUrl(String url) {
@@ -464,10 +470,7 @@ private void startQuad() {
464470
closeDecoder();
465471
closeAudio();
466472
nalQueue.clear();
467-
if (executor != null) {
468-
executor.shutdownNow();
469-
executor = null;
470-
}
473+
stopExecutor();
471474

472475
mSurface.setVisibility(View.GONE);
473476
quadContainer.setVisibility(View.VISIBLE);
@@ -676,6 +679,9 @@ private void startListener() {
676679
listenerGen++;
677680
final int gen = listenerGen;
678681

682+
// The previous pool's threads exit on the generation check; shut it down so its
683+
// idle threads do not leak on every camera switch (#23)
684+
stopExecutor();
679685
executor = Executors.newFixedThreadPool(2);
680686

681687
// Video decode thread
@@ -685,9 +691,12 @@ private void startListener() {
685691
try {
686692
Frame buffer = nalQueue.poll(5, TimeUnit.MILLISECONDS);
687693
if (buffer != null) {
688-
boolean needCreate = videoDecoder.decodeFrame(buffer, codecH265, 0);
694+
if (videoDecoder.decodeFrame(buffer, codecH265, 0)) {
695+
createDecoder();
696+
// The frame that triggered creation (often the SPS) goes in too
697+
videoDecoder.decodeFrame(buffer, codecH265, 0);
698+
}
689699
framePool.recycle(buffer);
690-
if (needCreate) createDecoder();
691700
}
692701
} catch (InterruptedException e) {
693702
Thread.currentThread().interrupt();
@@ -725,23 +734,30 @@ protected void onPause() {
725734
if (rtspClient != null) {
726735
rtspClient.stop();
727736
}
728-
if (executor != null) {
729-
executor.shutdownNow();
737+
// Disabled first: the decode thread can no longer create a decoder after this
738+
videoDecoder.setEnabled(false);
739+
// Short, bounded wait so the audio thread is out of AudioTrack before it is released
740+
// (#32: this used to block the UI thread for seconds)
741+
ExecutorService ex = executor;
742+
stopExecutor();
743+
if (ex != null) {
730744
try {
731-
if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
732-
executor.shutdownNow();
733-
}
745+
ex.awaitTermination(300, TimeUnit.MILLISECONDS);
734746
} catch (InterruptedException e) {
735-
executor.shutdownNow();
736747
Thread.currentThread().interrupt();
737748
}
738-
executor = null;
739749
}
740-
closeDecoder();
741750
closeAudio();
742751
nalQueue.clear();
743752
}
744753

754+
private void stopExecutor() {
755+
if (executor != null) {
756+
executor.shutdownNow();
757+
executor = null;
758+
}
759+
}
760+
745761
@Override
746762
protected void onDestroy() {
747763
super.onDestroy();
@@ -753,6 +769,7 @@ protected void onResume() {
753769
super.onResume();
754770

755771
loadSettings();
772+
videoDecoder.setEnabled(true);
756773
if (quadEnabled) {
757774
mSurface.setVisibility(View.GONE);
758775
quadContainer.setVisibility(View.VISIBLE);
@@ -773,6 +790,9 @@ protected void onResume() {
773790
getSharedPreferences("settings", MODE_PRIVATE).edit()
774791
.putBoolean("quad_enabled", true).apply();
775792
} else {
793+
// The views may still be in Quad layout from before the pause
794+
quadContainer.setVisibility(View.GONE);
795+
mSurface.setVisibility(View.VISIBLE);
776796
boolean configured = mHost != null && !mHost.isEmpty() && !mHost.equals(DEFAULT_URL);
777797
if (configured) {
778798
rtspClient.configure(mHost, mUserAgent);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0
3+
*
4+
* DecoderRetryTest.java — decoder re-creation back-off and enable/disable gate
5+
*
6+
*/
7+
8+
package com.openipc.decoder;
9+
10+
import static org.junit.Assert.assertEquals;
11+
import static org.junit.Assert.assertFalse;
12+
import static org.junit.Assert.assertTrue;
13+
14+
import org.junit.Test;
15+
16+
public class DecoderRetryTest {
17+
@Test
18+
public void delaysGrowAndAreCapped() {
19+
assertEquals(0, DecoderRetry.delayMs(0));
20+
assertEquals(500, DecoderRetry.delayMs(1));
21+
assertEquals(1000, DecoderRetry.delayMs(2));
22+
assertEquals(2000, DecoderRetry.delayMs(3));
23+
assertEquals(8000, DecoderRetry.delayMs(5));
24+
assertEquals(10_000, DecoderRetry.delayMs(6));
25+
assertEquals(10_000, DecoderRetry.delayMs(100));
26+
}
27+
28+
@Test
29+
public void failureBlocksUntilTheDelayPasses() {
30+
// #21: a failure is not permanent
31+
DecoderRetry r = new DecoderRetry();
32+
assertTrue(r.mayCreate(1000));
33+
r.onFailure(1000);
34+
assertFalse(r.mayCreate(1499));
35+
assertTrue(r.mayCreate(1500));
36+
r.onFailure(1500);
37+
assertFalse(r.mayCreate(2499));
38+
assertTrue(r.mayCreate(2500));
39+
assertEquals(2, r.failures());
40+
}
41+
42+
@Test
43+
public void successResetsTheBackOff() {
44+
DecoderRetry r = new DecoderRetry();
45+
r.onFailure(0);
46+
r.onFailure(0);
47+
r.onSuccess();
48+
assertEquals(0, r.failures());
49+
assertTrue(r.mayCreate(0));
50+
}
51+
52+
private static Frame sps() {
53+
byte[] d = {0, 0, 0, 1, 0x67, 0x42};
54+
return new Frame(d, d.length);
55+
}
56+
57+
@Test
58+
public void disabledManagerNeverAsksForADecoder() {
59+
// #32: after a pause no decoder may be created by a late decode thread
60+
MediaCodecManager m = new MediaCodecManager(null);
61+
assertTrue("no decoder yet: create one", m.decodeFrame(sps(), false, 0));
62+
m.setEnabled(false);
63+
assertFalse(m.decodeFrame(sps(), false, 0));
64+
m.createDecoder(false); // must be a no-op while disabled
65+
assertFalse(m.decodeFrame(sps(), false, 0));
66+
m.setEnabled(true);
67+
assertTrue(m.decodeFrame(sps(), false, 0));
68+
}
69+
70+
@Test
71+
public void savedSlotIsClamped() {
72+
// #22: a stored slot outside 0..3 falls back to the first camera
73+
assertEquals(2, Decoder.clampSlot(2));
74+
assertEquals(0, Decoder.clampSlot(-1));
75+
assertEquals(0, Decoder.clampSlot(4));
76+
}
77+
}

0 commit comments

Comments
 (0)