Skip to content

feat: http:// MJPEG slots — multipart/x-mixed-replace over HTTP (#121) - #150

Merged
compot-dev merged 6 commits into
v2from
feat/http-mjpeg
Oct 13, 2026
Merged

compot-dev merged 6 commits into
v2from
feat/http-mjpeg

Conversation

@compot-dev

Copy link
Copy Markdown
Collaborator

Closes #121.

A slot accepts an HTTP MJPEG URL such as Majestic's http://user:pass@<camera>/mjpeg — multipart/x-mixed-replace, each part one whole JPEG.

  • HttpMjpeg (pure Java): URL, response head, chunked bodies (Majestic sends Transfer-Encoding: chunked, one part per chunk), parts by Content-Length or else up to the next boundary.
  • RtspSession.httpMjpeg(): GET with Basic credentials up front, Digest after a challenge (RtspAuth); the same back-off, watchdog and status as RTSP; transport shown as HTTP. The frames go to a new callback onJpegFrame after an Sdp with videoJpeg, so the existing MJPEG paths (MJPEG over RTSP (RTP/JPEG, RFC 2435) — discussion #119 single view, feat: MJPEG cells in multi-view (#132) #139 multi-view) show them — hardware JPEG decoder where there is one, else BitmapFactory.
  • No audio on this path.
  • Unit tests HttpMjpegTest: URL, Majestic's response head, 401 challenge, quoted boundary, chunked parts with Content-Length, parts without length (CR LF - inside the data), a chunk split inside a part.

Device test (AG-216, CI build 9523733), http://root:***@172.17.32.133/mjpeg in slot 2:

  • single view: transport HTTP, MJPEG 1280x720, live picture (clock running);
  • 3×3 grid: 1280×720 MJPEG reduced · 1.0 fps · software, transport HTTP; 0 FATAL.
  • Found on the way: the session's userAgent is the whole header line already — fixed in 9523733.

compot-dev and others added 3 commits October 13, 2026 17:08
…he single view and in multi-view (#121)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Grey Divider


Action required

1. JPEG data can be mistaken for a boundary ✓ Resolved
Description
readParts stops a lengthless part as soon as it encounters CRLF-- followed by the boundary,
without checking that a complete delimiter line follows. If JPEG bytes contain that prefix followed
by another character, the frame is truncated and subsequent parts can be skipped or misparsed.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R177-178]

+                byte[] end = ("\r\n" + delimiter).getBytes(StandardCharsets.US_ASCII);
+                if (!readUntil(body, end, part)) return;
Evidence
readUntil returns on the prefix alone, while the caller consumes the rest of the presumed
delimiter line; other delimiter scans also use prefix matching.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[175-185]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[192-200]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[145-150]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A boundary prefix inside a lengthless JPEG is treated as a complete multipart delimiter, corrupting frame parsing.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[141-212]
## Recommended Fix
Recognize a boundary only when its prefix is followed by a valid delimiter terminator. Preserve candidate bytes as frame data when that check fails, and apply the same complete-line check to preamble and fixed-length scans.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. HTTP slot URLs can inject request headers ✓ Resolved
Description
HttpMjpeg.Url.parse copies the URL path into u.path without rejecting carriage returns or line
feeds, and httpMjpeg inserts it directly into the GET request line. A configured URL containing a
raw line break can add headers or alter the request sent to the camera.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R525-526]

+                String request = "GET " + u.path + " HTTP/1.1\r\n"
+                        + "Host: " + host + "\r\n"
Evidence
The path is copied without validation and then concatenated ahead of the HTTP version and CRLF
before being written to the socket.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-533]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raw line breaks in an HTTP slot path are written into the GET request line and can inject headers.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-75]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-530]
## Recommended Fix
Validate the parsed request target before constructing the request, rejecting control characters and other characters that cannot occur raw in an HTTP request target.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Digest credentials can inject headers ✓ Resolved
Description
HttpMjpeg.Url.parse percent-decodes credentials without rejecting control characters, which
RtspAuth.quote leaves intact in a Digest authorization value. When a camera challenges with
Digest, a username containing encoded CRLF produces extra request headers; the existing RTSP URL
parser rejects those characters.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[52]

+                u.userInfo = RtspSession.Url.percentDecode(authority.substring(0, at));
Evidence
Basic encoding masks the characters on the first request, but a Digest challenge switches to a
quoted username; quoting escapes slashes and quotes, not CRLF.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[50-54]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[513-538]
android/app/src/main/java/com/openipc/decoder/RtspAuth.java[73-93]
android/app/src/main/java/com/openipc/decoder/RtspAuth.java[169-173]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[1128-1141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Percent-decoded HTTP slot credentials can carry line breaks into a Digest authorization header.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[50-54]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[1128-1141]
## Recommended Fix
Reject control characters in decoded HTTP user information, using the existing RTSP credential validation as the model.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Some cameras cannot start MJPEG playback 🐞 Bug ≡ Correctness ⭐ New
Description
Response.boundary() searches only for the literal boundary=, so it misses a boundary parameter
with whitespace before the equals sign. When a camera sends a content type such as
multipart/x-mixed-replace; boundary = frame, httpMjpeg() rejects the response as not an MJPEG
stream before reading any frames.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R115-116]

+            int i = lower.indexOf("boundary=");
+            if (i < 0) return null;
Evidence
The literal search returns null for boundary = frame; the session treats a null boundary as a
connection error rather than entering multipart parsing.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[110-129]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[540-544]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A valid multipart boundary parameter with whitespace around `=` is not recognized, so the HTTP MJPEG connection is rejected.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[110-129]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[540-544]

## Recommended Fix
Parse content-type parameters as names and values, allowing whitespace around the equals sign while preserving quoted boundary values. Add a response-head test for `boundary = frame`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. A camera response can exhaust app memory 🐞 Bug ☼ Reliability ⭐ New
Description
readResponse() retains every WWW-Authenticate value in Response.challenges without limiting
the number or total size of headers. A camera that continuously sends individually valid header
lines can grow that list until the app runs out of memory, even though each line has a size limit.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R153-155]

+            else if (name.equals("transfer-encoding")) {
+                r.chunked = value.toLowerCase(Locale.ROOT).contains("chunked");
+            } else if (name.equals("www-authenticate")) r.challenges.add(value);
Evidence
The expandable challenges list receives a value on every matching header, while the header loop has
no aggregate limit; MAX_LINE limits only one call to readLine().

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[103-108]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[147-156]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[277-290]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The HTTP response parser caps individual lines but can retain an unbounded number of authentication headers from a camera.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[134-157]

## Recommended Fix
Enforce a cumulative response-header byte or count limit and throw `IOException` when it is exceeded. Add a test with many short authentication headers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Some valid stream URLs cannot connect ✓ Resolved
Description
HttpMjpeg.Url.parse splits the authority from the request target only at / and does not remove
URL fragments. A query-only URL such as http://camera?stream=1 becomes an invalid hostname, while
a URL with #preview sends that fragment to the camera as part of the GET target.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R46-49]

+            int slash = rest.indexOf('/');
+            String authority = slash >= 0 ? rest.substring(0, slash) : rest;
+            Url u = new Url();
+            if (slash >= 0) u.path = rest.substring(slash);
Evidence
The parser searches only for a slash and stores the remaining string unchanged; the request uses
that stored target verbatim.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-530]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Query-only HTTP URLs are parsed as hostnames, and URL fragments are sent in GET targets.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
## Recommended Fix
End the authority at the first path, query, or fragment delimiter; prepend `/` for query-only targets and remove fragments before storing the request target. Add tests for both forms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
7. Quoted boundaries can stop video playback ✓ Resolved
Description
Response.boundary truncates its parameter at the first semicolon before determining whether that
semicolon is inside a quoted value. A response with boundary="a;b" therefore produces the wrong
delimiter, and readParts never finds the camera's frames.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R94-97]

+            int semi = b.indexOf(';');
+            if (semi >= 0) b = b.substring(0, semi).trim();
+            if (b.length() >= 2 && b.startsWith("\"") && b.endsWith("\"")) {
+                b = b.substring(1, b.length() - 1);
Evidence
The semicolon search runs before quote removal; readParts then builds its delimiter from the
resulting truncated boundary.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[91-101]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[141-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A semicolon inside a quoted boundary is mistaken for the end of the parameter, preventing frame parsing.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[86-102]
## Recommended Fix
Parse Content-Type parameters with awareness of quoted values and escapes before extracting the boundary. Cover quoted semicolons in a test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. LF-only streams drop lengthless frames ✓ Resolved
Description
readParts recognizes the end of a lengthless part only as CRLF followed by the delimiter,
although readLine accepts LF-only multipart lines. When a camera uses LF-only lines and omits
Content-Length, the parser reads through the boundary to EOF and returns without delivering the
frame.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R175-178]

+                // No length: the data runs up to CRLF + delimiter
+                ByteArrayOutputStream part = new ByteArrayOutputStream();
+                byte[] end = ("\r\n" + delimiter).getBytes(StandardCharsets.US_ASCII);
+                if (!readUntil(body, end, part)) return;
Evidence
The byte matcher requires an exact CRLF prefix and returns false at EOF, whereas header and boundary
line reading explicitly accepts LF alone.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[175-185]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[192-212]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[224-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
LF-only multipart lines are accepted by the line reader but not by lengthless frame delimiter detection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[174-185]
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[224-237]
## Recommended Fix
Make lengthless delimiter recognition consistent with the supported line endings while still validating that a candidate is a complete boundary line. Test an LF-only part without Content-Length.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Invalid parts keep a blank stream alive ✓ Resolved
Description
RtspClient.onJpegFrame and QuadCell.onJpegFrame accept any part of at least four bytes and
report it as a usable JPEG. If a multipart endpoint continually sends non-JPEG parts, httpMjpeg
marks the stream connected and refreshes its watchdog while the rendering path discards the data.
Code

android/app/src/main/java/com/openipc/decoder/RtspClient.java[R143-148]

+        if (!videoJpeg || length < 4) return false;
+        Frame frame = framePool.obtain(length);
+        System.arraycopy(buf, offset, frame.data(), 0, length);
+        frame.setLength(length);
+        nalSink.onNal(frame);
+        return true;
Evidence
Both callbacks return true for arbitrary four-byte parts; the session uses that result for
connection status and watchdog timing, while the quad decoder checks the JPEG signature before
rendering.

android/app/src/main/java/com/openipc/decoder/RtspClient.java[140-149]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[552-563]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[563-573]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[595-606]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Non-JPEG multipart parts count as usable frames, keeping a stream connected without displaying video.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspClient.java[140-149]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[552-563]
## Recommended Fix
Check basic JPEG framing before enqueueing in both callbacks and return false for invalid parts so they do not refresh the frame watchdog.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 8/18, lines 664/200; both must reach the floor). Router rationale: Complex multipart parser, authentication, networking, and decoder integration create dense independent risks.

Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit bc26e69 ⚖️ Balanced

Results up to commit N/A


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. JPEG data can be mistaken for a boundary ✓ Resolved
Description
readParts stops a lengthless part as soon as it encounters CRLF-- followed by the boundary,
without checking that a complete delimiter line follows. If JPEG bytes contain that prefix followed
by another character, the frame is truncated and subsequent parts can be skipped or misparsed.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R177-178]

+                byte[] end = ("\r\n" + delimiter).getBytes(StandardCharsets.US_ASCII);
+                if (!readUntil(body, end, part)) return;
Evidence
readUntil returns on the prefix alone, while the caller consumes the rest of the presumed
delimiter line; other delimiter scans also use prefix matching.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[175-185]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[192-200]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[145-150]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A boundary prefix inside a lengthless JPEG is treated as a complete multipart delimiter, corrupting frame parsing.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[141-212]
## Recommended Fix
Recognize a boundary only when its prefix is followed by a valid delimiter terminator. Preserve candidate bytes as frame data when that check fails, and apply the same complete-line check to preamble and fixed-length scans.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. HTTP slot URLs can inject request headers ✓ Resolved
Description
HttpMjpeg.Url.parse copies the URL path into u.path without rejecting carriage returns or line
feeds, and httpMjpeg inserts it directly into the GET request line. A configured URL containing a
raw line break can add headers or alter the request sent to the camera.
Code

android/app/src/main/java/com/openipc/decoder/RtspSession.java[R525-526]

+                String request = "GET " + u.path + " HTTP/1.1\r\n"
+                        + "Host: " + host + "\r\n"
Evidence
The path is copied without validation and then concatenated ahead of the HTTP version and CRLF
before being written to the socket.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-533]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Raw line breaks in an HTTP slot path are written into the GET request line and can inject headers.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-75]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-530]
## Recommended Fix
Validate the parsed request target before constructing the request, rejecting control characters and other characters that cannot occur raw in an HTTP request target.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Digest credentials can inject headers ✓ Resolved
Description
HttpMjpeg.Url.parse percent-decodes credentials without rejecting control characters, which
RtspAuth.quote leaves intact in a Digest authorization value. When a camera challenges with
Digest, a username containing encoded CRLF produces extra request headers; the existing RTSP URL
parser rejects those characters.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[52]

+                u.userInfo = RtspSession.Url.percentDecode(authority.substring(0, at));
Evidence
Basic encoding masks the characters on the first request, but a Digest challenge switches to a
quoted username; quoting escapes slashes and quotes, not CRLF.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[50-54]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[513-538]
android/app/src/main/java/com/openipc/decoder/RtspAuth.java[73-93]
android/app/src/main/java/com/openipc/decoder/RtspAuth.java[169-173]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[1128-1141]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Percent-decoded HTTP slot credentials can carry line breaks into a Digest authorization header.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[50-54]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[1128-1141]
## Recommended Fix
Reject control characters in decoded HTTP user information, using the existing RTSP credential validation as the model.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
4. Some valid stream URLs cannot connect ✓ Resolved
Description
HttpMjpeg.Url.parse splits the authority from the request target only at / and does not remove
URL fragments. A query-only URL such as http://camera?stream=1 becomes an invalid hostname, while
a URL with #preview sends that fragment to the camera as part of the GET target.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R46-49]

+            int slash = rest.indexOf('/');
+            String authority = slash >= 0 ? rest.substring(0, slash) : rest;
+            Url u = new Url();
+            if (slash >= 0) u.path = rest.substring(slash);
Evidence
The parser searches only for a slash and stores the remaining string unchanged; the request uses
that stored target verbatim.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[523-530]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Query-only HTTP URLs are parsed as hostnames, and URL fragments are sent in GET targets.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[43-49]
## Recommended Fix
End the authority at the first path, query, or fragment delimiter; prepend `/` for query-only targets and remove fragments before storing the request target. Add tests for both forms.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Quoted boundaries can stop video playback ✓ Resolved
Description
Response.boundary truncates its parameter at the first semicolon before determining whether that
semicolon is inside a quoted value. A response with boundary="a;b" therefore produces the wrong
delimiter, and readParts never finds the camera's frames.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R94-97]

+            int semi = b.indexOf(';');
+            if (semi >= 0) b = b.substring(0, semi).trim();
+            if (b.length() >= 2 && b.startsWith("\"") && b.endsWith("\"")) {
+                b = b.substring(1, b.length() - 1);
Evidence
The semicolon search runs before quote removal; readParts then builds its delimiter from the
resulting truncated boundary.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[91-101]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[141-147]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A semicolon inside a quoted boundary is mistaken for the end of the parameter, preventing frame parsing.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[86-102]
## Recommended Fix
Parse Content-Type parameters with awareness of quoted values and escapes before extracting the boundary. Cover quoted semicolons in a test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. LF-only streams drop lengthless frames ✓ Resolved
Description
readParts recognizes the end of a lengthless part only as CRLF followed by the delimiter,
although readLine accepts LF-only multipart lines. When a camera uses LF-only lines and omits
Content-Length, the parser reads through the boundary to EOF and returns without delivering the
frame.
Code

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[R175-178]

+                // No length: the data runs up to CRLF + delimiter
+                ByteArrayOutputStream part = new ByteArrayOutputStream();
+                byte[] end = ("\r\n" + delimiter).getBytes(StandardCharsets.US_ASCII);
+                if (!readUntil(body, end, part)) return;
Evidence
The byte matcher requires an exact CRLF prefix and returns false at EOF, whereas header and boundary
line reading explicitly accepts LF alone.

android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[175-185]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[192-212]
android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[224-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
LF-only multipart lines are accepted by the line reader but not by lengthless frame delimiter detection.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[174-185]
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[224-237]
## Recommended Fix
Make lengthless delimiter recognition consistent with the supported line endings while still validating that a candidate is a complete boundary line. Test an LF-only part without Content-Length.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
7. Invalid parts keep a blank stream alive ✓ Resolved
Description
RtspClient.onJpegFrame and QuadCell.onJpegFrame accept any part of at least four bytes and
report it as a usable JPEG. If a multipart endpoint continually sends non-JPEG parts, httpMjpeg
marks the stream connected and refreshes its watchdog while the rendering path discards the data.
Code

android/app/src/main/java/com/openipc/decoder/RtspClient.java[R143-148]

+        if (!videoJpeg || length < 4) return false;
+        Frame frame = framePool.obtain(length);
+        System.arraycopy(buf, offset, frame.data(), 0, length);
+        frame.setLength(length);
+        nalSink.onNal(frame);
+        return true;
Evidence
Both callbacks return true for arbitrary four-byte parts; the session uses that result for
connection status and watchdog timing, while the quad decoder checks the JPEG signature before
rendering.

android/app/src/main/java/com/openipc/decoder/RtspClient.java[140-149]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[552-563]
android/app/src/main/java/com/openipc/decoder/RtspSession.java[563-573]
android/app/src/main/java/com/openipc/decoder/QuadCell.java[595-606]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Non-JPEG multipart parts count as usable frames, keeping a stream connected without displaying video.
## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/RtspClient.java[140-149]
- android/app/src/main/java/com/openipc/decoder/QuadCell.java[552-563]
## Recommended Fix
Check basic JPEG framing before enqueueing in both callbacks and return false for invalid parts so they do not refresh the frame watchdog.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/RtspSession.java
Comment thread android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java
Comment thread android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java Outdated
Comment thread android/app/src/main/java/com/openipc/decoder/RtspClient.java
compot-dev and others added 3 commits October 13, 2026 17:27
…ntials, query/fragment URLs, quoted boundaries, LF-only and boundary-like data in lengthless parts, only JPEG parts count

Co-Authored-By: Claude <noreply@anthropic.com>
…te (#150)

Co-Authored-By: Claude <noreply@anthropic.com>
@compot-dev

Copy link
Copy Markdown
Collaborator Author

Qodo findings 1–7 → cac8f6b + bc26e69:
1/6. Lengthless parts end at a line that is the delimiter (after LF or CR LF, nothing but -- or blanks after it); a look-alike inside the data goes back byte for byte. Tests boundaryTextInsideTheDataIsData, lfOnlyPartsWithoutLength.
2/3. Control characters and spaces in the URL path, and control characters in the decoded credentials, are rejected — nothing can be injected into the request line or the Authorization header. Tests for both.
4. The authority ends at / or ?; a fragment is dropped (http://cam?stream=1#x → GET /?stream=1).
5. A quoted boundary is read up to its closing quote (boundary="a;b").
7. Only parts that start with a JPEG SOI count as frames for the watchdog and the connected state.
Device re-test (bc26e69, AG-216): single view HTTP · MJPEG 1280x720, grid MJPEG reduced · 1.0 fps · software over HTTP, 0 FATAL.

@compot-dev

Copy link
Copy Markdown
Collaborator Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Qodo findings 1–7 → cac8f6b + bc26e69:
1/6. Lengthless parts end at a line that is the delimiter (after LF or CR LF, nothing but -- or blanks after it); a look-alike inside the data goes back byte for byte. Tests boundaryTextInsideTheDataIsData, lfOnlyPartsWithoutLength.
2/3. Control characters and spaces in the URL path, and control characters in the decoded credentials, are rejected — nothing can be injected into the request line or the Authorization header. Tests for both.
4. The authority ends at / or ?; a fragment is dropped (http://cam?stream=1#x → GET /?stream=1).
5. A quoted boundary is read up to its closing quote (boundary="a;b").
7. Only parts that start with a JPEG SOI count as frames for the watchdog and the connected state.
Device re-test (bc26e69, AG-216): single view HTTP · MJPEG 1280x720, grid MJPEG reduced · 1.0 fps · software over HTTP, 0 FATAL.

Thanks for the update. The reported fixes cover findings 1–7, and the current review shows all findings resolved. The device re-test also confirms HTTP MJPEG works in both single-view and grid modes with no fatal errors.

@compot-dev
compot-dev merged commit c47bee7 into v2 Oct 13, 2026
4 checks passed
@compot-dev
compot-dev deleted the feat/http-mjpeg branch October 13, 2026 17:33
Comment on lines +115 to +116
int i = lower.indexOf("boundary=");
if (i < 0) return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Some cameras cannot start mjpeg playback 🐞 Bug ≡ Correctness

Response.boundary() searches only for the literal boundary=, so it misses a boundary parameter
with whitespace before the equals sign. When a camera sends a content type such as
multipart/x-mixed-replace; boundary = frame, httpMjpeg() rejects the response as not an MJPEG
stream before reading any frames.
Agent Prompt
## Issue description
A valid multipart boundary parameter with whitespace around `=` is not recognized, so the HTTP MJPEG connection is rejected.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[110-129]
- android/app/src/main/java/com/openipc/decoder/RtspSession.java[540-544]

## Recommended Fix
Parse content-type parameters as names and values, allowing whitespace around the equals sign while preserving quoted boundary values. Add a response-head test for `boundary = frame`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +153 to +155
else if (name.equals("transfer-encoding")) {
r.chunked = value.toLowerCase(Locale.ROOT).contains("chunked");
} else if (name.equals("www-authenticate")) r.challenges.add(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. A camera response can exhaust app memory 🐞 Bug ☼ Reliability

readResponse() retains every WWW-Authenticate value in Response.challenges without limiting
the number or total size of headers. A camera that continuously sends individually valid header
lines can grow that list until the app runs out of memory, even though each line has a size limit.
Agent Prompt
## Issue description
The HTTP response parser caps individual lines but can retain an unbounded number of authentication headers from a camera.

## Fix Focus Areas
- android/app/src/main/java/com/openipc/decoder/HttpMjpeg.java[134-157]

## Recommended Fix
Enforce a cumulative response-header byte or count limit and throw `IOException` when it is exceeded. Add a test with many short authentication headers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit bc26e69

compot-dev added a commit that referenced this pull request Oct 15, 2026
…ntials, query/fragment URLs, quoted boundaries, LF-only and boundary-like data in lengthless parts, only JPEG parts count

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
…te (#150)

Co-Authored-By: Claude <noreply@anthropic.com>
compot-dev added a commit that referenced this pull request Oct 15, 2026
feat: http:// MJPEG slots — multipart/x-mixed-replace over HTTP (#121)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP MJPEG slots (multipart/x-mixed-replace), e.g. http://<camera>/mjpeg

1 participant