skip to content

Why does cv2.VideoWriter produce an empty video file without ever raising an error?

level: middleimportance: must knowfreq 62%

answer

  1. write() has no return value
  2. geometry is fixed at construction
  3. width before height
  4. fourcc must match the container
  5. release() finalizes the file

basics

~20 s

VideoWriter.write() silently ignores any frame whose size or channel count differs from the frameSize and isColor passed to the constructor, and the writer never opens at all if the fourcc codec is unavailable. Check isOpened() and pass (width, height).

solid answer

~40 s

`cv2.VideoWriter(filename, fourcc, fps, frameSize, isColor=True)` fixes the encoder's geometry at construction time, and `write()` returns nothing — it has no way to complain. Three things go wrong silently. First, `frameSize` is `(width, height)`, the reverse of `frame.shape[:2]`, so a transposed tuple means every frame mismatches and nothing lands in the file. Second, a fourcc the local backend cannot encode, or one that does not fit the container implied by the extension (for example `mp4v` in a `.avi`, or `avc1` without an OpenH264 build), leaves the writer closed. Third, `fps` of 0 — which is exactly what `cap.get(cv2.CAP_PROP_FPS)` returns for many streams — gives a degenerate file. The fix is defensive: assert `writer.isOpened()` right after construction, resize or convert frames to the declared geometry before writing, and clamp fps to a sane default.

code

python · 24 lines
python
import cv2

cap = cv2.VideoCapture("input.mp4")
fps = cap.get(cv2.CAP_PROP_FPS)
if not fps or fps <= 0:
    fps = 30.0
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

fourcc = cv2.VideoWriter_fourcc(*"mp4v")
writer = cv2.VideoWriter("out.mp4", fourcc, fps, (w, h))
if not writer.isOpened():
    raise RuntimeError("no encoder for this fourcc/extension")

try:
    while True:
        ok, frame = cap.read()
        if not ok:
            break
        assert frame.shape[:2] == (h, w)
        writer.write(frame)
finally:
    cap.release()
    writer.release()

go deeper

for a junior

Know the constructor signature and that frameSize is (width, height) while NumPy shape is (height, width). Say out loud that you check writer.isOpened() before the loop and call release() after it.

for a middle

Be ready to walk the four silent failures — dead codec, transposed or mismatched size, wrong channel count, bogus fps — and explain why none of them raises, given that write() returns nothing.

for a senior

Show the defensive recording loop you would actually ship: fps clamped, geometry asserted, isOpened checked, release() in a finally, and a deliberate choice of a widely available fourcc rather than whatever the tutorial used.

for a principal

Own the delivery question: whether recording should even go through OpenCV. Discuss recording a reliable intermediate and transcoding with a real encoder for shipped artifacts, and the storage and retention cost of writing raw pipeline output at all.

## What VideoWriter is `cv2.VideoWriter` is OpenCV's encoding side of video I/O. It is constructed as `cv2.VideoWriter(filename, fourcc, fps, frameSize, isColor=True)` and then fed one frame at a time with `write(frame)`. Unlike `cv2.imwrite`, which returns a boolean, `write()` returns nothing at all. There is no error channel, so every failure mode of this class is a silent one: the script runs to completion, prints nothing, and leaves behind a zero-byte or header-only file. ## Failure one: the codec never opened The fourcc is a four-character code identifying the codec: `cv2.VideoWriter_fourcc(*"mp4v")` (the class-method spelling `cv2.VideoWriter.fourcc` is the same call). Whether that codec can actually be used depends on the backend your wheel was built with — typically FFmpeg — and on the container implied by the filename extension. `mp4v` in `.mp4` and `MJPG` in `.avi` are the two combinations that work almost everywhere. `avc1`/`H264` frequently does not, because H.264 encoding is not shipped in every build for licensing reasons. When the encoder cannot be created, the constructor still returns an object; only `writer.isOpened()` tells you the truth, and it tells you immediately, before a single frame has been read. That is why the check belongs on the line after construction. ## Failure two: the frame geometry does not match `frameSize` is `(width, height)`. A NumPy frame's `shape` is `(height, width, channels)`. Writing `cv2.VideoWriter(path, fourcc, fps, frame.shape[:2])` therefore hands the encoder a transposed size, and unless the video happens to be square, every subsequent `write()` is discarded. The same thing happens when the loop resizes or crops frames — a `cv2.resize` to 640x360 inside the loop while the writer was built from the capture's native 1920x1080 produces an empty file. The discipline is to decide the output geometry once, build the writer from it, and force every frame to it before writing. ## Failure three: channel count `isColor` defaults to True, meaning the encoder expects three-channel BGR frames. Recording a single-channel product of the pipeline — a foreground mask, a Canny edge map, a grayscale conversion — needs either `isColor=False` at construction or a `cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)` before each write. Mixing the two is a common mistake because the same loop that displays a mask fine with `imshow` writes nothing. ## Failure four: fps Many sources report a bogus frame rate. `cap.get(cv2.CAP_PROP_FPS)` returns 0 for a good number of live devices and for some containers with variable frame rate. Passing 0 straight into the writer yields a file players cannot time. Clamp it: read the property, and if it is not a positive finite number, substitute a measured or assumed rate. Note also that fps is metadata only — the writer does not pace anything. If your loop produces frames at 12 fps and you declare 30, the recording plays back 2.5x fast. ## Detecting the problem early A robust recording loop looks like this: read the source's width, height and fps once; sanity-check fps; construct the writer; assert `isOpened()`; inside the loop, assert or force `frame.shape[:2] == (h, w)`; on exit, call `writer.release()`. That last call matters more than it looks — the encoder flushes and finalizes the container in `release()`. A script killed with Ctrl-C or one that returns early without releasing can leave a file whose moov atom was never written, which most players refuse to open even though frames were encoded. Wrapping the loop in `try/finally` with `release()` in the `finally` is the standard remedy. ## A useful diagnostic order When someone hands you a broken recording, check in this order, because each check is cheaper than the next: is the file zero bytes and does `isOpened()` return False (codec problem)? Is the file a small constant size regardless of how many frames you write (geometry or channel mismatch)? Does it play but at the wrong speed (fps)? Does it play in VLC but not in a browser or QuickTime (container/codec compatibility rather than an OpenCV bug at all)? Only the first two are OpenCV's doing; the last is a delivery-format decision, and the usual answer is to record with `mp4v` or `MJPG` for reliability and transcode afterwards with a real encoder if the artifact has to be shipped.

  • The recording plays but everything moves at double speed — what did you get wrong?
    The fps declared to the writer is higher than the rate at which frames were actually produced. VideoWriter does not pace anything; fps is just metadata stamped into the container. If a processing loop emits 15 frames per second and the writer was told 30, playback is 2x. Either measure the real production rate and declare that, or drop/duplicate frames to hit the declared rate.
  • Why can a recording be unplayable even though isOpened() was True and every frame matched?
    Because `release()` never ran. The encoder flushes buffered frames and writes the container index in `release()`, so a process killed mid-loop, or one that returns early on an exception, leaves a file with no usable index. Put `writer.release()` in a `finally` block, and do the same for the capture.
  • How do you record a foreground mask from a background subtractor to video?
    Either construct the writer with `isColor=False`, which tells the encoder to expect single-channel input, or convert each mask with `cv2.cvtColor(mask, cv2.COLOR_GRAY2BGR)` before writing. Passing a one-channel array to a writer built with the default `isColor=True` writes nothing and reports nothing.

saying these in an interview costs you the question

  • Assuming write() returns a success flag you can check
  • Passing frame.shape[:2] as frameSize, so height and width are swapped
  • Believing OpenCV resizes frames to fit the writer automatically
  • Trusting cap.get(cv2.CAP_PROP_FPS) without checking for 0
  • Thinking any fourcc works with any file extension

context