aethercall
v1.0.1
Published
A scalable WebRTC video calling API built with Node.js and OpenVidu
Downloads
10
Maintainers
Readme
AetherCall
AetherCall is a production-ready, open-source video calling API built on top of OpenVidu. It provides a robust HTTP REST API for integrating real-time video communication, audio calling, screen sharing, and recording capabilities into any application.
Features
- Real-time Video & Audio: High-quality WebRTC-based communication
- Screen Sharing: Share entire screen or specific applications with participants
- Session Recording: Record video sessions with configurable layouts and formats
- Room-based Access: Simple room code system for easy participant joining
- JWT Authentication: Secure token-based authentication with role management
- HTTP REST API: Clean, documented API endpoints for all operations
- Database Flexibility: Support for PostgreSQL, MongoDB, filesystem, or in-memory storage
- Production Ready: Built-in rate limiting, CORS, security headers, and error handling
- Comprehensive Testing: 45+ automated tests covering all functionality
Architecture
AetherCall/
├── index.js # Main application entry point
├── src/
│ ├── core/ # Core business logic
│ │ ├── auth/ # JWT token management
│ │ ├── openvidu/ # OpenVidu API wrapper
│ │ └── storage/ # Database abstraction layer
│ │
│ ├── interfaces/
│ │ └── http/ # HTTP REST API
│ │ ├── routes/ # API route handlers
│ │ └── server.js # Express server setup
│ │
│ └── utils/ # Shared utilities
│ ├── config.js # Configuration management
│ └── logger.js # Structured logging
│
├── tests/ # Test suite (Jest)
├── docs/ # API documentation
└── package.json # Project dependenciesInstallation
Prerequisites
- Node.js 16.x or higher
- npm or yarn
- OpenVidu Server (Docker recommended)
Install via npm
npm install aethercallInstall from Source
git clone https://github.com/RayenMiri/AetherCall.git
cd AetherCall
npm installQuick Start
1. Start OpenVidu Server
AetherCall requires an OpenVidu server. The easiest way is using Docker:
# Pull and run OpenVidu development server
docker run -p 4443:4443 --rm \
-e OPENVIDU_SECRET=MY_SECRET \
openvidu/openvidu-dev:2.29.02. Configure Environment
Create a .env file in your project root:
# OpenVidu Configuration
OPENVIDU_URL=http://localhost:4443
OPENVIDU_SECRET=MY_SECRET
# Server Configuration
PORT=3000
HOST=localhost
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
# Database (optional - defaults to memory)
DB_TYPE=memory
# Logging
LOG_LEVEL=info3. Start the API Server
# Using npm
npm start
# Using yarn
yarn start
# Development mode with auto-reload
npm run dev
# Or using the setup helper
npm run setup4. Using as a Module
See examples/usage-example.js for detailed examples of how to integrate AetherCall into your existing Node.js applications:
# Run different usage examples
node examples/usage-example.js simple # Simple usage with defaults
node examples/usage-example.js advanced # Advanced configuration
node examples/usage-example.js express # Integration with Express app
node examples/usage-example.js production # Production configurationThe API will be available at http://localhost:3000
5. Verify Installation
Check if the API is running:
curl http://localhost:3000/healthExpected response:
{
"status": "healthy",
"timestamp": "2025-07-27T10:00:00.000Z",
"version": "1.0.0"
}API Usage
Base URL
http://localhost:3000/apiAuthentication
All API requests (except health check) require a JWT token:
// Get API token
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientId: 'your-application-id',
expiresIn: '1h' // optional
})
});
const { data } = await response.json();
const apiToken = data.accessToken;Core Endpoints
Sessions Management
// Create a video session
const session = await fetch('/api/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
mediaMode: 'ROUTED',
recordingMode: 'MANUAL',
metadata: JSON.stringify({ roomName: 'My Meeting' })
})
});
// Get session info
const sessionInfo = await fetch(`/api/sessions/${sessionId}`, {
headers: { 'Authorization': `Bearer ${apiToken}` }
});
// Close session
await fetch(`/api/sessions/${sessionId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${apiToken}` }
});Connection Management
// Create connection token for participant
const connection = await fetch('/api/connections', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'session-id',
role: 'PUBLISHER', // SUBSCRIBER, PUBLISHER, MODERATOR
data: JSON.stringify({
userId: 'user123',
displayName: 'John Doe'
})
})
});
// Join room with room code (simplified API)
const roomConnection = await fetch('/api/connections/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomCode: 'ABC123',
userId: 'user123',
displayName: 'John Doe',
role: 'PUBLISHER'
})
});Recording Management
// Start recording
const recording = await fetch('/api/recordings/start', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: 'session-id',
name: 'my-recording',
outputMode: 'COMPOSED',
recordingLayout: 'BEST_FIT'
})
});
// Stop recording
await fetch(`/api/recordings/stop/${recordingId}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiToken}` }
});
// Get recording info
const recordingInfo = await fetch(`/api/recordings/${recordingId}`, {
headers: { 'Authorization': `Bearer ${apiToken}` }
});Room Management (Simplified API)
// Create room with simple code
const room = await fetch('/api/auth/room', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
roomName: 'Daily Standup',
maxParticipants: 10,
roomCode: 'STANDUP123' // optional, will be generated if not provided
})
});Frontend Integration
Using with OpenVidu Browser SDK
React Example
import React, { useState, useEffect, useRef } from 'react';
import { OpenVidu } from 'openvidu-browser';
function VideoRoom({ roomCode, userDisplayName }) {
const [session, setSession] = useState(null);
const [publisher, setPublisher] = useState(null);
const [subscribers, setSubscribers] = useState([]);
const publisherRef = useRef(null);
useEffect(() => {
joinRoom();
return () => leaveRoom();
}, []);
const joinRoom = async () => {
try {
// Get connection token from AetherCall API
const response = await fetch('/api/connections/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomCode: roomCode,
userId: `user-${Date.now()}`,
displayName: userDisplayName,
role: 'PUBLISHER'
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const { data } = await response.json();
// Initialize OpenVidu session
const OV = new OpenVidu();
const session = OV.initSession();
// Event handlers
session.on('streamCreated', (event) => {
const subscriber = session.subscribe(event.stream, undefined);
setSubscribers(prev => [...prev, subscriber]);
});
session.on('streamDestroyed', (event) => {
setSubscribers(prev =>
prev.filter(sub => sub.stream !== event.stream)
);
});
// Connect to session
await session.connect(data.token, {
clientData: data.userId
});
// Create and publish local stream
const publisher = await OV.initPublisherAsync(undefined, {
audioSource: undefined,
videoSource: undefined,
publishAudio: true,
publishVideo: true,
resolution: '640x480',
frameRate: 30,
insertMode: 'APPEND',
mirror: false
});
await session.publish(publisher);
setSession(session);
setPublisher(publisher);
} catch (error) {
console.error('Error joining room:', error);
}
};
const leaveRoom = () => {
if (session) {
session.disconnect();
}
};
const toggleVideo = () => {
if (publisher) {
publisher.publishVideo(!publisher.stream.videoActive);
}
};
const toggleAudio = () => {
if (publisher) {
publisher.publishAudio(!publisher.stream.audioActive);
}
};
return (
<div className="video-room">
<div className="controls">
<button onClick={toggleVideo}>
{publisher?.stream?.videoActive ? 'Turn Off Video' : 'Turn On Video'}
</button>
<button onClick={toggleAudio}>
{publisher?.stream?.audioActive ? 'Mute' : 'Unmute'}
</button>
<button onClick={leaveRoom}>Leave Room</button>
</div>
<div className="video-container">
<div ref={publisherRef} className="publisher-video" />
<div className="subscribers">
{subscribers.map((subscriber, index) => (
<div
key={index}
ref={(ref) => {
if (ref && subscriber.videos[0]) {
subscriber.addVideoElement(ref);
}
}}
className="subscriber-video"
/>
))}
</div>
</div>
</div>
);
}
export default VideoRoom;Vanilla JavaScript Example
<!DOCTYPE html>
<html>
<head>
<title>AetherCall Video Room</title>
<script src="https://github.com/OpenVidu/openvidu/releases/download/v2.29.0/openvidu-browser-2.29.0.min.js"></script>
</head>
<body>
<div id="video-container">
<div id="publisher"></div>
<div id="subscribers"></div>
</div>
<div id="controls">
<button onclick="toggleVideo()">Toggle Video</button>
<button onclick="toggleAudio()">Toggle Audio</button>
<button onclick="leaveRoom()">Leave Room</button>
</div>
<script>
let session;
let publisher;
async function joinRoom(roomCode, displayName) {
try {
// Get token from AetherCall
const response = await fetch('/api/connections/join-room', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomCode: roomCode,
userId: `user-${Date.now()}`,
displayName: displayName,
role: 'PUBLISHER'
})
});
const { data } = await response.json();
// Create OpenVidu session
const OV = new OpenVidu();
session = OV.initSession();
// Handle new streams
session.on('streamCreated', (event) => {
const subscriber = session.subscribe(event.stream, 'subscribers');
});
// Connect and publish
await session.connect(data.token);
publisher = await OV.initPublisherAsync(undefined, {
publishAudio: true,
publishVideo: true
});
await session.publish(publisher);
publisher.addVideoElement('publisher');
} catch (error) {
console.error('Error:', error);
}
}
function toggleVideo() {
if (publisher) {
publisher.publishVideo(!publisher.stream.videoActive);
}
}
function toggleAudio() {
if (publisher) {
publisher.publishAudio(!publisher.stream.audioActive);
}
}
function leaveRoom() {
if (session) {
session.disconnect();
}
}
// Join room on page load
joinRoom('DEMO123', 'Demo User');
</script>
</body>
</html>Backend Integration
Using AetherCall as a Service
Express.js Integration
const express = require('express');
const axios = require('axios');
const app = express();
const AETHERCALL_API_URL = 'http://localhost:3000/api';
// Get API token (you should cache this)
async function getAetherCallToken() {
const response = await axios.post(`${AETHERCALL_API_URL}/auth/token`, {
clientId: 'your-app-id'
});
return response.data.data.accessToken;
}
// Create meeting endpoint
app.post('/api/meetings', async (req, res) => {
try {
const token = await getAetherCallToken();
// Create room in AetherCall
const roomResponse = await axios.post(`${AETHERCALL_API_URL}/auth/room`, {
roomName: req.body.title,
maxParticipants: req.body.maxParticipants || 10
}, {
headers: { Authorization: `Bearer ${token}` }
});
const { data } = roomResponse.data;
res.json({
meetingId: data.sessionId,
roomCode: data.roomCode,
joinUrl: `${req.protocol}://${req.get('host')}/join/${data.roomCode}`
});
} catch (error) {
console.error('Error creating meeting:', error);
res.status(500).json({ error: 'Failed to create meeting' });
}
});
// Join meeting endpoint
app.post('/api/meetings/:roomCode/join', async (req, res) => {
try {
const { roomCode } = req.params;
const { userId, displayName, role = 'PUBLISHER' } = req.body;
const connectionResponse = await axios.post(`${AETHERCALL_API_URL}/connections/join-room`, {
roomCode,
userId,
displayName,
role
});
res.json(connectionResponse.data);
} catch (error) {
console.error('Error joining meeting:', error);
res.status(500).json({ error: 'Failed to join meeting' });
}
});Recording Management
// Start recording for a session
app.post('/api/meetings/:sessionId/recording/start', async (req, res) => {
try {
const token = await getAetherCallToken();
const { sessionId } = req.params;
const recording = await axios.post(`${AETHERCALL_API_URL}/recordings/start`, {
sessionId,
name: `meeting-${sessionId}-${Date.now()}`,
outputMode: 'COMPOSED',
recordingLayout: 'BEST_FIT'
}, {
headers: { Authorization: `Bearer ${token}` }
});
res.json({
recordingId: recording.data.data.id,
status: 'started'
});
} catch (error) {
console.error('Error starting recording:', error);
res.status(500).json({ error: 'Failed to start recording' });
}
});
// Stop recording
app.post('/api/recordings/:recordingId/stop', async (req, res) => {
try {
const token = await getAetherCallToken();
const { recordingId } = req.params;
const recording = await axios.post(`${AETHERCALL_API_URL}/recordings/stop/${recordingId}`, {}, {
headers: { Authorization: `Bearer ${token}` }
});
res.json({
recordingId,
duration: recording.data.data.duration,
downloadUrl: recording.data.data.url,
status: 'completed'
});
} catch (error) {
console.error('Error stopping recording:', error);
res.status(500).json({ error: 'Failed to stop recording' });
}
});Using AetherCall as a Library
const AetherCall = require('aethercall');
// Initialize AetherCall instance
const aetherCall = new AetherCall({
openviduUrl: process.env.OPENVIDU_URL,
openviduSecret: process.env.OPENVIDU_SECRET,
jwtSecret: process.env.JWT_SECRET,
storage: {
type: 'postgresql',
connectionString: process.env.DATABASE_URL
}
});
// Start the server
app.listen(3000, async () => {
await aetherCall.start();
console.log('AetherCall server running on port 3000');
});Configuration
Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| OPENVIDU_URL | OpenVidu server URL | http://localhost:4443 |
| OPENVIDU_SECRET | OpenVidu server secret | Required |
| PORT | HTTP server port | 3000 |
| JWT_SECRET | JWT signing secret | Required |
| DB_TYPE | Database type | memory |
| LOG_LEVEL | Logging level | info |
Database Support
# PostgreSQL
DB_TYPE=postgres
DB_CONNECTION_STRING=postgresql://user:pass@localhost:5432/aethercall
# MongoDB
DB_TYPE=mongodb
DB_CONNECTION_STRING=mongodb://localhost:27017/aethercall
# File System
DB_TYPE=filesystem
DATA_PATH=./data
# Memory (Development)
DB_TYPE=memoryDeployment
Production Deployment
Environment Variables for Production
# OpenVidu Configuration
OPENVIDU_URL=https://your-openvidu-server.com:4443
OPENVIDU_SECRET=your-production-secret
# Server Configuration
NODE_ENV=production
PORT=3000
HOST=0.0.0.0
JWT_SECRET=your-super-secure-jwt-secret-change-this
# Database
DB_TYPE=postgresql
DB_CONNECTION_STRING=postgresql://user:password@host:5432/aethercall
# Security
CORS_ORIGIN=https://yourdomain.com
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100
# Logging
LOG_LEVEL=warnDocker Deployment
Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci --only=production
# Copy source code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S aethercall -u 1001
USER aethercall
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
CMD ["npm", "start"]Docker Compose with PostgreSQL
version: '3.8'
services:
aethercall:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- OPENVIDU_URL=http://openvidu:4443
- OPENVIDU_SECRET=MY_SECRET
- DB_TYPE=postgresql
- DB_CONNECTION_STRING=postgresql://postgres:password@postgres:5432/aethercall
- JWT_SECRET=your-jwt-secret
depends_on:
postgres:
condition: service_healthy
openvidu:
condition: service_started
restart: unless-stopped
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=aethercall
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
openvidu:
image: openvidu/openvidu-dev:2.29.0
environment:
- OPENVIDU_SECRET=MY_SECRET
ports:
- "4443:4443"
restart: unless-stopped
volumes:
postgres_data:Cloud Platform Deployment
Railway
# Install Railway CLI
npm install -g @railway/cli
# Login and deploy
railway login
railway init
railway upHeroku
# Create Heroku app
heroku create your-app-name
# Set environment variables
heroku config:set NODE_ENV=production
heroku config:set OPENVIDU_URL=https://your-openvidu.herokuapp.com:4443
heroku config:set OPENVIDU_SECRET=your-secret
heroku config:set JWT_SECRET=your-jwt-secret
# Deploy
git push heroku mainRender
# render.yaml
services:
- type: web
name: aethercall-api
env: node
plan: starter
buildCommand: npm install
startCommand: npm start
envVars:
- key: NODE_ENV
value: production
- key: OPENVIDU_URL
value: https://your-openvidu-instance.onrender.com:4443OpenVidu Server Deployment
For production, you'll need a separate OpenVidu server. Options include:
- OpenVidu Cloud (Recommended for production)
- Self-hosted on AWS/GCP/Azure
- Docker deployment on your infrastructure
See OpenVidu Deployment Guide for detailed instructions.
Testing
AetherCall includes a comprehensive test suite with 45+ automated tests covering all functionality.
Running Tests
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run specific test file
npm test tests/http-api.test.js
# Run tests with coverage report
npm run test:coverage
# Run tests matching a pattern
npm test -- --grep "session"Test Coverage
- HTTP API Tests: All REST endpoints with authentication, validation, and error handling
- Storage Tests: Database operations across all supported storage types
- OpenVidu Integration Tests: Session, connection, and recording functionality
- Authentication Tests: JWT token management and role-based access control
Test Requirements
Before running tests, ensure you have:
- OpenVidu Server running (see Installation section)
- Test environment configured:
# .env.test
OPENVIDU_URL=http://localhost:4443
OPENVIDU_SECRET=MY_SECRET
JWT_SECRET=test-secret-key
DB_TYPE=memory
LOG_LEVEL=errorContinuous Integration
Tests are automatically run on:
- Pull requests
- Commits to main branch
- Release builds
Example GitHub Actions workflow:
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
openvidu:
image: openvidu/openvidu-dev:2.29.0
ports:
- 4443:4443
env:
OPENVIDU_SECRET: MY_SECRET
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
- run: npm ci
- run: npm testMonitoring & Analytics
Health Check
curl http://localhost:3000/healthSystem Metrics
// Built-in metrics endpoint
app.get('/metrics', (req, res) => {
res.json({
uptime: process.uptime(),
memory: process.memoryUsage(),
activeSessions: aetherCall.getActiveSessionCount(),
totalConnections: aetherCall.getTotalConnectionCount()
});
});Security
Rate Limiting
Built-in rate limiting (100 requests per 15 minutes by default):
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX_REQUESTS=100CORS Configuration
CORS_ORIGIN=https://yourdomain.com
CORS_METHODS=GET,POST,PUT,DELETEToken Security
- JWT tokens with configurable expiration
- Role-based access control (SUBSCRIBER, PUBLISHER, MODERATOR)
- Automatic token validation middleware
Contributing
We welcome contributions from the community! Please read our Contributing Guidelines before submitting PRs.
Development Setup
- Fork and clone the repository
git clone https://github.com/YOUR_USERNAME/AetherCall.git
cd AetherCall- Install dependencies
npm install- Set up development environment
cp .env.example .env
# Edit .env with your configuration- Start OpenVidu development server
docker run -p 4443:4443 --rm \
-e OPENVIDU_SECRET=MY_SECRET \
openvidu/openvidu-dev:2.29.0- Run in development mode
npm run devContribution Guidelines
- Code Style: We use ESLint and Prettier for code formatting
- Testing: All new features must include tests
- Documentation: Update README and API docs for any changes
- Commit Messages: Use conventional commit format
Pull Request Process
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Run the test suite:
npm test - Update documentation if needed
- Commit your changes:
git commit -m 'feat: add amazing feature' - Push to your fork:
git push origin feature/amazing-feature - Open a Pull Request with a clear description
Security
Reporting Security Issues
If you discover a security vulnerability, please email us at [email protected] instead of using the issue tracker.
Security Features
- JWT Authentication: Secure token-based authentication with configurable expiration
- Rate Limiting: Built-in protection against abuse (configurable)
- CORS Configuration: Proper cross-origin resource sharing setup
- Input Validation: All API inputs are validated and sanitized
- Security Headers: Helmet.js for security headers
- Role-based Access: SUBSCRIBER, PUBLISHER, MODERATOR roles
;; ## License
;; This project is licensed under the MIT License - see the LICENSE file for details.
Support and Documentation
Documentation
- API Reference - Complete API documentation
- OpenVidu Integration Guide - OpenVidu documentation ;; - Examples Repository - Sample implementations
;; ### Community Support ;; - GitHub Discussions - Community Q&A ;; - Issue Tracker - Bug reports and feature requests ;; - Discord Server - Real-time community chat
Professional Support
- Email Support - Technical support ;; - Enterprise Support - Custom solutions and consulting
;; ## Roadmap
;; ### Version 2.0 (Q3 2025) ;; - [ ] WebSocket API: Real-time event streaming ;; - [ ] GraphQL Interface: Alternative to REST API ;; - [ ] Advanced Analytics: Detailed session and participant metrics ;; - [ ] Load Balancing: Multi-instance deployment support
;; ### Version 2.1 (Q4 2025) ;; - [ ] Mobile SDKs: React Native and Flutter wrappers ;; - [ ] Advanced Recording: Individual participant streams ;; - [ ] Moderation Tools: Participant management and content moderation
- [ ] Custom Layouts: Configurable recording layouts
Future Releases
- [ ] AI Integration: Automatic transcription and translation
- [ ] Cloud Storage: Direct integration with AWS S3, Google Cloud Storage
- [ ] Streaming: RTMP/HLS streaming support
- [ ] SIP Integration: Traditional phone system integration
License
AetherCall is released under the MIT License. This means you can:
✅ What You Can Do:
- Use commercially - Build and sell applications using AetherCall
- Modify freely - Customize the code to fit your needs
- Distribute - Share the software with others
- Sublicense - Include it in projects with different licenses
- Private use - Use in proprietary/closed-source projects
📋 Requirements:
- Include copyright notice - Keep the original copyright and license notice
- Include license text - Include the MIT license text in distributions
🚫 Limitations:
- No warranty - Software is provided "as-is" without warranties
- No liability - Authors are not liable for damages or issues
💡 Why MIT License?
The MIT License offers maximum flexibility for developers while being business-friendly:
- Corporate adoption - Companies can use and modify without legal concerns
- Open source ecosystem - Compatible with most other open source licenses
- Simple compliance - Easy to understand and follow requirements
- Innovation encouragement - Allows derivative works and improvements
- Community growth - Promotes sharing and collaboration
AetherCall - Professional video calling API for modern applications
