callway 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +172 -0
- package/dist/index.d.ts +241 -0
- package/dist/index.js +726 -0
- package/dist/index.js.map +1 -0
- package/dist/mediaUtils-5gVEq26H.d.ts +49 -0
- package/dist/react/index.d.ts +87 -0
- package/dist/react/index.js +186 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Callway
|
|
2
|
+
|
|
3
|
+
A lightweight, framework-agnostic WebRTC engine with pluggable signaling. Zero runtime dependencies; you bring your own signaling backend.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
- Mesh-ready multi-peer engine with perfect-negotiation-style collision handling.
|
|
8
|
+
- Pluggable signaling adapters (interface-driven). Works with WebSocket, Firebase, Supabase, Redis, custom APIs, etc.
|
|
9
|
+
- Optional React hooks.
|
|
10
|
+
- TypeScript-first, ESM.
|
|
11
|
+
- No bundled runtime deps.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install callway
|
|
17
|
+
# optional React hooks
|
|
18
|
+
npm install callway react
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Core API (quick start, vanilla JS style)
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
import { PeerManager, MediaManager } from 'callway';
|
|
25
|
+
import { SignalingAdapter } from 'callway/src/signaling/SignalingAdapter'; // interface shape
|
|
26
|
+
|
|
27
|
+
// Minimal adapter (fill with your backend wiring: WebSocket/Firebase/etc.)
|
|
28
|
+
const signaling = {
|
|
29
|
+
registerPeer(peerId, handler, roomId) {
|
|
30
|
+
// TODO: subscribe to your backend and call handler(message) on incoming
|
|
31
|
+
},
|
|
32
|
+
unregisterPeer(peerId) {},
|
|
33
|
+
async sendMessage(message) {
|
|
34
|
+
// TODO: send via your backend transport
|
|
35
|
+
},
|
|
36
|
+
cleanup() {}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const peerManager = new PeerManager('peer-a');
|
|
40
|
+
const mediaManager = new MediaManager();
|
|
41
|
+
peerManager.setSignalingAdapter(signaling, 'room-1');
|
|
42
|
+
|
|
43
|
+
// Simple DOM helpers
|
|
44
|
+
const remoteContainer = document.getElementById('remotes');
|
|
45
|
+
function renderRemote(remoteId, stream) {
|
|
46
|
+
let video = remoteContainer.querySelector(`[data-peer="${remoteId}"]`);
|
|
47
|
+
if (!video) {
|
|
48
|
+
video = document.createElement('video');
|
|
49
|
+
video.setAttribute('data-peer', remoteId);
|
|
50
|
+
video.autoplay = true;
|
|
51
|
+
video.playsInline = true;
|
|
52
|
+
video.muted = false;
|
|
53
|
+
remoteContainer.appendChild(video);
|
|
54
|
+
}
|
|
55
|
+
video.srcObject = stream;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Handle remote media
|
|
59
|
+
peerManager.onRemoteStream((remoteStream, remoteId) => {
|
|
60
|
+
console.log('remote stream from', remoteId);
|
|
61
|
+
renderRemote(remoteId, remoteStream);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Get local media, attach, and start call
|
|
65
|
+
const localStream = await mediaManager.getUserMedia({ audio: true, video: true });
|
|
66
|
+
document.getElementById('local').srcObject = localStream;
|
|
67
|
+
|
|
68
|
+
// Add a peer and connect (assume peer-b exists on the same signaling backend)
|
|
69
|
+
await peerManager.addPeer('peer-b');
|
|
70
|
+
mediaManager.attachToPeer(peerManager, 'peer-b');
|
|
71
|
+
await peerManager.createOffer('peer-b');
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Signaling adapter interface
|
|
75
|
+
|
|
76
|
+
Implement to use any backend (WebSocket, Firebase, etc.):
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import type { SignalingAdapter } from 'callway';
|
|
80
|
+
|
|
81
|
+
interface SignalingAdapter {
|
|
82
|
+
registerPeer(peerId: string, handler: SignalingHandler, roomId?: string): void;
|
|
83
|
+
unregisterPeer(peerId: string): void;
|
|
84
|
+
sendMessage(message: SignalingMessage): Promise<void>;
|
|
85
|
+
broadcastToRoom?(from: string, roomId: string, messageType: string, data: any): Promise<void>;
|
|
86
|
+
cleanup(): void;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Swapping signaling backends
|
|
91
|
+
|
|
92
|
+
- Custom backend: implement `SignalingAdapter` and pass to `peerManager.setSignalingAdapter(adapter, roomId?)`.
|
|
93
|
+
- Example scaffolds (external; add deps yourself):
|
|
94
|
+
- `examples/FirebaseSignalingAdapter.ts`
|
|
95
|
+
- `examples/test-group-firebase.ts`
|
|
96
|
+
|
|
97
|
+
### Multi-peer (mesh) usage tips
|
|
98
|
+
|
|
99
|
+
- Each remote peer gets its own `RTCPeerConnection`.
|
|
100
|
+
- Lower peerId initiates offers; higher peerId acts “polite” and rolls back on collisions.
|
|
101
|
+
- ICE candidates are queued until remote descriptions are set; duplicates are ignored.
|
|
102
|
+
|
|
103
|
+
### React usage (optional)
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
107
|
+
import { PeerManager, MediaManager } from 'callway';
|
|
108
|
+
import type { SignalingAdapter } from 'callway/src/signaling/SignalingAdapter';
|
|
109
|
+
import { useIsCameraOff, useIsMicrophoneOff, useMediaState } from 'callway/react';
|
|
110
|
+
|
|
111
|
+
// Your adapter
|
|
112
|
+
class MySignalingAdapter implements SignalingAdapter {
|
|
113
|
+
registerPeer() {}
|
|
114
|
+
unregisterPeer() {}
|
|
115
|
+
async sendMessage() {}
|
|
116
|
+
cleanup() {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function CallApp() {
|
|
120
|
+
const [stream, setStream] = useState<MediaStream | null>(null);
|
|
121
|
+
const peerManager = useMemo(() => new PeerManager('peer-a'), []);
|
|
122
|
+
const mediaManager = useMemo(() => new MediaManager(), []);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const adapter = new MySignalingAdapter();
|
|
126
|
+
peerManager.setSignalingAdapter(adapter, 'room-1');
|
|
127
|
+
|
|
128
|
+
peerManager.onRemoteStream((remote, id) => {
|
|
129
|
+
console.log('remote', id, remote);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
mediaManager.getUserMedia({ audio: true, video: true })
|
|
133
|
+
.then((s) => {
|
|
134
|
+
setStream(s);
|
|
135
|
+
mediaManager.attachToPeer(peerManager, 'peer-b');
|
|
136
|
+
return peerManager.addPeer('peer-b');
|
|
137
|
+
})
|
|
138
|
+
.then(() => peerManager.createOffer('peer-b'))
|
|
139
|
+
.catch(console.error);
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
peerManager.cleanup();
|
|
143
|
+
mediaManager.cleanup();
|
|
144
|
+
adapter.cleanup();
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
|
|
148
|
+
const isCamOff = useIsCameraOff(stream);
|
|
149
|
+
const isMicOff = useIsMicrophoneOff(stream);
|
|
150
|
+
const state = useMediaState(stream);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div>
|
|
154
|
+
<div>Camera: {isCamOff ? 'Off' : 'On'}</div>
|
|
155
|
+
<div>Mic: {isMicOff ? 'Off' : 'On'}</div>
|
|
156
|
+
<div>Tracks: A{state.microphone.available ? 1 : 0} / V{state.camera.available ? 1 : 0}</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Production readiness
|
|
163
|
+
|
|
164
|
+
- Core engine: dependency-free, ESM, TypeScript, mesh-ready with perfect negotiation, ICE queuing, and collision handling.
|
|
165
|
+
- Signaling: pluggable; you must supply a production signaling adapter (WebSocket/Firebase/Supabase/custom).
|
|
166
|
+
- Media: uses native WebRTC (getUserMedia/RTCPeerConnection). No media servers included.
|
|
167
|
+
- Testing: use your own lightweight harness or the external examples; swap in your adapter for end-to-end testing.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
ISC
|
|
172
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
export { M as MediaState, g as getCameraTrack, c as getMediaState, b as getMicrophoneTrack, i as isCameraOff, a as isMicrophoneOff, o as observeMediaState } from './mediaUtils-5gVEq26H.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SignalingAdapter - abstraction layer for signaling backends.
|
|
5
|
+
* Implement this interface for any backend (WebSocket, Firebase, custom API).
|
|
6
|
+
*
|
|
7
|
+
* The package remains dependency-free; adapters are user-provided.
|
|
8
|
+
*/
|
|
9
|
+
interface SignalingAdapter {
|
|
10
|
+
registerPeer(peerId: string, handler: SignalingHandler, roomId?: string): void;
|
|
11
|
+
unregisterPeer(peerId: string): void;
|
|
12
|
+
sendMessage(message: SignalingMessage): Promise<void>;
|
|
13
|
+
broadcastToRoom?(from: string, roomId: string, messageType: string, data: any): Promise<void>;
|
|
14
|
+
cleanup(): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* PeerManager - Manages RTCPeerConnection instances for WebRTC calls
|
|
19
|
+
*
|
|
20
|
+
* Handles:
|
|
21
|
+
* - Creating and managing peer connections per remote participant
|
|
22
|
+
* - Creating offers and answers
|
|
23
|
+
* - Adding ICE candidates
|
|
24
|
+
* - Connection state management
|
|
25
|
+
*
|
|
26
|
+
* Supports both 1:1 and multi-peer/group calls.
|
|
27
|
+
* For 1:1 calls, use initialize() with a single remoteId.
|
|
28
|
+
* For group calls, use addPeer() to add multiple peers dynamically.
|
|
29
|
+
*/
|
|
30
|
+
interface PeerConnectionConfig {
|
|
31
|
+
iceServers?: RTCConfiguration['iceServers'];
|
|
32
|
+
}
|
|
33
|
+
interface SignalingMessage {
|
|
34
|
+
type: 'offer' | 'answer' | 'ice-candidate';
|
|
35
|
+
from: string;
|
|
36
|
+
to: string;
|
|
37
|
+
data: RTCSessionDescriptionInit | RTCIceCandidateInit;
|
|
38
|
+
}
|
|
39
|
+
type SignalingHandler = (message: SignalingMessage) => void | Promise<void>;
|
|
40
|
+
declare class PeerManager {
|
|
41
|
+
private peerConnections;
|
|
42
|
+
private peerConnection;
|
|
43
|
+
private remoteId;
|
|
44
|
+
private localId;
|
|
45
|
+
private signalingHandler;
|
|
46
|
+
private signalingAdapter;
|
|
47
|
+
private registeredInAdapter;
|
|
48
|
+
private config;
|
|
49
|
+
private remoteStreamCallback;
|
|
50
|
+
private connectionStateCallback;
|
|
51
|
+
private iceConnectionStateCallback;
|
|
52
|
+
constructor(localId: string, config?: PeerConnectionConfig);
|
|
53
|
+
/**
|
|
54
|
+
* Set the signaling handler for sending messages
|
|
55
|
+
*/
|
|
56
|
+
setSignalingHandler(handler: SignalingHandler): void;
|
|
57
|
+
/**
|
|
58
|
+
* Set a signaling adapter. This will register this peer and wire sending.
|
|
59
|
+
*/
|
|
60
|
+
setSignalingAdapter(adapter: SignalingAdapter, roomId?: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Create a new peer connection for a specific remote peer
|
|
63
|
+
*/
|
|
64
|
+
private createPeerConnection;
|
|
65
|
+
/**
|
|
66
|
+
* Initialize peer connection for a call (1:1 compatibility)
|
|
67
|
+
* For multi-peer calls, use addPeer() instead
|
|
68
|
+
*/
|
|
69
|
+
initialize(remoteId: string): Promise<void>;
|
|
70
|
+
/**
|
|
71
|
+
* Add a new peer to the room (multi-peer support)
|
|
72
|
+
* Creates a new RTCPeerConnection for this peer
|
|
73
|
+
*/
|
|
74
|
+
addPeer(remoteId: string, isInitiator?: boolean): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Remove a peer from the room
|
|
77
|
+
*/
|
|
78
|
+
removePeer(remoteId: string): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Get all remote peer IDs
|
|
81
|
+
*/
|
|
82
|
+
getRemotePeerIds(): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Check if a peer exists
|
|
85
|
+
*/
|
|
86
|
+
hasPeer(remoteId: string): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Get peer connection for a specific remote peer (multi-peer support)
|
|
89
|
+
*/
|
|
90
|
+
getPeerConnectionFor(remoteId: string): RTCPeerConnection | null;
|
|
91
|
+
/**
|
|
92
|
+
* Create an offer for a specific peer (caller side)
|
|
93
|
+
*/
|
|
94
|
+
createOffer(remoteId: string): Promise<RTCSessionDescriptionInit>;
|
|
95
|
+
/**
|
|
96
|
+
* Create offers for all peers (multi-peer support)
|
|
97
|
+
*/
|
|
98
|
+
createOffersForAll(): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Handle incoming offer from a peer (callee side)
|
|
101
|
+
*
|
|
102
|
+
* Collision strategy: the peer who already has a local offer ignores the
|
|
103
|
+
* incoming offer (impolite); with deterministic initiator selection in the
|
|
104
|
+
* caller, collisions should be rare. This keeps the signaling state valid.
|
|
105
|
+
*/
|
|
106
|
+
handleOffer(offer: RTCSessionDescriptionInit, remoteId: string): Promise<RTCSessionDescriptionInit | null>;
|
|
107
|
+
/**
|
|
108
|
+
* Handle incoming answer from a peer (caller side)
|
|
109
|
+
*/
|
|
110
|
+
handleAnswer(answer: RTCSessionDescriptionInit, remoteId: string): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Process queued ICE candidates for a peer
|
|
113
|
+
*/
|
|
114
|
+
private processIceCandidateQueue;
|
|
115
|
+
/**
|
|
116
|
+
* Add ICE candidate for a specific peer
|
|
117
|
+
* Queues candidates if remote description is not set yet
|
|
118
|
+
*/
|
|
119
|
+
addIceCandidate(candidate: RTCIceCandidateInit, remoteId: string): Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Add a media track to a specific peer connection
|
|
122
|
+
*/
|
|
123
|
+
addTrack(track: MediaStreamTrack, stream: MediaStream, remoteId?: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* Add media tracks to all peer connections (multi-peer support)
|
|
126
|
+
*/
|
|
127
|
+
addTrackToAll(track: MediaStreamTrack, stream: MediaStream): void;
|
|
128
|
+
/**
|
|
129
|
+
* Get the remote media stream for a specific peer
|
|
130
|
+
*/
|
|
131
|
+
getRemoteStream(remoteId: string): MediaStream | null;
|
|
132
|
+
/**
|
|
133
|
+
* Set handler for when remote stream is available
|
|
134
|
+
* Callback receives (stream, remoteId) for multi-peer support
|
|
135
|
+
* Can be called before or after initialization
|
|
136
|
+
*/
|
|
137
|
+
onRemoteStream(callback: (stream: MediaStream, remoteId: string) => void): void;
|
|
138
|
+
/**
|
|
139
|
+
* Set handler for connection state changes
|
|
140
|
+
* Callback receives (state, remoteId) for multi-peer support
|
|
141
|
+
*/
|
|
142
|
+
onConnectionStateChange(callback: (state: RTCPeerConnectionState, remoteId: string) => void): void;
|
|
143
|
+
/**
|
|
144
|
+
* Set handler for ICE connection state changes
|
|
145
|
+
* Callback receives (state, remoteId) for multi-peer support
|
|
146
|
+
*/
|
|
147
|
+
onIceConnectionStateChange(callback: (state: RTCIceConnectionState, remoteId: string) => void): void;
|
|
148
|
+
/**
|
|
149
|
+
* Get the peer connection instance (legacy 1:1 support)
|
|
150
|
+
*/
|
|
151
|
+
getPeerConnection(): RTCPeerConnection | null;
|
|
152
|
+
/**
|
|
153
|
+
* Cleanup and close all peer connections
|
|
154
|
+
*/
|
|
155
|
+
cleanup(): Promise<void>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* MediaManager - Manages local media streams (getUserMedia)
|
|
160
|
+
*
|
|
161
|
+
* Handles:
|
|
162
|
+
* - Requesting user media (audio/video)
|
|
163
|
+
* - Managing local media streams
|
|
164
|
+
* - Attaching streams to peer connections
|
|
165
|
+
*
|
|
166
|
+
* Currently supports single stream management.
|
|
167
|
+
* TODO: Extend for multiple streams or screen sharing in the future.
|
|
168
|
+
*/
|
|
169
|
+
interface MediaConstraints {
|
|
170
|
+
audio?: boolean | MediaTrackConstraints;
|
|
171
|
+
video?: boolean | MediaTrackConstraints;
|
|
172
|
+
}
|
|
173
|
+
declare class MediaManager {
|
|
174
|
+
private localStream;
|
|
175
|
+
/**
|
|
176
|
+
* Request user media (audio and/or video)
|
|
177
|
+
*/
|
|
178
|
+
getUserMedia(constraints?: MediaConstraints): Promise<MediaStream>;
|
|
179
|
+
/**
|
|
180
|
+
* Get the current local stream
|
|
181
|
+
*/
|
|
182
|
+
getLocalStream(): MediaStream | null;
|
|
183
|
+
/**
|
|
184
|
+
* Stop all tracks in the local stream
|
|
185
|
+
*/
|
|
186
|
+
stopLocalStream(): void;
|
|
187
|
+
/**
|
|
188
|
+
* Attach local stream to a peer connection (1:1 or specific peer)
|
|
189
|
+
* This adds all tracks from the local stream to the peer connection
|
|
190
|
+
*/
|
|
191
|
+
attachToPeer(peerManager: PeerManager, remoteId?: string): void;
|
|
192
|
+
/**
|
|
193
|
+
* Attach local stream to all peers in a room (multi-peer support)
|
|
194
|
+
* This adds all tracks from the local stream to all peer connections
|
|
195
|
+
*/
|
|
196
|
+
attachToAllPeers(peerManager: PeerManager): void;
|
|
197
|
+
/**
|
|
198
|
+
* Check if camera (video) is off/muted
|
|
199
|
+
* Returns true if camera is off, false if on
|
|
200
|
+
*/
|
|
201
|
+
isCameraOff(): boolean;
|
|
202
|
+
/**
|
|
203
|
+
* Check if microphone (audio) is off/muted
|
|
204
|
+
* Returns true if microphone is off, false if on
|
|
205
|
+
*/
|
|
206
|
+
isMicrophoneOff(): boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Get camera track state
|
|
209
|
+
* Returns the first video track or null
|
|
210
|
+
*/
|
|
211
|
+
getCameraTrack(): MediaStreamTrack | null;
|
|
212
|
+
/**
|
|
213
|
+
* Get microphone track state
|
|
214
|
+
* Returns the first audio track or null
|
|
215
|
+
*/
|
|
216
|
+
getMicrophoneTrack(): MediaStreamTrack | null;
|
|
217
|
+
/**
|
|
218
|
+
* Get detailed media state information
|
|
219
|
+
*/
|
|
220
|
+
getMediaState(): {
|
|
221
|
+
hasStream: boolean;
|
|
222
|
+
camera: {
|
|
223
|
+
available: boolean;
|
|
224
|
+
enabled: boolean;
|
|
225
|
+
muted: boolean;
|
|
226
|
+
readyState: MediaStreamTrackState | null;
|
|
227
|
+
};
|
|
228
|
+
microphone: {
|
|
229
|
+
available: boolean;
|
|
230
|
+
enabled: boolean;
|
|
231
|
+
muted: boolean;
|
|
232
|
+
readyState: MediaStreamTrackState | null;
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Cleanup - stop all tracks
|
|
237
|
+
*/
|
|
238
|
+
cleanup(): void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export { type MediaConstraints, MediaManager, type PeerConnectionConfig, PeerManager, type SignalingAdapter, type SignalingHandler, type SignalingMessage };
|