System designTroubleshooting

Invalid JPEG file structure: two SOI markers in an MJPEG stream

Two SOI markers means your viewer joined the MJPEG stream mid-frame. Read the JPEG markers, prove the splice with a byte count, then fix the URL or the parser.

LumaSpec Vision10 min read Share on LinkedIn

On this page 8 sections
  1. 01What "two SOI markers" means in a JPEG
  2. 02How a live MJPEG stream loses its frame boundaries
  3. 03The causes to check, in order, with a test for each
  4. 04Worked example: counting markers in a 10 s capture at 10 fps
  5. 05The fixes, in the order that works
  6. 06When to hand the stream to dedicated hardware
  7. 07Checklist before you change any code
  8. 08Questions

The viewer connects, the video runs for a few seconds, then it stops with "IIOException: Invalid JPEG file structure: two SOI markers". The message comes from the JPEG decoder inside your client: the bytes it received contained a second start-of-image marker before any frame finished. That is a frame-boundary fault between the stream and the parser, not a dying camera. This page gives you the marker structure every correct frame follows, the likely causes ranked by how often they turn out to be the culprit, a capture-and-count test that pins down the exact splice, and the fixes: a parser that honors declared part lengths, a clean reconnect, or a still snapshot URL polled on a timer instead of a live feed joined mid-frame.

What "two SOI markers" means in a JPEG

A JPEG image is a strict marker sequence defined in ITU-T T.81. It opens with the two bytes FF D8, Start of Image (SOI), and closes with FF D9, End of Image (EOI). Between them sit the header segments: a JFIF APP0 block, quantization tables, the Start of Frame header (SOF, FF C0), then Start of Scan (SOS, FF DA) and the compressed data. The standard permits exactly one SOI and one EOI per image.

Motion JPEG is a sequence of such images: each frame is compressed as an independent JPEG and the frames are sent one after another. A decoder handed one frame therefore expects a single SOI at byte 0, headers, scan data, and a single EOI at the end.

The error text names the broken rule. "Two SOI markers": the decoder read FF D8, started through the headers, and met a second FF D8 before any SOF or SOS. The slice is not one frame — it is a fragment of one frame glued to the head of the next. The twin error "SOS before SOF" means the client joined inside a frame's scan data. And "extraneous bytes before marker" means tail bytes rode along in front of the real start; that variant is covered in corrupt JPEG data: extraneous bytes before marker. All three describe the same fault: the client is cutting the byte stream in the wrong places.

How a live MJPEG stream loses its frame boundaries

A live MJPEG feed over HTTP is normally a multipart/x-mixed-replace response. The server writes a boundary line, then per-frame headers — Content-Type: image/jpeg and usually Content-Length — then the JPEG bytes, then the next boundary, forever. The client's only job is to cut between frames at those boundaries.

The cut goes wrong four ways. The client connects while the server is mid-frame, so recording starts at an arbitrary byte. It ignores Content-Length and guesses. It matches the boundary string by hand and misses one. Or it consumes frames slower than the camera produces them, the socket buffer overruns, and bytes are discarded from the middle of a part.

This is also why the video looks fine first. Early frames land on boundaries, the first missed boundary shifts every later cut, and the message appears only when a shifted slice lands header-on-header. The corruption begins before the error shows.

The causes to check, in order, with a test for each

Work down the list and stop at the first test that fails.

  1. URL type and viewer mode mismatch. A still snapshot URL fed to a viewer expecting multipart — or the reverse — produces slices at arbitrary offsets. Test: request the URL once and read the first bytes. A snapshot endpoint returns FF D8 immediately; a multipart endpoint returns boundary text. Then set the viewer's video mode to match what the URL actually serves. If the viewer shows nothing at all, start with MJPEG viewer shows no video; for HMI work see display an RTSP camera stream in an HMI.
  2. Mid-stream join. A reconnect after a drop resumes mid-frame. Test: after the error, reconnect. If the first seconds are always clean and the error returns after the next network hiccup, the join is the culprit. At every reconnect, discard everything up to the next boundary before decoding.
  3. Naive slicing. Test: log each slice's length against the Content-Length its part declared. A mismatch means the parser guessed. Header payloads are arbitrary bytes, so a blind search for FF D8 can cut inside a header by chance.
  4. Transport loss. Test: open the same URL in a second client on a wired link. If it runs clean for minutes, the camera and network are fine and your reader is losing bytes through slow consumption or buffer handling.
  5. Corrupt saved file. The same message fires on a single still file when two JPEGs were appended into one file or a write was truncated. Test: count FF D8 and FF D9 in the file and apply the count check in the next section.

Worked example: counting markers in a 10 s capture at 10 fps

Capture the failing feed to disk while the error is on screen; 10 s matches the few seconds of good video the message typically follows. The example uses a 10 fps stream, a common camera setting — substitute your own configured rate. The marker bytes come from the JPEG standard itself: SOI is FF D8, EOI is FF D9.

The rule: a complete frame holds exactly one SOI and one EOI, so a healthy capture satisfies count(SOI) = count(EOI) = number of frames sent. Expected frames:

frames = run time (s) × frame rate (fps)

10 s × 10 frames/s = 100 frames expected. The count on a failing capture came back FF D8 × 101, FF D9 × 100: one start marker with no end marker of its own before the next start — one splice.

Find it by offset. List the byte positions of every FF D8 and look at the gaps: real frames at inspection resolutions run to thousands of bytes, so two SOI starts a few dozen bytes apart with no FF D9 between them cannot be two frames. A failing slice looks like this:

offset 0:   FF D8            SOI
offset 2:   FF E0 00 10      APP0 header, segment length 16
offset 6:   4A 46 49 46 00   "JFIF"
offset 11:  01 01            JFIF version 1.1
offset 13:  FF D8            second SOI, no SOF or SOS in between

The two SOI starts sit 13 bytes apart. No complete frame fits in 13 bytes, so this slice is the head of one frame glued to the head of the next.

Decision: an SOI count one above the EOI count, plus one SOI-to-SOI gap far below your average frame size, convicts the client's slicing — the camera itself sent valid frames. If the counts match and the lengths are right but the error still fires, check both ends of each slice: the first two bytes must be FF D8 and the last two FF D9. A slice missing its closing FF D9 is a truncated write or a premature buffer flush.

The fixes, in the order that works

  1. Poll the still snapshot URL for inspection work. Each request returns one complete JPEG from a fresh frame, so the boundary problem cannot occur. Set the interval from the frame period — at 10 fps that is 100 ms (1 s ÷ 10) or slower. The request pattern is in HTTP snapshot URL polling and the grab loop in grab images from an IP camera over an HTTP snapshot URL.
  2. Parse the multipart response properly for continuous video. Take the boundary token from the response's Content-Type header, read each part's headers, and cut the JPEG at exactly Content-Length bytes; never re-scan inside a part. If you stream from your own code, the server side must declare the boundary and lengths too — the working pattern is in stream camera frames in Flask with multipart JPEG.
  3. Recover cleanly. On any decode error, length mismatch or missing EOI, close the socket, discard the partial part, reconnect, and drop the first frame after connecting. The sequence is in MJPEG stream restart.
  4. Keep the reader inside the frame period. At 10 fps that is 100 ms per frame. Move saving, logging and display off the read loop so a slow consumer cannot overrun the socket buffer mid-part.
  5. Archive one file per frame. A file holding two appended JPEGs trips strict decoders with this same message — the failure comes from the JPEG library inside the reader, not from the streaming application that wrote it.

When to hand the stream to dedicated hardware

If the frames feed a decoder rather than a person, boundary integrity is a requirement: one splice can cost a read. The decode side of that chain — protocol choice, slicing and retry logic — is in decode barcodes from a live RTSP or MJPEG video stream. If the end goal is codes on the line, a fixed code reader does acquisition and decoding in one networked device and removes the DIY stream entirely:

Fixed code readers2560 x 2048 fixed code reader, 30 fps, 60 codes/s, 8 mm lens (LS-CRD-0028)LS-CRD-0028
Interface
Fast Ethernet
Resolution
5 MP
Sensor format
1/1.7"
Shutter
Global
$920.00View part

The LS-CRD-0028 reads 60 codes/s from 2560 x 2048 frames at 30 fps over Fast Ethernet.

For human monitoring, the same boundary rules apply to whatever serves the page, including smart cameras publishing inspection video to a dashboard — the pattern is in stream smart camera live video and metrics to a custom dashboard. A source with fixed, well-formed output makes the client's job trivial:

Smart cameras & vision sensors1.6 MP smart camera, 60 fps, 16 mm lens, white light (polarized, transparent light) (LS-SMC-0063)LS-SMC-0063
Interface
GigE
Resolution
Up to 3 MP
$1,110.00View part

The LS-SMC-0063 captures 1.6 MP frames at 60 fps over GigE. At that rate one frame arrives every 17 ms (1 s ÷ 60), which is the budget your reader must beat per frame.

Checklist before you change any code

  • First two bytes of every slice are FF D8 and last two are FF D9 — a slice failing either end is mis-cut or truncated.
  • Over a 10 s capture, count(FF D8) equals count(FF D9) equals the frames the camera sent — a mismatch of one marks the splice.
  • Every slice length equals the Content-Length its part declared.
  • The viewer's video mode matches the URL: still JPEG for a snapshot endpoint, multipart for the live feed.
  • Reader work per frame stays inside the frame period — 100 ms at 10 fps.
  • Archives hold one JPEG per file, never appended frames.

Fix the boundary handling first: that is where this error lives in practice. If you would rather remove the fragile link between camera and decoder altogether, pick the source in the camera finder or start the configurator at the camera step. More stream and capture fixes sit on the systems topic page, and the contact page reaches the applications team when a build decision is close.

Questions

What are the SOI and EOI marker bytes in a JPEG file?

SOI is FF D8 and must be the file's first two bytes; EOI is FF D9 and must be the last two. The JPEG standard, ITU-T T.81, allows exactly one of each per image. Inside the compressed scan data a standalone FF is escaped as FF 00, so a genuine FF D8 cannot appear mid-scan. Restart markers FF D0 through FF D7 do appear inside scans and are not frame boundaries.

Why does the video play for a few seconds before the two SOI error appears?

The connection usually opens at a frame boundary, so the first frames slice cleanly. The corruption starts when the client misses one boundary: a slow reader, a socket-buffer overrun or a mid-part reconnect. The message only surfaces when the shifted slicing hands the decoder a slice carrying two SOI markers, which can be many frames after the actual splice.

Can I split an MJPEG stream by searching for the FF D8 bytes?

Mostly, but not safely. Byte stuffing keeps FF D8 out of scan data, yet APP and COM header payloads are arbitrary bytes and can contain FF D8 by chance, which cuts a frame at a false boundary. Read the boundary token and each part's Content-Length from the multipart response and slice at the declared length instead.

Does the two SOI markers error mean the camera is faulty?

No. The message is raised by the JPEG decoder in the client, so it describes the bytes the client received, not the sensor or the camera's compression engine. Run the same URL in a second client on the same network: if it plays cleanly for minutes, the camera and its stream are fine and the fault sits in your parser, its buffer handling or a URL and mode mismatch.

Should I use MJPEG or still snapshot grabs for machine-vision inspection?

Use still snapshot grabs for anything measured or decoded: each request returns one complete JPEG, so boundary errors cannot occur and exposure is set per grab. Keep MJPEG for human viewing such as dashboards and remote monitoring, where a rare artefact is cosmetic. If the frames feed a barcode decoder, a dedicated networked reader avoids the DIY stream chain entirely.