The pattern is familiar: an embedded camera serves MJPEG over HTTP, the first connection streams perfectly, and every restart after that shows a frozen frame or a connection that never comes up. The sensor is not failing and the listening socket is not broken. Two small defects went wrong together: the streaming loop leaves its accepted client socket open when it exits, and the browser never opens a fresh connection because the image URL is byte-for-byte identical to the one it already cached. This guide gives you the 30-second test that separates the server side from the browser side, the few-line fix on each side, and the checks that keep a restart clean on the next firmware revision.
Symptom patterns and the 30-second test
Each symptom points at a different layer, so run the test before changing code: the two fixes live in different files, and applying the wrong one costs a firmware cycle. Suspect the leaked accepted socket first, the replayed cache second, link trouble third.
| Symptom | Layer to suspect | 30-second test | Fix |
|---|---|---|---|
| Second start shows one static frame; motion never resumes | Browser replaying the cached first response | Hard-reload the page (Ctrl+F5). Motion returns: the URL is the defect | Timestamp tag on the stream URL |
| Second start hangs; no headers arrive at all | Server socket state: the previous accepted socket is still open | curl the stream URL from a PC. curl hangs while the browser showed a frame: the server side is stuck | client.close() in a finally block |
| Stream dies mid-view after a fixed delay; every start then fails | A swallowed socket timeout exits the loop without cleanup | Watch the console as the delay hits. A silent exit means the exception was passed over | Same close fix, plus log the exit reason |
| Reconnect works, but only after 5-10 s of nothing | TCP retry backoff on a blocked listener or full socket pool | ping the camera during the wait. It answers: the network is up, the server is not accepting | Fix the leak; the wait disappears |
The test that settles it:
curl -s -m 3 http://192.168.1.50:8080 | head -c 200
The address is illustrative; use your camera's. A healthy server answers at once with a status line, a Content-Type: multipart/x-mixed-replace header carrying your boundary token, then JPEG parts. Headers that stop without image data mean the send loop stalled mid-frame — go to the bandwidth section. No headers inside three seconds means nothing accepted the connection: server side. For restart failures that reach past this pattern, the camera stream restart timeout diagnosis runs the same triage from the network side.
Cause 1: the accepted client socket never closes
accept() returns a connected socket, not a flag. The listener and the client connection are two objects with two lifetimes: closing or rebinding the listener does nothing to a connection already accepted. If the send loop exits through its socket timeout — client.settimeout(5.0) turns any send that stalls five seconds into an exception — and the handler is an empty except OSError with pass, control returns to the command loop with the client socket still open and nothing owning it. The browser still holds an established connection; your stack still carries the socket in its table. Embedded TCP stacks have a handful of sockets in total, so every leaked one shrinks the pool, and after a few start-stop cycles a new connection has nothing left to use.
The fix is one line in the right place:
# Illustrative names: server is the listening socket, client the accepted one.
# accept(), settimeout(), sendall() and close() are standard socket calls.
def stream_video(server):
client, addr = server.accept() # one connection per viewer
client.settimeout(5.0) # a stalled send exits the loop
try:
client.sendall(stream_header) # multipart header, built once
while True:
frame = sensor.snapshot()
jpg = frame.compressed(quality=35)
client.sendall(part_header(jpg.size())) # boundary + length line
client.sendall(jpg)
finally:
client.close() # runs on timeout, reset and stop
stream_header and part_header stand for your own header bytes: the boundary line, Content-Type: image/jpeg and a Content-Length per part. sensor.snapshot() and frame.compressed() stand for whatever your camera runtime calls the same two steps.
Three details in the same file deserve the same pass. First, the elapsed-time limit must call the diff helper with both timestamps — on embedded Python runtimes that provide it, that reads time.ticks_diff(time.ticks_ms(), start); subtracting the two values inside the parentheses instead of passing them as two arguments wrecks the stop condition, so the loop never ends on its own. Second, keep SO_REUSEADDR on the listener: it lets the server rebind the port at once after a restart instead of failing with address-in-use, though it does nothing for the frozen stream itself. Third, log the exit reason — an exception passed over in silence is what turns a five-second timeout into a permanent leak.
Cause 2: the browser replays the cached stream
The second defect lives in the page. The image element asks for the same URL every time: address plus port, byte for byte. Cache lookups key on that exact string, and a replacing multipart response is a special case — several browsers treat the arriving content as a fresh load and do not carry the first response's cache policy the way they would for a plain image. Cache-Control: no-cache and Pragma: no-cache in the multipart header help some clients and do not bind the rest. Removing the image element from the page does not close the connection either; the request ends on the browser's own schedule.
The deterministic fix is to never show the cache a URL it has seen:
// ip holds your camera's address; the server never reads the query tag.
var timestamp = new Date().getTime();
var img = document.createElement('img');
img.setAttribute('src', 'http://' + ip + ':8080/?t=' + timestamp);
document.getElementById('sec_div').appendChild(img);
The server receives the same GET with a ?t= millisecond count appended and routes on the port and path as before. Every start now presents a URL the cache has never stored, so the browser must open a new connection — exactly the fresh multipart GET your repaired server loop is waiting to accept.
Stopping deserves the same care as starting. Clearing the container's HTML alone is what leaves a static frame behind: remove the element, then reassign its src away from the stream URL — a one-pixel data URI works — so the old resource is canceled at once instead of timing out.
Prove the fix: three clean restarts
- Start the server, open the page, confirm motion.
- Stop the stream from your interface, or let its time limit expire. Your log line names the exit reason, and client.close() runs because it sits in a finally block.
- Press start again without touching anything but your button. Motion resumes within one to two seconds.
- Run a third cycle, then repeat the curl command from above: headers must return on demand after any number of stops.
A fix that survives three clean restarts survives the production shift. If the third start still stalls, what remains is physical, not logical.
Still stalling: size the stream to the link
With both layers fixed, the freeze that remains is throughput. A link that cannot carry the stream stalls a send, the five-second timeout fires and the loop exits — harmless now that cleanup runs, but the view still stops. Size it before deployment:
Worked example with illustrative sizes: QVGA is 320 × 240 pixels, and grayscale JPEG at quality 35 typically lands between 10 and 20 KB per frame on a machine-vision scene. Take 15 KB at 20 frames per second: 15,000 B × 20 × 8 = 2,400,000 bits/s, a 2.4 Mbit/s payload. Budget roughly 3 Mbit/s once TCP, IP and wireless overhead join. Halve the frame rate and the payload halves with it: 10 frames per second puts the same stream at 1.2 Mbit/s. Measure rather than estimate — the compressed size your loop already computes gives you the real frame size.
Margins put those numbers in perspective. A wired 100 Mbit/s port carries the 3 Mbit/s budget more than thirty times over, GigE more than three hundred times. Wireless at the edge of range can deliver less than the budget, and those stalls correlate with distance and interference, not with restart count. Keep the socket timeout near five seconds: shorter values cut streams on ordinary jitter; longer ones let a dead viewer hold the socket for most of a command cycle.
If the viewer is a PC program rather than a browser, take the browser out of the loop: acquiring an RTSP stream from an IP camera in your program replaces page caching with a client you control. The same exit discipline — stop acquisition before you close the handle — is also what fixes USB cameras that time out on reopen: stop acquisition before you close.
Prevention checklist for the next revision
- Close every accepted socket in a finally block — a timeout or reset is a normal exit, not an exceptional one.
- Append a timestamp or counter tag to every stream URL — cache keys are exact strings, so make every key new.
- Send Cache-Control: no-cache and Pragma: no-cache in the multipart header — correct intent that helps the clients which honor it, never the only defense.
- Keep SO_REUSEADDR on the listener so a restart rebinds at once — it fixes address-in-use, nothing else.
- Log the exit reason instead of passing on the exception — a silent timeout is what hides the leak behind the next freeze.
- Check frame size × frame rate × 8 against the link before deployment, and keep it under half the rate the link reliably delivers — headroom absorbs retransmits before they stall a send.
- Give each stream a server-side time limit when the viewer may vanish — a browser that walks away never sends a close, so only your timeout ends the conversation.
The systems topic page gathers the neighbouring stream and acquisition fixes.
Building the view on purpose
If the station needs a live operator view by design, give each job a part that owns it: a frame source, a host that serves, and where possible a decision made on the device.
The frame source: a board-level camera such as the MV-CB016-10GM-M-W feeds frames to your host over its GigE interface, leaving the HTTP layer to the code you now know how to keep alive.
- Sensor format
- 1/2.9"
- Lens mount
- M12
- Interface
- GigE
- Resolution
- Up to 3 MP
The host: a vision controller serves the stream beside the inspection application, with camera ports and light channels already wired in. The VC2100 unit below pairs an N97 processor with 8 GB of memory, a 1 TB hard drive and four built-in light channels.
- LAN ports
- 3-4 LAN ports
- USB 3 ports
- 2 USB 3 ports
- Light outputs
- 4 built-in channels
- Memory
- 8 GB
The shortcut: when the browser exists only so someone can watch an inspection run, a smart camera makes the decision on the device and the stream becomes a convenience. The SC5000X model below inspects at the camera, so a stalled view never stops the verdicts.
- Interface
- GigE
- Resolution
- 6-10 MP
- Sensor format
- 1/2.4"
- Shutter
- Rolling
For viewing without custom code, the vision sensor browser display for local operator viewing shows the ready-made path, and a remote IP camera snapshot URL covers the one-shot case: one request, one response, no multipart state to leak. Shortlist hardware in the camera finder.
The decision, case by case
- Static frame on the second start while curl still returns headers: browser cache. Add the timestamp tag — one line, and the refresh test confirms it.
- curl hangs, or the pool empties after a few cycles: leaked accepted socket. Wrap the send loop so client.close() runs in a finally block, and log the exit reason — two lines.
- Stalls that follow signal strength instead of restart count: bandwidth. Drop frame rate or quality until frame size × frame rate × 8 sits under half the link's reliable rate.
- Apply both code fixes on any server you own: the layers fail independently, and together they cost about ten lines.
Neither defect is a hardware defect — no camera swap closes a leaked socket or renames a URL. If you are specifying the station rather than debugging it, the configurator walks camera, lens and light in order, or reach the team through the contact page.
Questions
Does the timestamp query tag change anything on the server?
No. The browser sends the same GET with ?t= plus a millisecond count appended, and the server keeps routing on the port and path; nothing in your handler reads it. The tag exists because a cache lookup is keyed on the exact URL string, so a URL the cache has never stored forces a real request and a fresh multipart connection. Milliseconds also keep every URL unique for the life of the page.
Why are the no-cache response headers not enough on their own?
Keep sending them, but do not rely on them for multipart/x-mixed-replace. Several engines treat the arriving multipart content as a fresh load and do not apply the first response's cache policy the way they would for a plain image or page. The unique query tag works everywhere because it never gives the cache a matching key.
How do I stop the stream cleanly from the browser?
Clearing the container's HTML removes the image element but leaves its connection to die on the browser's schedule. Remove the element, then reassign its src away from the stream URL — a one-pixel data URI loads instantly with no network and cancels the old resource. Keep the server-side socket timeout too, so a viewer that walks away cannot hold the accepted socket past five seconds.
Why does a reconnect sometimes wait five to ten seconds?
That pause is TCP retransmission backoff, not your loop. When an open attempt is dropped — a blocked listener, a socket pool with nothing free, or wireless interference — the operating system retries at roughly 1, 2, 4 and 8 seconds, and the cumulative wait is what you are watching. Fix the underlying socket or slot shortage and the delay disappears with it.
Can more than one viewer watch the same stream?
This pattern serves one viewer per accept call, so viewers take turns unless the loop hands each connection to its own task. Simultaneous viewers need per-client handling or a dedicated streaming service; a vision controller with several Ethernet ports can keep viewer traffic off the camera network. If a second application also reaches the camera over TCP, the same one-connection discipline applies there.


