Skip to content

Commit a5ecfe7

Browse files
committed
fix: Qodo on #135 — oversized frames and silent decoders fall back to software, size walks the markers, no restart after stop, surface swaps not fatal, stack trace logged
1 parent 6e2e0f4 commit a5ecfe7

4 files changed

Lines changed: 59 additions & 15 deletions

File tree

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,22 +1607,24 @@ private int dp(float dp) {
16071607
* with BitmapFactory (into the previous bitmap when the size is the same) and drawn over
16081608
* the whole surface; the view's fit transform keeps the aspect ratio, as for video.
16091609
*/
1610-
private void drawJpeg(Frame frame) {
1610+
private void drawJpeg(Frame frame, int gen) {
16111611
Surface s = mVideoSurface;
1612-
if (s == null || !s.isValid()) return;
1612+
if (s == null || !s.isValid() || gen != listenerGen) return;
16131613
// The hardware JPEG decoder first, where there is one (#132)
16141614
if (!hwJpegFailed && !jpegDrawn && JpegCodec.candidate() != null) {
16151615
int[] size = JpegAssembler.size(frame.data(), frame.length());
16161616
if (size != null) {
16171617
try {
1618-
if (jpegCodec.decode(frame, s, size[0], size[1])) {
1618+
if (jpegCodec.decode(frame, s, size[0], size[1],
1619+
() -> gen == listenerGen && s == mVideoSurface)) {
16191620
jpegShown(size[0], size[1]);
16201621
}
16211622
return;
16221623
} catch (Exception e) {
1623-
Log.w(TAG, "Hardware MJPEG decoder failed, decoding in software: "
1624-
+ e.getMessage());
16251624
jpegCodec.release();
1625+
// The surface was replaced meanwhile: not the decoder's fault, try again
1626+
if (s != mVideoSurface || gen != listenerGen) return;
1627+
Log.w(TAG, "Hardware MJPEG decoder failed, decoding in software", e);
16261628
hwJpegFailed = true;
16271629
}
16281630
}
@@ -1703,7 +1705,7 @@ private void startListener() {
17031705
try {
17041706
Frame buffer = nalQueue.poll(5, TimeUnit.MILLISECONDS);
17051707
if (buffer != null && rtspClient.isJpeg()) {
1706-
drawJpeg(buffer);
1708+
drawJpeg(buffer, gen);
17071709
framePool.recycle(buffer);
17081710
} else if (buffer != null && surfacePending) {
17091711
// Waiting for a fresh surface after MJPEG; the next key frame follows
@@ -1781,7 +1783,6 @@ public void run() {
17811783

17821784
private void stopStreams() {
17831785
lastJpegFrameMs = 0;
1784-
jpegCodec.release();
17851786
mSurface.removeCallbacks(singleUdpCheck);
17861787
// The single view's stream ends here: it is no longer "connected" for the debug page
17871788
singleState = "disconnected";
@@ -1803,6 +1804,9 @@ private void stopStreams() {
18031804
// for on the UI thread (#32: this used to block it for seconds)
18041805
audioManager.setEnabled(false);
18051806
stopExecutor();
1807+
// After the worker's generation is gone: a frame still in flight sees that and can no
1808+
// longer create the JPEG decoder again (Qodo on #135)
1809+
jpegCodec.release();
18061810
nalQueue.clear();
18071811
}
18081812

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,21 @@ private void drop(String reason) {
222222

223223
/** {width, height} from the SOF0 header of a JPEG, or null (#132). */
224224
static int[] size(byte[] d, int length) {
225-
for (int i = 2; i + 8 < length; i++) {
226-
if ((d[i] & 0xFF) == 0xFF && (d[i + 1] & 0xFF) == 0xC0) {
225+
// Walk the marker segments by their lengths: table bytes may contain FF C0 (Qodo on #135)
226+
int i = 2;
227+
while (i + 4 <= length) {
228+
if ((d[i] & 0xFF) != 0xFF) return null;
229+
int marker = d[i + 1] & 0xFF;
230+
if (marker == 0xDA || marker == 0xD9) return null; // scan or end before any SOF
231+
int len = ((d[i + 2] & 0xFF) << 8) | (d[i + 3] & 0xFF);
232+
if (len < 2) return null;
233+
if (marker == 0xC0) {
234+
if (i + 9 > length) return null;
227235
int h = ((d[i + 5] & 0xFF) << 8) | (d[i + 6] & 0xFF);
228236
int w = ((d[i + 7] & 0xFF) << 8) | (d[i + 8] & 0xFF);
229237
return new int[] {w, h};
230238
}
239+
i += 2 + len;
231240
}
232241
return null;
233242
}

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,18 @@ static String[] candidate() {
9898
return byNameTried ? null : BY_NAME[0];
9999
}
100100

101-
synchronized boolean decode(Frame frame, Surface target, int w, int h) throws Exception {
101+
/** Frames queued in a row without any output: the decoder is not working (Qodo on #135). */
102+
static final int MAX_SILENT_FRAMES = 15;
103+
private int silentFrames;
104+
105+
/** Checked under the lock: false once the stream that called decode() has stopped. */
106+
interface Current {
107+
boolean holds();
108+
}
109+
110+
synchronized boolean decode(Frame frame, Surface target, int w, int h, Current current)
111+
throws Exception {
112+
if (!current.holds()) return false;
102113
String[] dec = chosen != null ? chosen : find();
103114
if (dec == null) {
104115
if (byNameTried) throw new IllegalStateException("no JPEG decoder");
@@ -120,6 +131,7 @@ synchronized boolean decode(Frame frame, Surface target, int w, int h) throws Ex
120131
}
121132
codec = c;
122133
chosen = dec;
134+
silentFrames = 0;
123135
surface = target;
124136
width = w;
125137
height = h;
@@ -128,13 +140,15 @@ synchronized boolean decode(Frame frame, Surface target, int w, int h) throws Ex
128140
int in = codec.dequeueInputBuffer(40_000);
129141
if (in >= 0) {
130142
ByteBuffer b = codec.getInputBuffer(in);
131-
if (b != null && b.capacity() >= frame.length()) {
132-
b.clear();
133-
b.put(frame.data(), 0, frame.length());
134-
codec.queueInputBuffer(in, 0, frame.length(), System.nanoTime() / 1000, 0);
135-
} else {
143+
if (b == null || b.capacity() < frame.length()) {
144+
// A frame larger than the decoder's input buffers: let the software path have it
136145
codec.queueInputBuffer(in, 0, 0, 0, 0);
146+
throw new IllegalStateException("JPEG frame of " + frame.length()
147+
+ " bytes does not fit the input buffer");
137148
}
149+
b.clear();
150+
b.put(frame.data(), 0, frame.length());
151+
codec.queueInputBuffer(in, 0, frame.length(), System.nanoTime() / 1000, 0);
138152
}
139153
boolean rendered = false;
140154
int out;
@@ -146,6 +160,10 @@ synchronized boolean decode(Frame frame, Surface target, int w, int h) throws Ex
146160
}
147161
in = -1; // after the first output, only take what is ready
148162
}
163+
silentFrames = rendered ? 0 : silentFrames + 1;
164+
if (silentFrames > MAX_SILENT_FRAMES) {
165+
throw new IllegalStateException(MAX_SILENT_FRAMES + " frames without output");
166+
}
149167
return rendered;
150168
}
151169

‎android/app/src/test/java/com/openipc/decoder/JpegAssemblerTest.java‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,17 @@ public void sizeFromTheFrameHeader() throws Exception {
179179
assertEquals(64, size[0]);
180180
assertEquals(32, size[1]);
181181
}
182+
183+
@Test
184+
public void sizeSkipsTableBytesThatLookLikeSof() {
185+
// A DQT whose table contains FF C0 before the real SOF0 (64x32)
186+
byte[] tables = new byte[128];
187+
tables[10] = (byte) 0xFF;
188+
tables[11] = (byte) 0xC0;
189+
byte[] h = JpegAssembler.headers(0, 64, 32, tables, 0);
190+
int[] size = JpegAssembler.size(h, h.length);
191+
assertNotNull(size);
192+
assertEquals(64, size[0]);
193+
assertEquals(32, size[1]);
194+
}
182195
}

0 commit comments

Comments
 (0)