# utils/video_recorder.pyimport osimport subprocessimport threadingimport timefrom pathlib import Pathclass 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): """启动录制(后台线程)""" 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) ] # 启动 FFmpeg 进程(不显示控制台窗口) 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) self._thread.daemon = True self._thread.start() def stop(self): """停止录制""" if self.process: self._stop_event.set() # 发送 'q' 命令给 FFmpeg 正常退出 try: self.process.stdin.write(b'q') self.process.stdin.flush() except: pass self.process.wait(timeout=5) if self.process.poll() is None: self.process.kill() self.process = None def _monitor(self): """监控录制状态""" while not self._stop_event.is_set(): time.sleep(0.1)⚠️ 关键参数说明:-preset ultrafast:牺牲压缩率换取速度-pix_fmt yuv420p:确保视频能在浏览器播放fps=10:平衡清晰度与文件大小(UI 操作无需高帧率)
1. 在 conftest.py 中管理录制生命周期# conftest.pyimport pytestimport allurefrom utils.video_recorder import VideoRecorderfrom selenium import webdriverimport osimport tempfile@pytest.fixture(scope="function")def driver(): # 初始化 WebDriver options = webdriver.ChromeOptions() options.add_argument("--headless=new") # 如需可视化可注释 driver = webdriver.Chrome(options=options) yield driver driver.quit()@pytest.fixture(scope="function")def video_recorder(request): """视频录制 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() # 仅当用例失败时附加到 Allure 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="操作视频", attachment_type=allure.attachment_type.MP4 ) # 清理临时文件(可选) # os.remove(video_path)# Hook: 捕获用例执行结果@pytest.hookimpl(tryfirst=True, hookwrapper=True)def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() setattr(item, f"rep_{rep.when}", rep)2. 在测试用例中使用# test_login.pydef test_login_failure(driver, video_recorder): """故意制造失败:输入错误密码""" 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