@mena-stream/client
v1.0.1
Published
MENA-Stream JavaScript SDK
Maintainers
Readme
@mena-stream/client
JavaScript/TypeScript SDK for MENA-Stream WebRTC API.
What is MENA-Stream?
MENA-Stream is a production-grade WebRTC CPaaS platform built for the Arab market.
Architecture:
- This SDK — Open-source (MIT). Handles all WebRTC complexity in the browser.
- Backend — Built on mediasoup, extended with proprietary optimizations for low-latency real-time communication. Available via Cloud API or Self-Hosted Enterprise.
Benchmarks
Tested on a $29/month server:
| Metric | Result | |------------------------------|:---------:| | Concurrent Users (Signaling) | 1,642 | | Concurrent WebRTC Producers | 200 | | Server CPU at peak | 13% | | HTTP Failure Rate | 0% | | Success Rate | 100% |
Installation
npm install @mena-stream/clientOr via CDN:
<script src="https://cdn.jsdelivr.net/npm/@mena-stream/client/dist/mena-stream-sdk.js"></script>
Quick Start
import MenaStreamClient from '@mena-stream/client';
// Initialize
const client = new MenaStreamClient({
apiKey: 'sk_live_...',
baseUrl: 'https://api.mena-stream.com'
});
// Create a room
const room = await client.createRoom({ name: 'My Meeting' });
// Join the room
const session = await room.join({ displayName: 'Ahmed' });
// Enable camera & microphone
const stream = await session.enableCamera();
document.getElementById('local-video').srcObject = stream;API Reference
new MenaStreamClient(config)
const client = new MenaStreamClient({
apiKey: 'sk_live_...', // Required — get from dashboard.mena-stream.com
baseUrl: 'https://...', // Optional — defaults to MENA-Stream Cloud
wsUrl: 'wss://...', // Optional — defaults to baseUrl with ws protocol
});client.createRoom(opts)
Create a new video room.
const room = await client.createRoom({
name: 'Weekly Standup', // Required
maxParticipants: 50, // Optional — default: 50
expiresIn: 3600, // Optional — seconds, default: 3600
});
console.log(room.id); // Room ID
console.log(room.name); // Room name
console.log(room.joinUrl); // Shareable join URLclient.getRoom(roomId)
Get an existing room by ID.
const room = await client.getRoom('room_id_here');room.join(opts)
Join a room and start a WebRTC session.
const session = await room.join({
displayName: 'Ahmed', // Required
role: 'participant', // Optional: 'participant' | 'moderator'
});
console.log(session.participantId); // Your participant IDRoles:
participant— Regular user. Will wait for admission if host has enabled it.moderator— Host. Enables admission control automatically.
room.close()
Close the room and disconnect all participants.
await room.close();session.enableCamera(opts)
Enable camera and microphone.
const stream = await session.enableCamera({
video: true, // Optional — default: true
audio: true, // Optional — default: true
});
// Display local video
document.getElementById('local-video').srcObject = stream;session.shareScreen()
Start screen sharing.
const stream = await session.shareScreen();
document.getElementById('screen-video').srcObject = stream;session.stopScreenShare()
Stop screen sharing.
session.stopScreenShare();session.consume(producerId)
Consume a remote participant's media track.
session.addEventListener('newProducer', async ({ detail }) => {
const track = await session.consume(detail.producerId);
if (detail.kind === 'video') {
const video = document.createElement('video');
video.srcObject = new MediaStream([track]);
video.autoplay = true;
document.body.appendChild(video);
} else {
const audio = document.createElement('audio');
audio.srcObject = new MediaStream([track]);
audio.autoplay = true;
document.body.appendChild(audio);
}
});session.setAudioEnabled(enabled)
Mute or unmute microphone.
session.setAudioEnabled(false); // Mute
session.setAudioEnabled(true); // Unmutesession.setVideoEnabled(enabled)
Enable or disable camera.
session.setVideoEnabled(false); // Camera off
session.setVideoEnabled(true); // Camera onsession.getParticipants()
Get the list of current participants.
const participants = await session.getParticipants();
// Returns: [{ id, displayName, uptime }]
participants.forEach(p => {
console.log(p.displayName, p.uptime + 's');
});session.setupIntersectionObserver(callback)
Automatically adjust video quality based on visibility. High quality for visible videos, low quality for off-screen videos.
// Set up observer — callback returns consumerId for a participantId
session.setupIntersectionObserver((participantId) => {
return consumerMap.get(participantId)?.videoConsumerId;
});
// Observe a video element
const observer = session.setupIntersectionObserver(getConsumerId);
const videoCard = document.getElementById(`video-${participantId}`);
videoCard.dataset.participantId = participantId;
observer.observe(videoCard);session.disconnect()
Leave the room and clean up all resources.
session.disconnect();Events
participantJoined
Fired when a new participant joins the room.
session.addEventListener('participantJoined', ({ detail }) => {
console.log('Joined:', detail.participantId, detail.displayName);
});participantLeft
Fired when a participant leaves the room.
session.addEventListener('participantLeft', ({ detail }) => {
console.log('Left:', detail.participantId);
// Remove their video element
document.getElementById(`video-${detail.participantId}`)?.remove();
});newProducer
Fired when a remote participant starts sending audio or video.
session.addEventListener('newProducer', async ({ detail }) => {
// detail: { producerId, participantId, kind, appData }
const track = await session.consume(detail.producerId);
const stream = new MediaStream([track]);
// Attach stream to video/audio element
});activeSpeakers
Fired every 800ms with the list of active speakers.
session.addEventListener('activeSpeakers', ({ detail }) => {
// detail: { speakers: [{ participantId, volume }] }
const topSpeaker = detail.speakers[0];
if (topSpeaker) {
console.log('Speaking:', topSpeaker.participantId);
// Highlight their video
document.getElementById(`video-${topSpeaker.participantId}`)
?.classList.add('speaking');
}
});waitingForAdmission
Fired when the participant is waiting for the host's approval.
session.addEventListener('waitingForAdmission', () => {
console.log('Waiting for host approval...');
// Show waiting UI
});admissionGranted
Fired when the host approves the admission request.
session.addEventListener('admissionGranted', async () => {
console.log('Admitted! Starting camera...');
const stream = await session.enableCamera();
document.getElementById('local-video').srcObject = stream;
});admissionDenied
Fired when the host denies the admission request.
session.addEventListener('admissionDenied', () => {
console.log('Admission denied');
session.disconnect();
});admissionRequest
Fired on the moderator's session when a participant requests to join.
session.addEventListener('admissionRequest', ({ detail }) => {
// detail: { participantId, displayName }
console.log(`${detail.displayName} wants to join`);
// Show approval UI
showAdmissionDialog(detail.participantId, detail.displayName);
});To approve or deny via WebSocket:
// Approve
session._ws.send(JSON.stringify({
type: 'admitUser',
id: String(Date.now()),
data: { participantId: detail.participantId }
}));
// Deny
session._ws.send(JSON.stringify({
type: 'denyUser',
id: String(Date.now()),
data: { participantId: detail.participantId }
}));producerPaused
Fired when a remote participant mutes their microphone or camera.
session.addEventListener('producerPaused', ({ detail }) => {
// detail: { participantId, producerId }
// Show muted indicator
});producerResumed
Fired when a remote participant unmutes.
session.addEventListener('producerResumed', ({ detail }) => {
// Hide muted indicator
});disconnected
Fired when the session is disconnected.
session.addEventListener('disconnected', ({ detail }) => {
console.log('Disconnected, code:', detail.code);
});Complete Example
import MenaStreamClient from '@mena-stream/client';
const client = new MenaStreamClient({
apiKey: 'sk_live_...',
baseUrl: 'https://api.mena-stream.com'
});
async function startMeeting() {
// Create room as moderator (enables admission control)
const room = await client.createRoom({ name: 'Team Meeting' });
const session = await room.join({
displayName: 'Ahmed',
role: 'moderator'
});
// Local video
const stream = await session.enableCamera();
document.getElementById('local').srcObject = stream;
// Remote participants
session.addEventListener('newProducer', async ({ detail }) => {
const track = await session.consume(detail.producerId);
if (detail.kind === 'video') {
const video = document.createElement('video');
video.srcObject = new MediaStream([track]);
video.autoplay = true;
document.getElementById('grid').appendChild(video);
}
});
// Participant joined/left
session.addEventListener('participantJoined', ({ detail }) => {
console.log(detail.displayName, 'joined');
});
session.addEventListener('participantLeft', ({ detail }) => {
document.getElementById(`v-${detail.participantId}`)?.remove();
});
// Active speaker
session.addEventListener('activeSpeakers', ({ detail }) => {
if (detail.speakers[0]) {
highlightSpeaker(detail.speakers[0].participantId);
}
});
// Admission requests
session.addEventListener('admissionRequest', ({ detail }) => {
if (confirm(`${detail.displayName} wants to join. Admit?`)) {
session._ws.send(JSON.stringify({
type: 'admitUser',
id: String(Date.now()),
data: { participantId: detail.participantId }
}));
}
});
// Controls
document.getElementById('mute').onclick = () =>
session.setAudioEnabled(false);
document.getElementById('unmute').onclick = () =>
session.setAudioEnabled(true);
document.getElementById('leave').onclick = () =>
session.disconnect();
}
startMeeting();Performance Features
| Feature | Description | Benefit | |------------------------|------------------------------------|------------------------------| | Simulcast | 3 quality layers (100/300/900 Kbps)| Auto quality adaptation | | SVC via VP9 | Scalable Video Coding S3T3 | 40% less bandwidth | | Audio DTX | No packets during silence | 60% audio bandwidth saving | | ActiveSpeaker | 800ms detection interval | Auto-highlight speaker | | Producer Pause | Zero bytes when muted | Instant bandwidth saving | | Intersection Observer | HD for visible, SD for hidden | Up to 80% bandwidth saving | | Transport Pool | Pre-allocated transports | 70% faster room join | | PipeTransport | Cross-worker media routing | Scales across 8 CPU cores |
Pricing
| Plan | Minutes/Month | Price | |------------|---------------|--------------| | Free | 1,000 | Free forever | | Starter | 10,000 | $29/month | | Growth | 100,000 | $99/month | | Scale | 500,000 | $299/month | | Enterprise | Unlimited | Custom |
Minutes = participants × minutes (3 users × 10 min = 30 minutes)
Payment: mada, Visa, Mastercard, Apple Pay
Self-Hosted Enterprise
Deploy MENA-Stream on your own servers with a license key.
# Your servers. Your data. Fixed monthly cost.
docker compose up -dContact: [email protected]
Links
- Site: https://mena-stream.com
- GitHub: https://github.com/salahaldain-abduljalil/mena_stream_project
- npm: https://www.npmjs.com/package/@mena-stream/client
License
MIT — The SDK is free and open-source.
The MENA-Stream backend infrastructure is proprietary. This SDK connects to it.
Copyright (c) 2026 Salah Aldain Abduljalil