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 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
+
@@ -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 };