Record a Zoom web meeting on a headless Ubuntu server by joining via Chrome in a container and capturing the video (Xvfb) + audio (PulseAudio null sink) to an MP4 with ffmpeg.
⚠️ Legal/ToS: Record only meetings you are authorized to record. Comply with Zoom's Terms of Service and local laws.
- Headless Chrome (via Selenium + undetected-chromedriver)
- Xvfb virtual display, PulseAudio null sink
- ffmpeg screen + system audio capture
- Dockerfile + docker-compose for reproducible deploys
- Configurable via
data/config.yaml - Open-source (MIT)
- Ensure Docker + Docker Compose are installed on your Ubuntu server.
- On your local machine, export your Zoom cookies (use a Chrome extension like "EditThisCookie") and save them to
data/cookies.json.
Edit data/config.yaml:
zoom_url: "https://zoom.us/wc/join/MEETING_ID?pwd=OPTIONAL_PASSWORD"
cookies_file: "/app/data/cookies.json"
output_file: "/app/data/meeting_recording.mp4"
headless: true
display: ":99"
resolution: "1280x720"
fps: 25
pulse_source: "ZoomSink.monitor"docker compose up --buildThe container will start Chrome, join the Zoom meeting, and begin recording to data/meeting_recording.mp4.
- Stop with
CTRL+Cordocker stop zoomrec. - Logs are printed to your console; ffmpeg runs as a child process.
- Use Zoom's web client join link format (
/wc/join/MEETING_ID); the app join flow is not supported. - If prompted with "Open Zoom Meetings?" the script tries clicking "Join from your browser" automatically.
- The script attempts to click "Join with Computer Audio"; if it misses, increase
timeouts.join_audio_button.
- Blank video / black screen: Ensure Xvfb is running (
docker logs zoomrec), and that resolution matchesffmpegargs. - No audio: Verify
pulse_sourceisZoomSink.monitor(default) and ffmpeg is capturing-f pulse -i ZoomSink.monitor. - Cookie issues: Make sure your cookies are for
.zoom.usand not expired. Re-export and replacedata/cookies.json.
- Running Chrome in a container uses flags (
--no-sandbox,--disable-dev-shm-usage) for stability. - We avoid host audio device passthrough by using a null sink in PulseAudio and recording its monitor.
- You can change output codec options in
utils/recorder.py.
MIT — see LICENSE.