Skip to content

Recording & Transcription (Advanced)

Setting up recording and AI transcription requires additional services beyond the base Meet deployment.

Architecture overview

Meet Backend ──► LiveKit Egress ──► Garage/S3
      ▲                                  │
      │         (webhook)                │
      └──────────────────────────────────┘
                                Summary Service
                                   │         │
                             Transcribe   Summarize
                              (Whisper)    (LLM API)

Part 1: Recording with LiveKit Egress

What is Egress?

LiveKit Egress is a LiveKit component that records rooms to files. It:

  • Captures the full room composite (all video/audio in the current layout)
  • Saves the output to S3-compatible storage
  • Notifies the Meet backend via an S3 webhook when the file is ready

Step 1: Configure Garage

Garage is an open-source, S3-compatible distributed object store designed for self-hosted deployments. It runs on minimal hardware (1GB RAM) and stores data across multiple zones for resilience.

Add Garage to your compose.yml:

garage:
  image: docker.io/deuxfleurs/garage:1.9.0
  command: server
  volumes:
    - ./data/garage:/data
  ports:
    - "3900:3900"
    - "3901:3901"
  environment:
    - RUST_LOG=warn
  healthcheck:
    test: ["CMD", "garage", "status"]
    interval: 10s
    timeout: 5s
    retries: 5

Create a garage.toml configuration file:

# garage.toml
meta_dir = "/var/lib/garage/meta"
data_dir = "/var/lib/garage/data"

[s3api]
api_key = "meet-access-key"
api_secret = "your-very-long-random-secret-here"

# Expose the S3 API on port 3900
[s3api.local]
bind_addr = "0.0.0.0:3900"

# Admin console on port 3901
[admin]
bind_addr = "0.0.0.0:3901"

Mount the config into the container:

garage:
  image: docker.io/deuxfleurs/garage:1.9.0
  command: server
  volumes:
    - ./data/garage:/data
    - ./garage.toml:/etc/garage/garage.toml:ro
  ports:
    - "3900:3900"
    - "3901:3901"

Initialize Garage and create the bucket:

# Initialize a single-node cluster
docker compose exec garage garage node id
docker compose exec garage garage node connect --no-s3-proxy

# Create the storage bucket
docker compose exec garage garage bucket create meet-media-storage

# Create an access key for the bucket
docker compose exec garage garage key create meet-access-key

# Grant the access key permission to the bucket
docker compose exec garage garage bucket allow meet-media-storage --key meet-access-key --read --write

Step 2: Configure the recording notification

The Meet backend needs to be notified when a recording file is uploaded. Configure Garage to send notifications when objects are created in the meet-media-storage bucket:

# Create an SQS queue for recording notifications
docker compose exec garage garage queue create meet-webhook

# Allow the bucket to send notifications to the queue
docker compose exec garage garage bucket notification allow meet-media-storage --queue meet-webhook --events put --filter "recordings/*"

Set the notification endpoint and credentials in Meet's environment:

STORAGE_WEBHOOK_AUTH_TOKEN=your-webhook-secret

The webhook endpoint on the Meet backend (POST /api/v1.0/recordings/storage-hook/) must be reachable from inside Docker. If running behind nginx-proxy, ensure the backend container is on the same Docker network.

Step 3: Set up LiveKit Egress

Egress requires its own configuration file. Create livekit-egress.yaml:

api_key: myapikey
api_secret: your-livekit-api-secret
ws_url: ws://livekit:7880

s3:
  access_key: meet-access-key
  secret: your-very-long-random-secret-here
  endpoint: http://garage:3900
  bucket: meet-media-storage
  region: us-east-1  # required for S3-compatible storage
  force_path_style: true  # required for Garage

Add to your compose.yml:

livekit-egress:
  image: livekit/egress:v1.11.0
  environment:
    EGRESS_CONFIG_FILE: /livekit-egress.yaml
  volumes:
    - ./livekit-egress.yaml:/livekit-egress.yaml
  depends_on:
    redis:
      condition: service_healthy
    garage:
      condition: service_healthy

Step 4: Configure Meet backend for recording

AWS_S3_ENDPOINT_URL=http://garage:3900
AWS_S3_ACCESS_KEY_ID=meet-access-key
AWS_S3_SECRET_ACCESS_KEY=your-very-long-random-secret-here
AWS_STORAGE_BUCKET_NAME=meet-media-storage
AWS_S3_SECURE_ACCESS=False

LIVEKIT_API_KEY=myapikey
LIVEKIT_API_SECRET=your-livekit-api-secret
LIVEKIT_API_URL=http://livekit:7880

Configurable recording encoding

As of v1.15.0 (unreleased), recording encoding is configurable:

LIVEKIT_EGRESS_ENCODING_VIDEO_CODEC=H264  # or VP8, VP9
LIVEKIT_EGRESS_ENCODING_AUDIO_CODEC=OPUS

Part 2: AI Transcription & Summarization

Transcription requires the Summary service — a separate FastAPI application with Celery workers.

Summary service architecture

Summary Service (FastAPI)
├── transcribe-queue worker → Whisper STT
└── summarize-queue worker  → LLM API

Step 1: Configure the summary service

The summary service is in src/summary/ in the repository. Create its environment file env.d/development/summary:

# Redis
REDIS_URL=redis://redis-summary:6379/0

# Garage/S3 (to download recordings)
AWS_S3_ENDPOINT_URL=http://garage:3900
AWS_S3_ACCESS_KEY_ID=meet-access-key
AWS_S3_SECRET_ACCESS_KEY=your-very-long-random-secret-here
AWS_STORAGE_BUCKET_NAME=meet-media-storage
AWS_S3_SECURE_ACCESS=False

# Meet backend (to report results)
MEET_API_URL=http://app:8000
MEET_API_TOKEN=internal-api-token

# STT configuration
STT_BACKEND=whisper  # or 'kyutai' for Kyutai's Moshi
WHISPER_MODEL=large-v3

# LLM for summarization (optional)
LLM_API_URL=https://api.openai.com/v1
LLM_API_KEY=sk-...
LLM_MODEL=gpt-4o

Step 2: Add to compose.yml

redis-summary:
  image: redis

app-summary:
  build:
    context: src/summary
  env_file:
    - env.d/development/summary
  ports:
    - "8001:8000"
  depends_on:
    - redis-summary

celery-summary-transcribe:
  build:
    context: src/summary
  command: celery -A summary.core.celery_worker worker --pool=solo -Q transcribe-queue
  env_file:
    - env.d/development/summary
  depends_on:
    - redis-summary
    - garage

celery-summary-summarize:
  build:
    context: src/summary
  command: celery -A summary.core.celery_worker worker --pool=solo -Q summarize-queue
  env_file:
    - env.d/development/summary
  depends_on:
    - redis-summary

Step 3: Connect Meet backend to Summary service

SUMMARY_SERVICE_URL=http://app-summary:8000
SUMMARY_SERVICE_TOKEN=internal-api-token

Step 4: Configure STT backend

The summary service supports multiple STT backends:

Backend Notes
whisper OpenAI Whisper (local). Requires significant GPU/CPU.
kyutai Kyutai's Moshi (real-time).
External API Any OpenAI-compatible STT API endpoint.

For production, use a GPU-enabled worker or an external STT API for reasonable transcription speed.

Multiple transcription workers

As of v1.15.0+ (unreleased), multiple transcription workers are supported for parallel processing:

# Helm values
summary:
  transcribeWorkers: 3
  transcribeEndpoints:
    - http://whisper-1:8000
    - http://whisper-2:8000
    - http://whisper-3:8000

Supported file formats

The summary service accepts: .mp4, .webm, .wav, .mp3, .ogg, and additional formats added in v1.14.0+.


Testing the full recording flow

  1. Start a meeting and click Record
  2. Speak for 30 seconds, then click Stop recording
  3. Check Garage for the recording file: docker compose exec garage garage object list meet-media-storage
  4. Check Meet backend logs for the webhook: docker compose logs app | grep storage-hook
  5. The room owner should receive an email with a download link
  6. If transcription is configured, check docker compose logs celery-summary-transcribe

Alternative S3 providers

Garage is recommended for self-hosted single-server deployments. Other S3-compatible providers work with Meet as well:

  • AWS S3 — production-scale, managed
  • OVH Object Storage — European hosting
  • Scaleway Object Storage — French hosting
  • Ceph RGW — enterprise-grade, self-hosted

All use the same environment variables (AWS_S3_ENDPOINT_URL, AWS_S3_ACCESS_KEY_ID, etc.) and are configured interchangeably.