System designHow-to

Write MP4 video from a camera on an embedded board

Why MP4 writing fails on an embedded board with no hardware H.264 encoder, and how to record MJPG video on the board, then transcode it to MP4 H.264 on a PC.

LumaSpec Vision11 min read Share on LinkedIn

On this page 8 sections
  1. 01Why MP4 fails when AVI works
  2. 02Decide first: encode on the board or transcode on a PC
  3. 03Record MJPG on the board
  4. 04Worked example: sizing 30 minutes of recording at 120 fps
  5. 05Transcode to MP4 H.264 on the PC
  6. 06When the usual answer is wrong
    1. Your records must be frame-exact
    2. You need the full bit depth
    3. The MP4 must stream off the board live
    4. The board records around the clock
  7. 07Checks before you trust the recording
  8. 08Questions

Your embedded board writes AVI and MKV without complaint, but the MP4 attempt fails or lands an empty file, and the AVI files eat your flash. The cause is rarely your Python code. An MP4 file needs an H.264, H.265 or MPEG-4 encoder, and many low-cost embedded SoCs ship with no hardware video encoder at all, so OpenCV has nothing to encode with. If you record camera video on an embedded board, this guide shows you how to confirm the cause, how to choose between encoding on the board in software and recording a light MJPG interim file for transcoding on a PC, and how to size the storage for each path. The arithmetic is worked end to end for one real case: a 3.1 MP camera recording 120 fps into a 30-minute take.

Why MP4 fails when AVI works

MP4 is a container, not a codec. What the writer must actually produce is set by the fourcc: "mp4v" asks for MPEG-4 Part 2, "avc1" or "X264" ask for H.264. OpenCV hands the request to its video backend, FFMPEG or GStreamer. When that backend has no encoder for the tag and container pair, it says so at open time — a message naming the tag, for example MP4V rejected with codec id 13 in the mp4 format — yet writer.isOpened() can still return True, and every frame you pass to write() is dropped. The released file has no playable track.

AVI with MJPG works on nearly every build because Motion JPEG is a low-cost encoder: each frame compresses as an independent JPEG, with no prediction between frames. That low cost is also the size penalty. At recording quality, MJPG lands roughly between 10:1 and 20:1 against 8-bit raw video, while H.264 at the same visual quality is typically another 6 to 10 times smaller again.

Silicon decides the rest. Vendors list a video hardware accelerator row in their SoC comparison tables; on encoder-less parts that row is empty and the vendor's hardware-codec release notes do not apply to that board, so encoding can only happen in software. It is still possible. On a board whose OpenCV build uses the GStreamer backend, installing the plugin package that carries the x264 encoder — gstreamer1.0-plugins-ugly-meta supplied it in one such case — was enough for the same writer code to produce MP4. The catch is that the encoder then runs on the CPU, which caps the sustainable frame rate; that cap is why the transcode-later path below is the default recommendation.

Which codecs your build can reach is a build-and-silicon question, not an OpenCV version number. The same split on the decode side is covered in GStreamer hardware decoder availability on x86, and the desktop side of this same writer API is in recording video from a machine vision camera in Python.

Decide first: encode on the board or transcode on a PC

One question decides the architecture: must the MP4 exist on the board, in real time? If yes, encode in software on the board and verify the frame-rate cap by measurement. If the MP4 is for review, archive or training clips, record MJPG into an AVI or MKV container on the board and transcode once on a PC. The interim path spends storage instead of CPU, keeps frames in capture order, and still ends at the same MP4.

Path Board CPU load 30 min at 3.1 MP, 120 fps Frame order Choose when
Software H.264 straight to MP4 High about 6.8 GB Encoder may reorder frames The MP4 must exist on the board immediately
MJPG interim, transcode on a PC Low about 56 GB interim, then 6.8 GB One JPEG per frame Review, archive, clips: the usual choice
Raw 8-bit frames None about 670 GB Exact Seconds-long grabs for algorithm work

The measurement that settles it: frames written divided by elapsed wall-clock seconds. If that achieved rate sits below your acquisition rate, the encoder is the bottleneck — cut resolution, cut fps, or move to the interim path. Keep at least 20 percent headroom on CPU and on storage write speed.

Record MJPG on the board

  1. Check what your build carries. cv2.getBuildInformation() lists the video backends compiled into your OpenCV, typically FFMPEG and sometimes GStreamer — this tells you which plugin path can even reach an encoder.
  2. Choose the interim container and codec. MJPG in an AVI file is the most portable pair and ships in every OpenCV build; MKV also works. Create the fourcc with cv2.VideoWriter_fourcc(*"MJPG").
  3. Open the writer with your exact frame size and acquisition rate. If the rate you pass differs from the rate you feed, every player will run the file at the wrong speed:
import cv2

# Illustrative values: adjust path, fps and frame size to your capture
path: str = "take01.avi"                        # interim file on board storage
fps: float = 120.0                              # match the acquisition rate exactly
size: tuple = (2048, 1536)                      # (width, height) in pixels
fourcc: int = cv2.VideoWriter_fourcc(*"MJPG")   # Motion JPEG: present in every build

writer = cv2.VideoWriter(path, fourcc, fps, size)
opened: bool = writer.isOpened()
assert opened, "no encoder for this fourcc and container - switch to MJPG in AVI"
  1. Feed BGR frames from your grab loop, then release the writer:
frames_written: int = 0
while grabbing:                                 # your acquisition loop, free-run or triggered
    ok, frame = cap.read()                      # cap: your cv2.VideoCapture or SDK grabber
    if not ok:
        break
    writer.write(frame)                         # frame: uint8 BGR, shape (1536, 2048, 3)
    frames_written += 1

writer.release()                                # finalizes the file - never skip this
  1. Confirm the count. frames_written is your ground truth for verifying both the interim file and the later transcode. If the grab loop outruns the writer, frames pile up in memory — bound the queue and reuse buffers as in the Python frame queue memory leak fix.

Worked example: sizing 30 minutes of recording at 120 fps

The capture camera for this case:

Board-level cameras3.1 MP USB3 board-level camera, IMX252, 1/1.8" global shutter, 120 fps (LS-BCM-0003)LS-BCM-0003
Sensor format
1/1.8"
Lens mount
C-mount
Interface
USB3
Resolution
5 MP
$690.00View partBuild a system with it

Inputs, each with its source:

  • 3.1 MP per frame and 120 fps — the camera data sheet above.
  • 8 bits per pixel, mono — declared planning assumption; 8-bit mono is the usual inspection recording format.
  • 1,800 s — a 30-minute take.
  • 12:1 MJPG compression — planning value; per-frame JPEG at recording quality lands between 10:1 and 20:1 on industrial scenes.
  • 0.08 bits per pixel per frame for H.264 — planning value for moderate motion; x264 at a mid CRF typically lands between 0.05 and 0.1.

The rule in words: on-board write rate is bytes per frame times frames per second, where bytes per frame is pixels times bytes per pixel divided by the compression ratio. The finished MP4 size is the H.264 bitrate times duration over 8.

raw rate (B/s) = pixels × bytes per pixel × fps MJPG rate (B/s) = raw rate / compression ratio MP4 bits/s = pixels × bits per pixel × fps MP4 size (B) = MP4 bits/s × seconds / 8

Arithmetic:

  • Raw: 3,100,000 px × 1 byte = 3.1 MB per frame. At 120 fps: 3.1 × 120 = 372 MB/s. Over 1,800 s: 372 × 1800 = 669,600 MB ≈ 670 GB — not a recording path.
  • MJPG interim: 372 ÷ 12 = 31 MB/s. Over 1,800 s: 31 × 1800 = 55,800 MB ≈ 56 GB.
  • Card class: a V30 card guarantees 30 MB/s sustained write, so 31 MB/s needs a V60 card (60 MB/s guaranteed). Halve the rate to 60 fps and the load is 31 ÷ 2 = 15.5 MB/s, which V30 carries with about twice the margin.
  • MP4 after transcode: 3,100,000 × 0.08 = 248,000 bits per frame; × 120 fps = 29.76 Mbps ≈ 30 Mbps. 30 ÷ 8 = 3.75 MB/s; × 1800 s = 6,750 MB ≈ 6.8 GB. The MP4 is 56 ÷ 6.8 ≈ 8 times smaller than the interim file.

Decision: at 120 fps, record the interim file onto a V60 card and budget about 56 GB of board storage per 30-minute take; at 60 fps a V30 card fits and the interim halves to 28 GB. Expect about 6.8 GB of MP4 per take after transcoding. Raw needs 372 MB/s, which no removable card class carries, so keep it for seconds-long grabs. Both ratios move with the scene: detailed, fast-moving content pushes MJPG toward 10:1 and H.264 past 0.1 bits per pixel; static scenes land the other way.

Every number scales linearly with frame rate and pixel count. Halve the fps and every result halves with it. A 6.3 MP camera at 18 fps writes 6.3 × 18 = 113.4 MB/s raw, or 113.4 ÷ 12 = 9.45 MB/s as MJPG — 17 GB per 30 minutes, which a V30 card carries with more than three times the margin. The camera in that lower-rate case:

Board-level cameras6.3 MP board-level camera, IMX178, 1/1.8" rolling shutter, 18 fps (LS-BCM-0020)LS-BCM-0020
Sensor format
1/1.8"
Lens mount
CS-mount
Resolution
6-10 MP
Shutter
Rolling
$290.00View partBuild a system with it

Resolution moves the same lever: area scan cameras at 12 MP multiply the per-frame bytes by roughly four against this case, which is why rate selection matters even more at high resolution.

Transcode to MP4 H.264 on the PC

One FFmpeg line does the conversion:

ffmpeg -i take01.avi -c:v libx264 -crf 20 -preset veryfast -pix_fmt yuv420p take01.mp4

libx264 is the software H.264 encoder. -crf 20 sets quality — lower is better and larger, and 18 to 23 covers most review work. -preset veryfast trades a little compression efficiency for speed; at this bitrate a desktop CPU typically transcodes several times faster than the recording ran. -pix_fmt yuv420p keeps 8-bit 4:2:0 chroma, which every mainstream player decodes. Run the transcode on the review workstation, or on an industrial mini PC mounted near the cell when files must stay on-site.

Verify the file by reading it back and comparing counts:

import cv2

cap = cv2.VideoCapture("take01.mp4")     # str: path to the transcoded file
decoded: int = 0
while True:
    ok, _frame = cap.read()
    if not ok:
        break
    decoded += 1

print(decoded == frames_written)         # bool: must be True before you trust the file

If fps metadata reads as zero in a player or in OpenCV, video FPS returns zero in OpenCV shows how to recover the true rate from timestamps. One playback trap from other sources: VideoCapture can report half the expected frames on interlaced MP4 files. Machine vision cameras are progressive, so when a foreign file reads short, that is the usual suspect — the OpenCV VideoCapture half-frame fix explains when it happens.

When the usual answer is wrong

Your records must be frame-exact

H.264 reorders frames and compresses each to a different size, so frame N in the MP4 is not guaranteed to be exposure N. Keep MJPG — one JPEG per frame in capture order — and tie frames to events by count. The hardware-trigger grab loop in Python pairs every frame with its trigger, which survives transcoding better than container metadata.

You need the full bit depth

MJPG and H.264 are 8-bit paths. When the decision lives in the top bits — a weld pool near saturation, glass in a dark field — record raw frames instead; recording a 12-bit grayscale video file keeps the depth.

The MP4 must stream off the board live

Software encoding caps you at some fps below what the acquisition could do. The durable fix is hardware — an SoC with a video encoder block, or encoding at the camera side — not more plugin tuning. Where the task needs only a small field at a modest rate, a 1.6 MP GigE board camera halves the worked example's pixel count and brings software encoding back within reach.

The board records around the clock

Continuous writing wears flash. Storing camera images on embedded vision board flash in C covers wear-aware storage layouts that apply to video files just as much as to stills.

Checks before you trust the recording

  • isOpened() returned True and no tag warning appeared at open — the missing encoder, not the file path, is the usual silent failure.
  • frames_written equals your trigger or pulse count — one integer ties the file to the process.
  • The released file plays in a normal player, not only in OpenCV — this catches container and pixel-format trouble.
  • Duration matches frames ÷ fps — this catches a writer opened at the wrong rate or frame size.
  • Sustained write rate sits at least 20 percent under the card class guarantee — 31 MB/s is already past V30's 30 MB/s.
  • The MP4 decode count equals frames_written — this proves the transcode kept every frame.

Pick the camera that matches the rate you actually need on the board-level cameras page, filter the catalog by rate and interface in the camera finder, or work through configuring a camera step by step. If you are unsure what your task needs, tell us the frame size, rate and take length through the contact page and we will size the storage with you. More acquisition and recording guides sit on the systems topic page.

Questions

Why does writer.isOpened() return True but the MP4 has no video?

OpenCV opens the container before it can know whether encoding will succeed. Without a matching encoder, each write() call is dropped silently and release() finalizes an empty track. Watch stderr at open time: a message naming the fourcc tag and the container is the tell. Recording MJPG into AVI sidesteps the missing encoder entirely, and the transcode step then produces the MP4 on a machine that has one.

Can I use the mp4v fourcc to write MP4 instead of H.264?

mp4v is MPEG-4 Part 2 inside an MP4 container. It encodes in software on any build that carries the encoder, but at the same visual quality it writes files several times larger than H.264, and some players and editors reject it. If the MP4 is for playback or archive, record MJPG on the board and transcode to H.264 once on a PC instead.

Which fourcc should I use for the on-board recording?

MJPG in an AVI container. Every OpenCV build ships the MJPEG encoder, it costs little CPU because each frame compresses independently, and it keeps frames in capture order. If a particular build refuses MJPG in AVI, XVID in AVI is the usual fallback, but it adds inter-frame compression that breaks frame-exact indexing. Reserve MKV for when you need extra metadata tracks alongside the video.

How much smaller is the transcoded MP4 than the on-board file?

In the 30-minute, 120 fps, 3.1 MP case: about 56 GB of MJPG interim becomes about 6.8 GB of H.264, roughly 8 times smaller. The ratio moves with content: static machine vision scenes compress further, detailed or fast motion compresses less. You can trade size for quality at transcode time by moving the CRF value a few points up or down.

Which SD card speed class does my recording rate need?

Compute the sustained write rate first: megapixels × fps ÷ 12 gives MB/s for MJPG at recording quality. A V30 card guarantees 30 MB/s sustained write and a V60 card 60 MB/s, so 3.1 MP at 60 fps needs about 15.5 MB/s and fits V30, while the same camera at 120 fps needs 31 MB/s and requires V60. Leave about 20 percent headroom below the guarantee.