Skip to content

Conversation

@LikoIlya
Copy link
Contributor

@LikoIlya LikoIlya commented Oct 24, 2025

Summary

Some MJPEG endpoints (e.g., FFmpeg’s -f mpjpeg and various embedded devices) omit Content-Length per-part and/or do not send a Content-Type header at all. Previous implementation assumed both headers were always present, causing exceptions and a broken video pipeline.

This PR makes the MJPEG client robust to missing headers by:

  • Detecting the multipart boundary from the HTTP Content-Type header or (if absent) sniffing it from the body.

  • Reading each part by exact length when Content-Length exists, or streaming until the next boundary when it doesn’t.

  • Making header parsing case-insensitive and resilient to minor formatting variations.

  • Hardening the HTTP request (keep-alive, explicit Accept).

Problem

  • KeyNotFoundException at int.Parse(getHeader(br)["Content-Length"]) when per-part Content-Length is missing.

  • Failure to initialize the stream when Content-Type is absent or lacks a boundary=... parameter.

Result: MP cannot display otherwise valid MJPEG streams (e.g., FFmpeg server, some IP cams), even though browsers can.

What’s changed (implementation)

  • Boundary detection

    • Try to parse boundary from Content-Type.

    • If not present, DetectBoundaryFromBody scans the preamble until --<token> and uses <token> as the boundary.

  • Part reading

    • If Content-Length is present → ReadExact(len).

    • If missing → ReadUntilNextBoundary(...) scans bytes until \r\n--<boundary> without over-reading.

    • After length-based reads, safely consume trailing CR/LF (SafeConsumeCrlf).

  • Parsing

    • ReadHeadersCI: case-insensitive header dictionary.
  • HTTP request hardening

    • req.KeepAlive = true, req.Accept = "multipart/x-mixed-replace".

    • Optional read timeout guarded with try/catch.

Compatibility

  • Backwards-compatible: compliant streams with Content-Type + Content-Length continue through the original exact-length path.

  • Adds support for more “relaxed” MJPEG producers (FFmpeg, some IP cameras, microcontroller streams).

Performance notes

The boundary-scan path reads byte-by-byte to avoid over-reading on non-seekable streams. This is reliable at 720p60 MJPEG in testing; if benchmarks ever show pressure, could be micro-optimize with a small rolling buffer. The exact-length path remains unchanged and fastest.

Testing

Tested against:

  • FFmpeg pass-through MJPEG (no re-encode, no per-part Content-Length):
ffmpeg -f dshow -framerate 60 -video_size 1280x720 -input_format mjpeg -i video="<WEBCAM_DEVICE>" -an -map 0:v:0 -c:v copy -f mpjpeg -fflags nobuffer -muxdelay 0 -listen 1 http://<IP>:<PORT>/stream.mjpg
  • FFmpeg with missing Content-Type (simulate via proxy/stripper) + body-only boundary.

  • Browser sanity: http://<IP>:<PORT>/stream.mjpg renders; closing the browser allows MP to connect (FFmpeg -listen 1 serves one client at a time).

Repro in MP:

  1. Start FFmpeg server (above).

  2. In MP, set source to http://<IP>:<PORT>/stream.mjpg.

  3. Video displays without exceptions even when Content-Length is not sent per part.

Note: If you keep the browser tab open, MP may fail to connect because FFmpeg’s -listen 1 serves only one client. Either close the tab or run a second FFmpeg on a different port for preview.

Risks / Mitigations

  • Stream variations: Multipart formatting in the wild can be quirky. Implementation handle both Content-Length and lengthless parts; also tolerate missing Content-Type by sniffing the boundary from the body.

  • Latency: No additional buffering introduced; the boundary-scan path avoids building up frames and keeps latency low.

Follow-ups (optional)

  • Add small ring-buffer optimization for the boundary scanner if future profiling suggests it.

  • Add unit/IO tests for: (1) with/without Content-Type, (2) with/without Content-Length, (3) different boundary tokens, (4) unusual CR/LF patterns.

…PEG streams

Some MJPEG producers (e.g., FFmpeg mpjpeg, certain IP cams) omit per-part Content-Length and/or the top-level Content-Type boundary, causing crashes and broken video in Mission Planner.

Changes:
- Parse boundary from Content-Type; if absent, sniff boundary from body.
- Case-insensitive header parsing.
- Read frame by exact length when Content-Length exists; otherwise read until the next boundary (no over-read on non-seekable streams).
- Safely consume trailing CR/LF after length-based reads.
- Harden HTTP request: bypass proxy, keep-alive, explicit Accept.

Behavior:
- Backwards-compatible for compliant streams.
- Adds support for FFmpeg-style MJPEG and other minimal endpoints.

Tested with:
- FFmpeg MJPEG pass-through (720p60) without Content-Length.
- Browser preview and MP client (note: -listen 1 serves one client at a time).

Refs: robustness for multipart/x-mixed-replace streams without strict headers.
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.

1 participant