An open-source automation bot for joining and recording video meetings across multiple platforms including Google Meet, Microsoft Teams, and Zoom. Built with TypeScript, Node.js, and Playwright for reliable browser automation.
- Multi-Platform Support: Join meetings on Google Meet, Microsoft Teams, and Zoom
- Automated Recording: Capture meeting recordings with configurable duration limits
- Single Job Execution: Ensures only one meeting is processed at a time across the entire system
- Dual Integration Options: RESTful API endpoints and Redis message queue for flexible integration
- Asynchronous Processing: Redis queue support for high-throughput, scalable meeting requests
- Docker Support: Containerized deployment with Docker and Docker Compose
- Graceful Shutdown: Proper cleanup and resource management
- Prometheus Metrics: Built-in monitoring and metrics collection
- Stealth Mode: Advanced browser automation with anti-detection measures
- Completion Notifications: Optional webhook and Redis notifications when a recording is completed
- Node.js 20+
- Docker and Docker Compose (for containerized deployment)
- Git
-
Clone the repository
git clone https://github.com/screenappai/meeting-bot.git cd meeting-bot -
Install dependencies
npm install
-
Environment Setup
cp .env.example .env # Edit .env with your configuration -
Run with Docker (Recommended)
npm run dev
Or run locally:
npm start
The server will start on http://localhost:3000
Meeting Bot operates with a single job execution model to ensure reliable meeting processing:
- Single Job Processing: Meeting Bot accepts only one job at a time and works until it's completely finished before accepting another job
- Automatic Retry: The bot automatically retries on certain errors such as automation failures or when it takes too long to admit the bot into a meeting
POST /google/join
Content-Type: application/json
{
"bearerToken": "your-auth-token",
"url": "https://meet.google.com/abc-defg-hij",
"name": "Meeting Notetaker",
"teamId": "team123",
"timezone": "UTC",
"userId": "user123",
"botId": "UUID"
}For Google Meet, you can optionally connect to an already-running Chrome via GOOGLE_CHROME_CDP_URL, or run the browser with a dedicated signed-in Google profile by setting GOOGLE_CHROME_USER_DATA_DIR or GOOGLE_CHROME_STORAGE_STATE_PATH. The CDP option is useful when the Docker browser is treated differently from a normal Chrome. If you use CDP, launch that Chrome with --auto-accept-this-tab-capture so recording can select the current Meet tab without a manual browser prompt. The Chrome window and virtual display should normally use 1280,800; the bot then sets the page viewport to 1280x720, leaving room for Chrome's top UI without clipping the Meet controls.
For Kubernetes, run Chrome as a sidecar container in the same pod and point GOOGLE_CHROME_CDP_URL at http://127.0.0.1:9222. The included Dockerfile.chrome-cdp builds a minimal Google Chrome + Xvfb CDP backend for that setup.
For local testing, docker compose up --build starts the chrome-cdp sidecar alongside the bot and wires GOOGLE_CHROME_CDP_URL to it automatically (via the sidecar's nginx proxy on 9223, which rewrites the Host header so cross-container CDP works). If host port 6379 is already taken by another local Redis, start with REDIS_PORT_HOST=6380 docker compose up --build. Join a Meet that allows guests, make sure a second participant is present (the bot leaves if it's alone), then POST /google/join (see above) and follow docker compose logs -f meeting-bot. The recording upload will fail without configured storage credentials — that's expected; the join and recording are what this exercises. Set GOOGLE_CHROME_CDP_URL= (empty) to fall back to the bot's in-container Chromium. This sidecar joins as an anonymous guest; meetings that require sign-in need a signed-in profile (GOOGLE_CHROME_USER_DATA_DIR/GOOGLE_CHROME_STORAGE_STATE_PATH), not yet wired into the local compose.
POST /microsoft/join
Content-Type: application/json
{
"bearerToken": "your-auth-token",
"url": "https://teams.microsoft.com/l/meetup-join/...",
"name": "Meeting Notetaker",
"teamId": "team123",
"timezone": "UTC",
"userId": "user123",
"botId": "UUID"
}POST /zoom/join
Content-Type: application/json
{
"bearerToken": "your-auth-token",
"url": "https://zoom.us/j/123456789",
"name": "Meeting Notetaker",
"teamId": "team123",
"timezone": "UTC",
"userId": "user123",
"botId": "UUID"
}You can configure Meeting Bot to notify external systems when a recording has finished and is ready. Two channels are supported:
- Webhook HTTP POST
- Redis list push (RPUSH) to a configurable DB and list
Webhook notifications are disabled by default. Redis completion notifications are enabled automatically for Redis-worker mode (REDIS_CONSUMER_ENABLED=true) and can also be enabled explicitly with NOTIFY_REDIS_ENABLED=true.
-
NOTIFY_WEBHOOK_ENABLED: Enable webhook delivery (default: false)
-
NOTIFY_WEBHOOK_URL: Webhook endpoint URL
-
NOTIFY_WEBHOOK_SECRET: Optional secret to HMAC-SHA256 sign payloads. Signature header: X-Webhook-Signature
-
NOTIFY_REDIS_ENABLED: Enable Redis notifications. Completion notifications are also enabled automatically when REDIS_CONSUMER_ENABLED=true so Redis jobs produce result-list entries.
-
NOTIFY_REDIS_URI: Optional Redis URI for notifications; if not set, falls back to REDIS_HOST/REDIS_PORT/etc via redisUri
-
NOTIFY_REDIS_DB: Optional Redis database number to use for notifications. If not set, the Redis client's default DB is used. DB 0 is allowed when explicitly configured.
-
NOTIFY_REDIS_LIST: Redis list key to RPUSH to (default: jobs:meetbot:recordings)
-
NOTIFY_REDIS_FAILURE_LIST: Redis list key to RPUSH failed meeting jobs to (default: jobs:meetbot:failures)
Existing REDIS_* connection envs are used to derive a default redisUri when NOTIFY_REDIS_URI is not specified.
An example JSON payload sent via webhook and pushed to the Redis list:
{
"recordingId": "abc123",
"meetingLink": "https://your.meeting/provider/link",
"status": "completed",
"timestamp": "2025-09-08T12:00:00Z",
"metadata": {
"userId": "user123",
"teamId": "team123",
"botId": "bot-uuid",
"contentType": "video/webm",
"uploaderType": "s3",
"storage": {
"provider": "s3",
"bucket": "my-bucket",
"key": "meeting-bot/user123/2025-09-08-12-00-00.webm",
"region": "eu-central-1",
"endpoint": "https://s3.eu-central-1.amazonaws.com",
"forcePathStyle": false,
"url": "https://my-bucket.s3.eu-central-1.amazonaws.com/meeting-bot/user123/2025-09-08-12-00-00.webm"
}
},
"blobUrl": "https://my-bucket.s3.eu-central-1.amazonaws.com/meeting-bot/user123/2025-09-08-12-00-00.webm"
}
Notes:
- The storage URL is provided as blobUrl to be storage-provider agnostic (works for S3, Azure Blob, etc.). It may be omitted if not available.
- If available from internal APIs (screenapp uploader), a direct file URL is used. For S3-compatible uploads, the URL is constructed based on S3 configuration. For Azure Blob Storage, notification URLs are SAS URLs; unsigned Azure public blob URLs are not pushed to Redis as a fallback.
- If a webhook secret is configured, the request body is signed with HMAC-SHA256 and sent in the X-Webhook-Signature header.
- The metadata.storage section includes provider-specific path details. For S3-compatible uploads: bucket and key are provided. For the Screenapp uploader, you may see
{ provider: "screenapp", fileId, url, defaultProfile }.
- Notifications are triggered only after the recording upload/processing has successfully completed.
- Failed meeting jobs are pushed to NOTIFY_REDIS_FAILURE_LIST after all join/recording retries are exhausted, or after a non-retryable upload failure.
- Failure notifications are Redis-only and use the same NOTIFY_REDIS_ENABLED, NOTIFY_REDIS_URI, and NOTIFY_REDIS_DB settings.
- If both channels are enabled, both will receive the payload.
- Failures to notify are logged but do not interrupt the main recording flow.
GET /isbusyGET /metricsSuccess Response (202 Accepted):
{
"success": true,
"message": "Meeting join request accepted and processing started",
"data": {
"userId": "user123",
"teamId": "team123",
"status": "processing"
}
}Busy Response (409 Conflict):
{
"success": false,
"message": "System is currently busy processing another meeting",
"error": "BUSY"
}Meeting Bot also supports adding meeting join requests via Redis message queue, which provides asynchronous processing and better scalability for high-throughput scenarios.
interface MeetingJoinRedisParams {
url: string;
name: string;
teamId: string;
userId: string;
bearerToken: string;
timezone: string;
botId?: string;
eventId?: string;
provider: 'google' | 'microsoft' | 'zoom'; // Required for Redis
}Using RPUSH (Recommended):
# Connect to Redis and add a message to the queue
redis-cli RPUSH jobs:meetbot:list '{
"url": "https://meet.google.com/abc-defg-hij",
"name": "Meeting Notetaker",
"teamId": "team123",
"timezone": "UTC",
"userId": "user123",
"botId": "UUID",
"provider": "google",
"bearerToken": "your-auth-token"
}'Using Redis Client Libraries:
Node.js (ioredis):
import Redis from 'ioredis';
const redis = new Redis({
host: 'localhost',
port: 6379,
password: 'your-password'
});
const message = {
url: "https://meet.google.com/abc-defg-hij",
name: "Meeting Notetaker",
teamId: "team123",
timezone: "UTC",
userId: "user123",
botId: "UUID",
provider: "google",
bearerToken: "your-auth-token"
};
await redis.rpush('jobs:meetbot:list', JSON.stringify(message));Python (redis-py):
import redis
import json
r = redis.Redis(host='localhost', port=6379, password='your-password')
message = {
"url": "https://meet.google.com/abc-defg-hij",
"name": "Meeting Notetaker",
"teamId": "team123",
"timezone": "UTC",
"userId": "user123",
"botId": "UUID",
"provider": "google",
"bearerToken": "your-auth-token"
}
r.rpush('jobs:meetbot:list', json.dumps(message))- FIFO Queue: Messages are processed in First-In-First-Out order
- Atomic Processing Move: The bot uses
BLMOVEto move messages from the pending queue into the processing queue before recording starts - Processing Acknowledgement: The bot removes the original message from the processing queue with
LREMafter the recording finishes or permanently fails - Automatic Processing: Messages are automatically picked up and processed by the Redis consumer service
- Single Job Execution: Only one meeting is processed at a time across the entire system
The following environment variables configure Redis connectivity:
| Variable | Description | Default |
|---|---|---|
REDIS_HOST |
Redis server hostname | redis |
REDIS_PORT |
Redis server port | 6379 |
REDIS_USERNAME |
Redis username (optional) | - |
REDIS_PASSWORD |
Redis password (optional) | - |
REDIS_QUEUE_NAME |
Queue name for meeting jobs | jobs:meetbot:list |
REDIS_PROCESSING_QUEUE_NAME |
Queue name for active Redis meeting jobs | jobs:meetbot:processing |
REDIS_CONSUMER_ENABLED |
Enable/disable Redis consumer service | false |
Note: When REDIS_CONSUMER_ENABLED is set to false, the Redis consumer service will not start, and the application will only support REST API endpoints for meeting requests. Redis message queue functionality will be disabled.
Meeting Bot automatically uploads the meeting recording to object storage when a meeting ends. You can choose between S3-compatible storage and Microsoft Azure Blob Storage at runtime using a configuration flag.
- AWS S3 - Amazon Web Services Simple Storage Service
- GCP Cloud Storage - Google Cloud Platform S3-compatible storage
- MinIO - Self-hosted S3-compatible object storage
- Other S3-compatible services - Any service that implements the S3 API
- Azure Blob Storage - Native Azure object storage
Select the storage backend without code changes:
# s3 (default) or azure
STORAGE_PROVIDER=s3When STORAGE_PROVIDER is not set, it defaults to s3 to preserve backward compatibility.
| Variable | Description | Default | Required |
|---|---|---|---|
S3_ENDPOINT |
S3-compatible service endpoint URL | - | Yes for non-AWS |
S3_ACCESS_KEY_ID |
Access key for bucket authentication | - | Yes |
S3_SECRET_ACCESS_KEY |
Secret key for bucket authentication | - | Yes |
S3_BUCKET_NAME |
Target bucket name for uploads | - | Yes |
S3_REGION |
AWS region (for AWS S3) | - | Yes |
S3_USE_MINIO_COMPATIBILITY |
Enable MinIO compatibility mode | false |
No |
AWS S3:
S3_ENDPOINT=https://s3.amazonaws.com
S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
S3_BUCKET_NAME=meeting-recordings
S3_REGION=us-west-2Google Cloud Storage (S3-compatible):
S3_ENDPOINT=https://storage.googleapis.com
S3_ACCESS_KEY_ID=your-gcp-access-key
S3_SECRET_ACCESS_KEY=your-gcp-secret-key
S3_BUCKET_NAME=meeting-recordings
S3_REGION=us-west1MinIO:
S3_ENDPOINT=http://localhost:9000
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_BUCKET_NAME=meeting-recordings
S3_REGION=us-west-2
S3_USE_MINIO_COMPATIBILITY=trueIf you prefer Azure Blob Storage, set STORAGE_PROVIDER=azure and configure one of the supported authentication methods below. The bot will preserve the same folder structure and naming used by S3.
Required:
AZURE_STORAGE_CONTAINER— Target container name- One of the following auth options:
AZURE_STORAGE_CONNECTION_STRINGAZURE_STORAGE_ACCOUNT+AZURE_STORAGE_ACCOUNT_KEYAZURE_STORAGE_ACCOUNT+AZURE_STORAGE_SAS_TOKEN(starts with?sv=)AZURE_STORAGE_ACCOUNT+AZURE_USE_MANAGED_IDENTITY=true(requires appropriate RBAC on the container)
Optional:
AZURE_SIGNED_URL_TTL_SECONDS— Expiry for generated SAS URLs (default:3600)AZURE_UPLOAD_CONCURRENCY— Parallelism for uploads (default:4)AZURE_BLOB_PREFIX— Optional prefix path within the container (defaults to none; the bot already includes ameeting-bot/...path in object keys)
Examples
Connection string:
STORAGE_PROVIDER=azure
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;AccountName=example;AccountKey=redacted;EndpointSuffix=core.windows.net
AZURE_STORAGE_CONTAINER=meeting-recordingsAccount + Key:
STORAGE_PROVIDER=azure
AZURE_STORAGE_ACCOUNT=example
AZURE_STORAGE_ACCOUNT_KEY=redacted
AZURE_STORAGE_CONTAINER=meeting-recordingsManaged Identity (AAD):
STORAGE_PROVIDER=azure
AZURE_STORAGE_ACCOUNT=example
AZURE_USE_MANAGED_IDENTITY=true
AZURE_STORAGE_CONTAINER=meeting-recordings- Automatic Upload: When a meeting recording completes, the bot automatically uploads the file to the configured object storage (S3-compatible or Azure Blob)
- File Naming: Recordings are uploaded with descriptive names including meeting details and timestamps
- Error Handling: If upload fails, the bot will automatically retry upload
- Cleanup: Local recording files are cleaned up after successful upload
Notes:
- The default object key layout is:
meeting-bot/{userId}/{fileName}{extension}(e.g.,meeting-bot/1234/My Meeting - 2025-11-13 14-42.webm). This same layout is used for both S3 and Azure to ensure parity. - When
STORAGE_PROVIDER=azureis set and Azure environment variables are provided, the upload will go to Azure Blob Storage instead of S3. - Signed URL generation for Azure uses SAS tokens with a configurable TTL via
AZURE_SIGNED_URL_TTL_SECONDS. Redis completion notifications use these SAS URLs for AzureblobUrlandmetadata.storage.url.
| Variable | Description | Default |
|---|---|---|
MAX_RECORDING_DURATION_MINUTES |
Maximum recording duration in minutes | 180 |
MEETING_INACTIVITY_MINUTES |
Continuous inactivity duration after which the bot will end meeting recording | 1 |
INACTIVITY_DETECTION_START_DELAY_MINUTES |
Initial grace period at the start of recording before inactivity detection begins | 1 |
LONE_PARTICIPANT_EXIT_DELAY_SECONDS |
Delay before stopping after the bot has seen other participants and then becomes alone | 10 |
TEAMS_PREWARM_ENABLED |
Enable the extra Microsoft Teams warmup browser pass for environments that still show first-run dialogs | false |
TEAMS_AUDIO_STABILIZATION_MS |
Delay before starting Microsoft Teams ffmpeg recording after joining | 1000 |
GOOGLE_CHROME_CDP_URL |
Optional CDP endpoint for Google Meet joins, e.g. http://host.docker.internal:9222, to use an external Chrome instead of Docker Chrome. |
- |
GOOGLE_CHROME_USER_DATA_DIR |
Optional persistent Chrome profile directory for Google Meet joins. Use a dedicated signed-in Google account profile. | - |
GOOGLE_CHROME_STORAGE_STATE_PATH |
Optional Playwright storage state JSON for Google Meet joins when not using a persistent profile. | - |
GOOGLE_ANONYMOUS_JOIN_REQUEST_ATTEMPTS |
Number of times to re-submit an anonymous Google Meet guest request if Meet redirects while waiting for host admission. | 10 |
PORT |
Server port | 3000 |
NODE_ENV |
Environment mode | development |
UPLOADER_FILE_EXTENSION |
Final recording file extension (e.g., .mkv, .webm) | .webm |
REDIS_HOST |
Redis server hostname | redis |
REDIS_PORT |
Redis server port | 6379 |
REDIS_USERNAME |
Redis username (optional) | - |
REDIS_PASSWORD |
Redis password (optional) | - |
REDIS_QUEUE_NAME |
Queue name for meeting jobs | jobs:meetbot:list |
REDIS_PROCESSING_QUEUE_NAME |
Queue name for active Redis meeting jobs | jobs:meetbot:processing |
REDIS_CONSUMER_ENABLED |
Enable/disable Redis consumer service | false |
S3_ENDPOINT |
S3-compatible service endpoint URL | - |
S3_ACCESS_KEY_ID |
Access key for bucket authentication | - |
S3_SECRET_ACCESS_KEY |
Secret key for bucket authentication | - |
S3_BUCKET_NAME |
Target bucket name for uploads | - |
S3_REGION |
AWS region (for AWS S3) | - |
S3_USE_MINIO_COMPATIBILITY |
Enable MinIO compatibility mode | false |
The project includes Docker support with separate configurations for development and production:
Dockerfile.development- Development buildDockerfile.production- Optimized production buildDockerfile.chrome-cdp- Google Chrome CDP backend for Kubernetes sidecar deploymentsdocker-compose.yml- Complete development environment
Build and publish the Chrome backend image separately from the bot image:
docker build -f Dockerfile.chrome-cdp -t ghcr.io/your-org/meeting-bot-chrome-cdp:latest .
docker push ghcr.io/your-org/meeting-bot-chrome-cdp:latestThen run it as a sidecar in the same Kubernetes pod as the bot:
containers:
- name: meeting-bot
image: ghcr.io/your-org/meeting-bot:latest
env:
- name: GOOGLE_CHROME_CDP_URL
value: http://127.0.0.1:9222
- name: chrome-cdp
image: ghcr.io/your-org/meeting-bot-chrome-cdp:latest
ports:
- name: cdp
containerPort: 9222
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 3GiDo not expose the CDP port through a Kubernetes Service or Ingress. Keep it local to the pod so only the bot container can control the browser.
The project automatically builds and publishes Docker images to GitHub Packages on every push to the main branch.
Pull the latest image:
docker pull ghcr.io/screenappai/meeting-bot:latestRun the container:
docker run -d \
--name meeting-bot \
-p 3000:3000 \
-e MAX_RECORDING_DURATION_MINUTES=60 \
-e NODE_ENV=production \
-e REDIS_CONSUMER_ENABLED=false \
-e S3_ENDPOINT= \
-e S3_ACCESS_KEY_ID= \
-e S3_SECRET_ACCESS_KEY= \
-e S3_BUCKET_NAME= \
-e S3_REGION= \
ghcr.io/screenappai/meeting-bot:latestAvailable tags:
latest- Latest stable release from main branchmain- Latest commit from main branchsha-<commit-hash>- Specific commit builds
src/
├── app/ # Express application and route handlers
├── bots/ # Platform-specific bot implementations
├── connect/ # Redis message broker and consumer services
├── lib/ # Core libraries and utilities
├── middleware/ # Express middleware
├── services/ # Business logic services
├── tasks/ # Background task implementations
├── types/ # TypeScript type definitions
└── util/ # Utility functions
- AbstractMeetBot: Base class for all platform bots
- JobStore: Manages single job execution across the system
- RecordingTask: Handles meeting recording functionality
- ContextBridgeTask: Manages browser context and automation
- RedisMessageBroker: Handles Redis queue operations (RPUSH/BLMOVE/LREM)
- RedisConsumerService: Processes messages from Redis queue asynchronously
Meeting Bot supports joining meetings where users can join with a direct link without requiring authentication. The following scenarios are not supported:
- Sign-in Required: Meetings that require users to sign in to the platform (Google, Microsoft, Zoom) before joining
- Enterprise Authentication: Meetings that require enterprise SSO or domain-specific authentication
- Password Protected: Meetings that require a password in addition to the meeting link
- Waiting Room with Authentication: Meetings where the waiting room requires user identification or authentication
Supported Scenarios:
- ✅ Public meeting links that allow direct join
- ✅ Meetings with waiting rooms that don't require authentication
- ✅ Meetings where the bot can join as a guest/anonymous participant
We welcome contributions! Please see our Contributing Guide for details on how to:
- Set up your development environment
- Submit bug reports and feature requests
- Contribute code changes
- Follow our coding standards
This project is licensed under the MIT License - see the LICENSE file for details.
🎯 Primary Support Channel:
- Discord: Join our Discord Community - Our main forum for discussions, support, and real-time collaboration
📋 Additional Resources:
- Issues: GitHub Issues - For bug reports and feature requests
- Documentation: Wiki - Detailed documentation and guides
- Built with Playwright for reliable browser automation
- Uses Express.js for the web server
- Containerized with Docker
- ✅ Google Meet support
- ✅ Microsoft Teams support
- ✅ Zoom support
- ✅ Recording functionality
- ✅ Docker deployment
- ✅ REST API support
- ✅ Redis message queue support
- ✅ Recording Upload support - S3-compatible bucket storage (AWS, GCP, MinIO)
- 🔄 Additional video format support (planned)
- 🔄 Enhanced platform feature support (planned)
Note: This project is for educational and legitimate automation purposes. Please ensure compliance with the terms of service of the platforms you're automating.