A camera URL that plays in a browser and fails in an HMI video player with a file not valid error is one of the most common faults in camera-to-HMI integration, and the URL is almost never the cause. The player is telling you the response body is not a media file it can decode. Most cameras deliver MJPEG over HTTP as a multipart wrapper — a boundary line and per-part headers ahead of every JPEG — and browsers accept that wrapper where media player components reject it. Below you will capture the response, identify the wrapper from its first bytes, and pick one of three fixes: poll the snapshot URL, give the player an access path it can use, or re-stream the video in a format it decodes.
Why the browser plays it and the player does not
MJPEG over HTTP is not a video file. The camera sends one response that never ends, and every frame arrives as its own part inside a multipart/x-mixed-replace body, introduced by a boundary line and a couple of headers. Browsers handle this in image contexts: they read the boundary, decode each JPEG and swap the picture in place, so the URL looks perfectly healthy in a browser tab.
A media player component validates the body instead. It expects either a bare image or a video stream in a codec it decodes, and it meets a line of boundary text and headers where it expected FF D8. It stops there and reports the failure in its own wording — file not valid, error loading media, no playable source. The bytes are identical; the parsers differ.
That gives the decision rule for this fault. Browser yes, player no: suspect the body wrapper first, authentication second, codec support third. If the player shows no video even with a URL that looks right, the capture below settles which of the three it is, and the guide on an MJPEG viewer showing no video covers the cases where the URL points at the wrong stream entirely. The broader choice between an MJPEG stream, an RTSP stream and a snapshot URL is laid out in the guide to HMI camera widgets: MJPEG versus RTSP and snapshot JPEG URLs.
Capture the response and read the first bytes
One capture answers the format question. From any machine that reaches the camera:
curl -s -D headers.txt -o body.bin "http://<camera-ip>/<stream-path>"
curl -s -D headers.txt -o snap.jpg --digest -u viewer:password "http://<camera-ip>/<snapshot-path>"
The angle-bracket values are yours: the camera address and the two paths come from its manual, and viewer:password stands for a read-only account. Read two things from the results.
In headers.txt, find the Content-Type line. image/jpeg means the endpoint returned one bare JPEG, and the player accepts that. A multipart type such as multipart/x-mixed-replace, with a boundary parameter, means a stream wrapper, and that is the fault. A 401 with a WWW-Authenticate: Digest header means authentication blocks you before format even matters.
In body.bin, look at the first bytes. A bare JPEG begins FF D8, which shows as the characters ÿØ in a text editor, usually followed by JFIF. A multipart body begins with readable text — the boundary line, then Content-Type and Content-Length headers, a blank line, then the JPEG. Bytes of junk ahead of FF D8 are the corrupt-stream case, and the fix for extra bytes before the marker is the same practical move: get a clean single image.
The body also hands you the frame size. The Content-Length line inside each part is that frame's byte count, which feeds the bandwidth math in the worked example below. If you want to see the multipart format from the server's side, the Flask multipart JPEG streaming example builds exactly this body by hand.
Worked example: a 1 fps stream whose frames carry 150,773 bytes
Inputs, taken from the capture above of a camera whose stream URL asks for one frame per second: the body opens with a boundary line of roughly 15 to 20 bytes; each part carries the two header lines Content-Type: image/jpeg (24 characters) and Content-Length: 150773 (22 characters), each with a line ending, plus a blank line; the JPEG data follows and shows its JFIF marker. One frame is 150,773 bytes and the wrapper around it comes to about 70 bytes.
Rule, in words: bit rate equals bytes per frame times frames per second times eight, and a planning-safe load is half the link speed.
Arithmetic for the stream: 150,773 B/frame × 1 frame/s × 8 = 1,206,184 bit/s ≈ 1.2 Mbit/s per camera. Wrapper overhead: 70 B out of 150,773 B is 0.046% of the frame, and even at 15 frames/s the wrapper adds 70 × 15 ≈ 1 kB/s. The wrapper is not a bandwidth problem. Capacity: 100 Mbit/s × 0.5 = 50 Mbit/s of planning headroom, and 50 ÷ 1.2 ≈ 41 such cameras per half link at 1 fps. At 15 fps the same frames need 150,773 × 15 × 8 = 18.1 Mbit/s each, and 50 ÷ 18.1 ≈ 2.8, so two or three cameras fill the half link.
Decision: bandwidth was never the fault. One frame per second costs about 1.2 Mbit/s — trivia on a plant network — and the wrapper is under 0.05% of it. The player rejects the body because of its format, so the fix changes the format, not the network: polling the snapshot endpoint once per second moves roughly the same 1.2 Mbit/s and returns image/jpeg, which the player accepts. Keep the multipart stream for browser pages.
Fix 1: point the player at the snapshot URL and refresh it
Nearly every camera that offers the stream also offers a still. It is usually the same CGI path with a snapshot action in place of the stream action, often with a profile parameter that picks size and quality. That endpoint returns Content-Type: image/jpeg with nothing but the one frame — the body the player wanted all along. If the still address is not in the manual, the guide to finding a camera's MJPEG CGI URL when RTSP fails walks the discovery with the browser's network tools.
Then configure the player for a still. The case above ran the component with its live feed option switched on, which is what made it treat the multipart body as a media file; turn that off, point the component at the snapshot URL and drive refresh with a timer of one or two seconds. Append a changing query value, for example ?ts= plus a timestamp, so nothing between camera and screen serves a stale frame — cached snapshots are the classic follow-up fault.
Bandwidth comes straight from the worked example: one frame per second is about 1.2 Mbit/s per camera, two per second about 2.4 Mbit/s. Snapshot URLs also keep HMI pages light, which matters once a dashboard carries several views; the stream-versus-snapshot effect on page speed has its own write-up. For one widely deployed HMI platform, this guide to IP camera integration with MJPEG and JPEG still streams documents the snapshot-plus-timer pattern end to end.
Fix 2: authentication the player never sends
If the capture came back 401 with a WWW-Authenticate: Digest header, no URL editing helps: the camera demands digest authentication and the player sends no credentials at all. Embedding a user name and password in the URL does not rescue you either — many player components ignore URL credentials entirely, and platforms strip them for security anyway.
Two ways through, in order of preference:
- Create a read-only viewer account on the camera and enable its anonymous viewer option if the firmware offers one. The player then connects with no credentials. Pair it with isolation: the camera goes in its own VLAN or behind a firewall rule so only the HMI server reaches its address, and the admin account keeps strong, unique credentials.
- If anonymous access is against site policy, leave authentication on and use the poll-and-serve pattern in Fix 3. curl negotiates digest authentication properly, and the player only ever talks to your own server.
The wider pattern of camera URLs that authenticate in a browser but fail inside an HMI has its own guide on camera stream authentication when URL logins fail.
Fix 3: re-serve or re-stream what the player can decode
Two service shapes cover everything the player refuses.
Poll-and-serve keeps the still-image approach and closes the authentication gap. A scheduled script or gateway task downloads the snapshot with curl --digest and writes it to a web root, a lightweight local web server serves it, and the player points at the local URL and refreshes on a timer. The picture is as current as the poll interval, and one to two seconds is practical. This is the classic CCTV integration pattern — one server fetches, many clients view — and these notes on CCTV integration using MJPEG video servers describe it in detail.
A restream gateway is the answer when you need real motion inside the player. A small media service pulls the camera's stream, RTSP where the camera offers it and the MJPEG URL otherwise, and republishes it in a container and codec the player decodes. It also normalizes the boundary quirks that some camera firmwares produce, which strict parsers trip over. Check first which protocols your player component accepts: if it takes RTSP natively, point it at the camera's RTSP address and skip the gateway. The general wiring of a feed into an HMI page is covered step by step in the guide to showing a camera live stream in an HMI web viewer widget.
Which fix to choose
- A still image refreshed every second or two is enough, as it is for most monitoring dashboards: Fix 1, the snapshot URL plus a timer. Cheapest to build, least to break.
- The camera forces digest authentication and policy forbids anonymous viewer access: Fix 3, poll-and-serve with curl.
- Anonymous viewer access is acceptable and the firmware offers it: Fix 2, with a VLAN or firewall rule around the camera.
- Real motion video inside the player is a requirement: Fix 3, RTSP if the component supports it, otherwise a restream gateway.
- Only browsers ever look at this camera: change nothing. The multipart stream is the right format for them.
More diagnostics for camera streams and HMI integration are collected on the systems topic page.
The same test before every camera-to-viewer integration
Run the capture before you wire anything in, every time:
- Fetch the candidate URL with curl -D headers.txt and keep the body.
- Check the Content-Type header and the first bytes: image/jpeg with FF D8 for a still, a boundary line for a stream wrapper, 401 for an access problem.
- Read the per-frame Content-Length and compute the bit rate; plan the total for all cameras under half the link.
- Decide the access model: viewer account, anonymous viewer on an isolated segment, or poll-and-serve.
- Note the working URL and repeat the capture after any camera firmware update, because CGI behavior is exactly what updates change.
The same sequence applies when the image source is a machine-vision camera rather than a security camera: smart cameras and fixed code readers sit on Fast Ethernet or GigE, so the capture, the header check and the bandwidth math carry over unchanged.
- Interface
- Fast Ethernet
- Resolution
- 5 MP
- Interface
- Fast Ethernet
- Resolution
- Up to 3 MP
The MV-SC3050XC-16M-ATN-H is a 5 MP color vision sensor on Fast Ethernet. The MV-ID3013PM-12M-WBN is a 1.3 MP compact code reader with a 12 mm focal length. Whatever still or stream address the device manual gives, capture it once with curl before it enters a viewer, and the fault in this guide never reaches the dashboard.
If you are specifying the cameras as well as the wiring, the camera finder filters by interface, resolution and shutter, and the configurator walks a station end to end. When a stream still refuses to play after all of this, the team answers on the contact page.
Questions
Why does the MJPEG URL play in a browser but fail with file not valid in the player?
The message text varies — error loading media, file not valid, unsupported source — but every version means the same thing: the component's parser gave up before it found a decodable frame. Browsers accept the multipart wrapper in image contexts; players validate the body as a single file or stream and stop at the boundary text. Some players also accept MJPEG only over RTSP, never over HTTP, which the capture in this guide reveals immediately.
How do I find the snapshot URL on a camera?
Open the camera's web interface, start a live view and watch the browser's network tools for a request whose response type is image/jpeg — that address is the snapshot URL. Camera manuals and API lists print it too, usually close to the stream endpoint. Confirm it before wiring it in: fetch it with curl -D headers.txt and check that the header says image/jpeg and the body begins with FF D8.
What refresh rate should a snapshot poll run at on an HMI dashboard?
One to two frames per second is enough for most monitoring dashboards, five at most. Size the link first: bytes per frame times frames per second times eight gives bits per second. A 150 kB frame at 2 fps is about 2.4 Mbit/s; at 5 fps, 6 Mbit/s. Keep the planned total for all cameras under half the switch link, and move anything faster to a real video stream.
Can I put the username and password in the camera URL?
Treat that as broken. Most player components pass no credentials at all, so a URL with the user name and password embedded is ignored, and platforms increasingly strip URL credentials for security. Cameras configured for digest authentication refuse plain URLs regardless. Use a read-only viewer account the player can reach without credentials on a restricted network segment, or fetch frames with curl --digest from a script and re-serve them locally.
Can I keep the multipart stream and show it some other way?
Yes. Browser pages render multipart/x-mixed-replace directly, so a web page or an image widget that behaves like a browser shows the live stream with no conversion. Mind the client count: each viewer opens its own stream and cameras limit concurrent sessions, so with several dashboards open the streams start to flicker or refuse. Media player components remain the wrong target for the multipart body.
Why does the JPEG I saved from the stream look corrupt or full of text?
You saved a multipart body rather than a picture. The boundary line and per-part headers sit ahead of each FF D8 marker, so decoders that expect a bare JPEG reject the file or complain about extra bytes before the image data. Cut the file at the FF D8 marker, or better, fetch the camera's snapshot endpoint, which returns a clean single JPEG every time.

