@vizipp/video-call
v0.2.1
Published
Headless Angular SDK for mediasoup-based video calls with Socket.IO signaling
Maintainers
Readme
@vizipp/video-call
Headless Angular SDK (Signals-based) for mediasoup SFU video calling with Socket.IO signaling.
Installation
npm i @vizipp/video-call mediasoup-client socket.io-clientAngular Setup
Provide the configuration in app.config.ts:
import { ApplicationConfig } from '@angular/core';
import { provideVideoCall } from '@vizipp/video-call';
export const appConfig: ApplicationConfig = {
providers: [
provideVideoCall({
signalingUrl: 'https://your-signaling-server.com',
debug: true
})
]
};Component Usage (No Directives Needed)
import { Component, inject } from '@angular/core';
import { VideoCallService } from '@vizipp/video-call';
@Component({
selector: 'app-meeting',
standalone: true,
template: `
<div class="video-grid">
<!-- Local Stream -->
@if (localStream()) {
<video [srcObject]="localStream()" autoplay muted playsinline class="local-video"></video>
}
<!-- Remote Participants -->
@for (participant of participants(); track participant.id) {
<div class="tile">
@if (participant.videoStream) {
<video [srcObject]="participant.videoStream" autoplay playsinline></video>
} @else {
<div class="avatar">{{ participant.name[0] }}</div>
}
<span>{{ participant.name }}</span>
</div>
}
</div>
<!-- Controls -->
<button (click)="toggleAudio()">Mute/Unmute Audio</button>
<button (click)="toggleVideo()">Mute/Unmute Video</button>
<button (click)="leave()">Leave</button>
`
})
export class MeetingComponent {
private readonly call = inject(VideoCallService);
readonly roomState = this.call.getRoomState();
readonly participants = this.call.participants;
readonly localStream = this.call.localStream;
async join() {
await this.call.joinRoom({
roomId: 'room-101',
displayName: 'Alice'
});
}
toggleAudio() {
const currentState = this.roomState().isAudioEnabled;
return currentState ? this.call.muteAudio() : this.call.unmuteAudio();
}
toggleVideo() {
const currentState = this.roomState().isVideoEnabled;
return currentState ? this.call.muteVideo() : this.call.unmuteVideo();
}
leave() {
return this.call.leaveRoom();
}
}Server Protocol & Event Contract
If you are building your own backend server, your Socket.IO server must handle the following events and payloads:
Types Exported by @vizipp/video-call
Import server types directly in your Node.js TypeScript server:
import type {
JoinRoomResponse,
PeerJoinedEvent,
PeerLeftEvent,
NewProducerEvent,
ProducerToggledEvent,
ProducerClosedEvent,
ActiveSpeakerEvent,
ChatMessageEvent
} from '@vizipp/video-call';Client → Server Events
| Event Name | Parameters | Callback Response |
| :--- | :--- | :--- |
| joinRoom | { roomId: string, name: string } | { success: boolean, participantId: string, existingParticipants: ExistingParticipant[], error?: string } |
| getRouterRtpCapabilities | roomId: string | RtpCapabilities |
| createTransport | — | { id, iceParameters, iceCandidates, dtlsParameters } |
| connectTransport | { transportId, dtlsParameters } | { success: boolean, error?: string } |
| produce | { transportId, kind, rtpParameters, appData } | { id: string } |
| consume | { transportId, producerId, rtpCapabilities } | { id, producerId, rtpParameters, kind } |
| resumeConsumer | { consumerId } | { success: boolean } |
| toggleProducer | { producerId, paused } | { success: boolean } |
| chatMessage | { text: string } | { success: boolean } |
| leaveRoom | — | — |
Server → Client Events
| Event Name | Payload Shape | Description |
| :--- | :--- | :--- |
| peerJoined | { id: string, name: string } | Emitted to room when a new peer joins |
| peerLeft | { participantId: string } | Emitted to room when a peer disconnects/leaves |
| newProducer | { producerId, participantId, kind, appData } | Emitted when a peer starts audio/video |
| producerToggled | { participantId, producerId, kind, paused } | Emitted when a peer mutes/unmutes |
| producerClosed | { producerId, participantId, kind, appData } | Emitted when a producer stops |
| activeSpeaker | { participantId: string \| null } | Emitted by AudioLevelObserver |
| chatMessage | ChatMessageEvent | Emitted when a chat message is sent |
Server Starter Code (Node.js + Mediasoup + Socket.IO)
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import * as mediasoup from 'mediasoup';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });
let worker: mediasoup.types.Worker;
let router: mediasoup.types.Router;
async function start() {
worker = await mediasoup.createWorker({ rtcMinPort: 40000, rtcMaxPort: 49999 });
router = await worker.createRouter({
mediaCodecs: [
{ kind: 'audio', mimeType: 'audio/opus', clockRate: 48000, channels: 2 },
{ kind: 'video', mimeType: 'video/VP8', clockRate: 90000 }
]
});
io.on('connection', (socket) => {
socket.on('joinRoom', ({ roomId, name }, cb) => {
socket.join(roomId);
socket.data = { roomId, name };
cb({ success: true, participantId: socket.id, existingParticipants: [] });
socket.to(roomId).emit('peerJoined', { id: socket.id, name });
});
socket.on('getRouterRtpCapabilities', (roomId, cb) => cb(router.rtpCapabilities));
socket.on('createTransport', async (cb) => {
const transport = await router.createWebRtcTransport({
listenInfos: [{ protocol: 'udp', ip: '0.0.0.0', announcedIp: '127.0.0.1' }],
enableUdp: true,
enableTcp: true
});
cb({
id: transport.id,
iceParameters: transport.iceParameters,
iceCandidates: transport.iceCandidates,
dtlsParameters: transport.dtlsParameters
});
});
});
httpServer.listen(3011, () => console.log('Server running on port 3011'));
}
start();License
MIT © Vizipp
