System designHow-to

Capture a still image from a laptop webcam in an HMI client

Grab a one-click still image from a laptop webcam inside a desktop HMI client: getUserMedia preview, canvas capture, JPEG payload math, and rollout checks.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01Why the direct script path is closed
  2. 02What you need
  3. 03Build the capture page in five steps
  4. 04Prove the capture works
  5. 05Timing and payload for a 1080p webcam still at 30 fps
  6. 06Move the picture into the project
  7. 07Three integration paths compared
  8. 08When the photos are really inspection data
  9. 09Common mistakes
  10. 10Questions

Operators photograph equipment with the laptop's webcam, save the file, then hunt for it and upload through a file browser — three slow steps per photo. The job needs a popup in the HMI client instead: live camera preview, one button, and the picture delivered to the project with the asset ID already attached. A desktop client's script API has no camera handle, so the direct route is closed. The client's web browser component is the way in: it embeds a real browser engine, and the web's camera API works inside it. This guide gives you the five-step build, the test that proves the capture, the timing and payload math for a 1080p webcam at 30 fps, and the checks to run before rollout.

Why the direct script path is closed

The client runtime executes scripts in a sandbox with a fixed function set: tags, windows, SQL, file dialogs. A camera is hardware access, and it is not in that set. The workaround some projects take is a custom client module that loads a native capture library into the runtime — workable, but the module is signed and rebuilt for every client release, and it only pays off when no browser engine is available.

The lighter door is the web browser component. It embeds a full browser engine inside the client window, and that engine ships the standard camera API, getUserMedia. A page of about 30 lines requests the camera, shows the live preview and captures a still on click. The same door is what makes browser camera access work without any vendor module: see browser camera access for HMI barcode scanning.

What you need

  • A laptop with its built-in webcam, or any USB webcam the operating system exposes as a standard video device.
  • A desktop client with a web browser component, placed in the capture popup window.
  • A home for the capture page, served over https or from the client machine's localhost: the camera API exists only in a secure context, and over plain http the browser hides it entirely.
  • A destination for the picture: a file share, a database table with a binary column, or the client's file-writing functions.
  • About 30 lines of HTML and JavaScript.

Build the capture page in five steps

  1. Serve the page and embed it. Host capture.html on the plant web server over https, or on the client machine's localhost, and point the browser component at that URL inside the popup.

  2. Request the camera once. A single getUserMedia call with video constraints of width 1920 and height 1080 opens the device behind one permission prompt. The two failures worth handling are NotAllowedError, when the operator declines, and NotReadableError, when another application already holds the camera.

  3. Preview the stream. Assign the returned MediaStream to the video element's srcObject and let autoplay start the feed.

  4. Capture on click. Size the canvas from the live track, draw the current frame, and export the JPEG with the canvas's documented toDataURL method:

// Documented web APIs here: getUserMedia, drawImage, toDataURL.
// camPreview, camStill, btnCapture are this page's own element IDs;
// openCamera / captureStill / closeCamera are this page's own functions.
let stream = null;                                        // MediaStream, null until granted
const videoEl  = document.getElementById('camPreview');   // <video autoplay playsinline muted>
const canvasEl = document.getElementById('camStill');     // <canvas hidden>

async function openCamera() {
  stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 1920, height: 1080 }
  });
  videoEl.srcObject = stream;                             // live preview starts here
}

function captureStill() {                                 // wired to btnCapture
  canvasEl.width  = videoEl.videoWidth;                   // native track width, e.g. 1920
  canvasEl.height = videoEl.videoHeight;                  // native track height, e.g. 1080
  canvasEl.getContext('2d').drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
  const jpegUrl = canvasEl.toDataURL('image/jpeg', 0.9);  // data:image/jpeg;base64,...
  sendToClient(jpegUrl);                                  // your bridge into the HMI client
}

function closeCamera() {                                  // call on popup close
  stream.getTracks().forEach(track => track.stop());      // releases the device
}
  1. Hand the string back and release the camera. Send jpegUrl to the client through the browser component's script bridge, or post it to a web API the server exposes — the same trigger-from-UI pattern as software-triggered image capture from a GUI button. On popup close, stop every track: the LED goes out and the device is free.

Prove the capture works

Grant the prompt, then check three things.

  1. The preview runs and the camera LED is on.
  2. Click capture and test the string: jpegUrl.startsWith('data:image/jpeg;base64,/9j/') must return true. The four characters /9j/ are base64 for the JPEG start marker 0xFF 0xD8, so the payload is a real JPEG and not a blank canvas.
  3. Time the handler with performance.now() on both sides: the draw and encode typically finish within 10 to 50 ms at 1080p, and the saved frame is at most one frame old.

Decode the string once on the receiving side, write the bytes to a file and open it in an image viewer: 1920×1080, sharp at the center, the operator's hands where you expect them.

Timing and payload for a 1080p webcam still at 30 fps

Size storage before you build. The inputs: 30 fps, the preview rate a browser typically grants a laptop webcam; 1920×1080, the native mode requested in step 2; quality 0.9, the parameter set in the capture call; and base64's exact ratio of 4 characters out per 3 bytes in.

frame time (ms) = 1000 / frame rate (fps) payload (KB) = JPEG size (KB) × 4 / 3
  • Frame time: 1000 / 30 = 33.3 ms. drawImage copies the frame the video element already holds, so the click adds no waiting — the still is at most 33 ms behind the button press.
  • Payload: a busy industrial scene at 1920×1080 and quality 0.9 encodes to roughly 400 KB of JPEG, typical range 300 to 600 KB, plain backgrounds lower. The data URL is 400 × 4 / 3 ≈ 533 KB of text.
  • Downscale check: 1280×720 is 921,600 pixels against 2,073,600 at full 1080p, a factor of 2.25. The same scene at 720p lands near 533 / 2.25 ≈ 237 KB of text.

Decision: budget the string at about 1 MB, which by the 4 / 3 ratio is a 750 KB JPEG (750 × 4 / 3 = 1,000 KB). The 400 KB source above sits at 533 KB of text, inside the budget. When a detailed scene pushes past it, set the canvas to 1280×720 before the draw: the string drops to roughly 237 KB and stays sharp enough for documentation.

Move the picture into the project

The data URL is text, and text is the wrong storage format. Strip everything before the comma, decode the base64, and you hold the raw JPEG: the 533 KB string becomes the 400 KB file. Write it to the asset's folder with the capture timestamp and asset ID in the file name, or insert it into a binary column beside the work-order record. Take the timestamp at capture time, not write time — the popup may sit open for minutes. What an HMI window can receive and actually show live is its own subject; what displays live when a smart camera image lands in a PLC HMI walks that path.

Three integration paths compared

Path What it takes Where it wins Watch for
Browser component + getUserMedia one HTML page over https or localhost no custom module, native resolution, one permission prompt the string bridge back to the client
Custom client module + native capture library a signed module loaded into the runtime works when the client has no browser engine rebuild and redeploy on every client release
Web or mobile client, file input with the capture attribute one HTML attribute on an upload field the phone's own camera app handles focus and exposure the image arrives through the upload flow, not a script string

Choose the browser path for any desktop client with a browser component — least code, nothing to re-sign. The module path is for clients with no browser engine at all. For technicians on phones or tablets, the capture attribute is the right door, because the mobile camera app does the capture work for you.

When the photos are really inspection data

A webcam still documents condition: coupling wear, a missing guard, a faded label. It does not produce repeatable inspection evidence — the camera moves, exposure adapts itself and no light is controlled. When the popup's job drifts toward defect detection, build a vision station and keep the one-button idea.

An on-device smart camera runs the check at the part and hands the HMI a verdict. The SC5000X class does this over GigE, at 6 MP mono with an 8 mm M12-mount lens:

Smart cameras & vision sensorsHikrobot MV-SC5060XM-08M-WBN SC5000X series Smart Camera, 6 MP, Mono, 8 mm focal length, M12-mountMV-SC5060XM-08M-WBN
Interface
GigE
Resolution
6-10 MP
Sensor format
1/2.4"
Shutter
Rolling
$1,629.00View part

Parts that need controlled light inside the camera — polarization against glare, transmitted light for clear parts — are what this model is built for, at 1.6 MP and 60 fps with a 16 mm lens:

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

Where several lines feed one archive, a vision controller collects streams and results in one place; this unit pairs an Intel N-series processor with 8 GB memory and 128 GB SSD plus 1 TB HDD of local storage:

Vision controllersHikrobot MV-VC2140-128G40-1T VC2100 Vision Controller, N97, 8GB+128G SSD+ 1THDDMV-VC2140-128G40-1T
LAN ports
3-4 LAN ports
USB 3 ports
2 USB 3 ports
Light outputs
4 built-in channels
Memory
8 GB
$1,679.00View partBuild a system with it

A fixed plant camera changes the capture call, not the pattern: a one-button live view from an IP camera snapshot URL and a vision sensor browser display for the local operator cover the two halves. The camera finder filters the smart cameras by resolution, interface and shutter.

Common mistakes

  • Capturing before the stream is live: the canvas holds a black frame. Gate the button on the video element's loadedmetadata event.
  • Leaving the canvas at its default 300×150: drawImage fills only that region and the image comes out cropped. Set width and height from videoWidth and videoHeight on every capture.
  • Quality forced to 1.0: strings run to multiple megabytes for no visible gain. 0.9 keeps documentation detail near the sizes in the worked example.
  • Skipping track stop on popup close: the LED stays on and other applications see the camera as busy.
  • Mirrored expectations: the front camera preview is mirrored on screen, but the canvas saves the unmirrored frame. If operators expect the mirror, apply a ctx.scale(-1, 1) transform before the draw.
  • Storing the base64 text instead of decoded bytes: rows grow by a third for no benefit.

Build the page, wire the bridge, and the popup replaces the file-browser ritual: preview, click, and the photo sits in the project within one frame time. Hold the 1 MB budget from the worked example, run the three checks, and the browser component stays ready for whatever you point it at next. More client-side camera work is collected on the systems topic page; for inspection-grade capture, start from the camera finder above or contact the applications team with the part and the defect.

Questions

Why is the camera API undefined inside my HMI client's browser component?

Because the page is not in a secure context. The camera API exists only on pages served over https or from localhost; over plain http the browser hides it instead of failing at call time. Move the capture page to an https origin on the plant web server or serve it from the client machine's localhost, and the API appears with no code change.

What resolution does the captured still actually have?

Whatever the browser granted the video track, read from videoWidth and videoHeight once the stream is live. Request 1920×1080 in the getUserMedia constraints; if the camera cannot deliver it, the browser silently falls back to its next mode, commonly 1280×720, and the canvas follows automatically because its size is set from the track at capture time.

How large is the capture string, and can a string tag hold it?

Base64 stores 4 characters for every 3 bytes, exactly. A 1080p JPEG at quality 0.9 typically encodes to 300 to 600 KB, so expect roughly 400 to 800 KB of text. Strip the prefix and decode to binary before storing: the 25% overhead disappears and the database holds real JPEG bytes in a binary column.

Does this work with an external USB webcam instead of the built-in camera?

Yes. Any USB webcam the operating system exposes as a standard video device works, because the browser sees the same capture interface either way. The browser picks the system default; to pin a specific unit on a docking station with several cameras, list the devices with enumerateDevices and pass the chosen deviceId in the getUserMedia constraints.

Can I skip the browser component and capture directly from client script?

Only with a custom client module that loads a native capture library into the runtime and calls the camera from module scope. That avoids the browser engine but adds a signed module to rebuild and redeploy for every client release, so it earns its keep only when the client has no browser component to embed the capture page in.