Capture Failing UI Tests as Video with Pytest, Selenium, and FFmpeg

This guide shows how to automatically record videos of failed Selenium UI tests in a Pytest environment using a pure‑Python solution with FFmpeg, storing only the necessary clips, integrating them into Allure reports, and optimizing performance across Windows, macOS, and Linux.

Test Development Learning Exchange
Test Development Learning Exchange
Test Development Learning Exchange
Capture Failing UI Tests as Video with Pytest, Selenium, and FFmpeg

Why video recording matters for UI test flakiness

Intermittent UI test failures (missing elements, unresponsive clicks, rendering glitches) are difficult to diagnose with only screenshots and logs. Recording the test execution provides a continuous, replayable context that serves as a debugging tool.

Core objectives

Record video only when a test case fails, reducing storage usage.

Keep recording lightweight and non‑blocking.

Attach videos automatically to Allure reports.

Support Windows, macOS, and Linux.

Why FFmpeg?

FFmpeg is an industry‑standard audio‑video processing tool. It offers real‑time screen capture, hardware‑accelerated H.264 encoding, and low CPU overhead, enabling on‑the‑fly MP4 generation without large intermediate files.

Step 1 – Install dependencies

Install FFmpeg for your operating system and the required Python packages.

# Windows
# Download from https://www.gyan.dev/ffmpeg/builds/ and add the <em>bin</em> folder to PATH

# macOS
brew install ffmpeg

# Linux (Ubuntu)
sudo apt install ffmpeg

# Python packages
pip install selenium pytest allure-pytest opencv-python

Verify the installation with ffmpeg -version.

Step 2 – Implement the video recorder

# utils/video_recorder.py
import os
import subprocess
import threading
import time
from pathlib import Path

class VideoRecorder:
    def __init__(self, output_path: str, fps: int = 10):
        self.output_path = Path(output_path)
        self.fps = fps
        self.process = None
        self._stop_event = threading.Event()
        self._thread = None

    def start(self):
        """Start recording in a background thread."""
        if os.name == 'nt':  # Windows
            cmd = [
                'ffmpeg', '-f', 'gdigrab', '-framerate', str(self.fps),
                '-i', 'desktop', '-vcodec', 'libx264', '-preset', 'ultrafast',
                '-pix_fmt', 'yuv420p', '-y', str(self.output_path)
            ]
        else:  # macOS / Linux
            display = os.environ.get('DISPLAY', ':0.0')
            cmd = [
                'ffmpeg', '-f', 'x11grab', '-framerate', str(self.fps),
                '-s', '1920x1080', '-i', display,
                '-vcodec', 'libx264', '-preset', 'ultrafast',
                '-pix_fmt', 'yuv420p', '-y', str(self.output_path)
            ]
        startupinfo = None
        if os.name == 'nt':
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
        self.process = subprocess.Popen(
            cmd,
            stdin=subprocess.PIPE,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            startupinfo=startupinfo
        )
        self._stop_event.clear()
        self._thread = threading.Thread(target=self._monitor, daemon=True)
        self._thread.start()

    def stop(self):
        """Stop recording."""
        if self.process:
            self._stop_event.set()
            try:
                self.process.stdin.write(b'q')
                self.process.stdin.flush()
            except Exception:
                pass
            self.process.wait(timeout=5)
            if self.process.poll() is None:
                self.process.kill()
            self.process = None

    def _monitor(self):
        """Monitor recording status."""
        while not self._stop_event.is_set():
            time.sleep(0.1)

Key parameters: -preset ultrafast for speed, -pix_fmt yuv420p for browser compatibility, and a default fps of 10 to balance clarity and file size.

Step 3 – Hook into Pytest + Selenium

# conftest.py
import pytest, allure, os, tempfile
from utils.video_recorder import VideoRecorder
from selenium import webdriver

@pytest.fixture(scope="function")
def driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")  # remove for visual debugging
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()

@pytest.fixture(scope="function")
def video_recorder(request):
    """Video recorder fixture."""
    test_name = request.node.name
    video_path = os.path.join(tempfile.gettempdir(), f"{test_name}.mp4")
    recorder = VideoRecorder(video_path)
    recorder.start()
    yield recorder
    recorder.stop()
    if hasattr(request.node, 'rep_call') and request.node.rep_call.failed:
        if os.path.exists(video_path) and os.path.getsize(video_path) > 0:
            with open(video_path, "rb") as f:
                allure.attach(f.read(),
                             name="Test video",
                             attachment_type=allure.attachment_type.MP4)
# test_login.py
def test_login_failure(driver, video_recorder):
    """Intentionally fail to trigger video capture."""
    driver.get("https://example.com/login")
    driver.find_element("id", "username").send_keys("admin")
    driver.find_element("id", "password").send_keys("wrong_password")
    driver.find_element("id", "submit").click()
    assert "Welcome" in driver.page_source

Advanced optimizations

Record only the browser window

Adjust the FFmpeg command to include -offset_x, -offset_y, and -video_size based on the window rectangle obtained via pygetwindow. This reduces file size and protects privacy.

Dynamic quality settings

if os.getenv("CI"):
    fps = 5          # lower frame rate in CI
    preset = "superfast"
else:
    fps = 10
    preset = "ultrafast"

Automatic cleanup of old videos

import shutil
from datetime import datetime, timedelta
from pathlib import Path
import tempfile

def cleanup_old_videos():
    tmp_dir = Path(tempfile.gettempdir())
    for video in tmp_dir.glob("test_*.mp4"):
        if datetime.now() - datetime.fromtimestamp(video.stat().st_mtime) > timedelta(hours=24):
            video.unlink()

Common pitfalls and solutions

FFmpeg not found : Ensure the FFmpeg binary is in PATH or use an absolute path.

Empty video file : Verify that FFmpeg has permission to capture the screen; on macOS/Linux you may need to grant screen‑recording permission.

Stuttering recordings : Reduce fps to 5‑10 and use the ultrafast preset.

CI without GUI : Headless mode cannot capture the screen; consider generating a video from a sequence of screenshots instead.

Large files : Limit recording duration (e.g., 60 s) or transcode with higher compression.

Conclusion

Integrating video capture turns UI automation into a “black box” that records the exact moment of failure. The recorded MP4 can be attached to Allure reports, enabling developers to reproduce issues instantly and reducing the “works on my machine” debate.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PythonFFmpegUI testingseleniumpytestAllureVideo Recording
Test Development Learning Exchange
Written by

Test Development Learning Exchange

Test Development Learning Exchange

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.