covekit 0.1.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 +139 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +197 -0
- package/dist/protocol.d.ts +123 -0
- package/dist/protocol.js +1 -0
- package/dist/room.d.ts +65 -0
- package/dist/room.js +386 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# covekit
|
|
2
|
+
|
|
3
|
+
The official JavaScript/TypeScript SDK for the **CoveKit Media Suite**. This package provides headless WebRTC meeting orchestration, media transport handshakes, and signaling channel loops.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install covekit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Integration Paths
|
|
16
|
+
|
|
17
|
+
Developers can integrate CoveKit in two ways:
|
|
18
|
+
|
|
19
|
+
1. **Option A: Direct Embed (Low-Code / Complete UI)**: Embed our pre-built responsive UI widget inside an iframe.
|
|
20
|
+
2. **Option B: Headless SDK (Pro-Code / Custom UI)**: Orchestrate WebRTC connections and media tracks programmatically to build a custom interface.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Option A: Direct Embed
|
|
25
|
+
|
|
26
|
+
Render an `<iframe>` referencing the CoveKit meeting URL with specific query parameters to hide standard menus/headers and automatically enter the room:
|
|
27
|
+
|
|
28
|
+
```html
|
|
29
|
+
<iframe
|
|
30
|
+
src="https://meet.covekit.com/m/lobby?embed=true&autoJoin=true&displayName=Satoshi&mic=on&cam=off"
|
|
31
|
+
allow="camera; microphone; display-capture; fullscreen"
|
|
32
|
+
style="width: 100%; height: 600px; border: none; border-radius: 8px;"
|
|
33
|
+
></iframe>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
#### Supported Query Parameters:
|
|
37
|
+
* `embed=true` (Required): Strips out header spacing, navigation menus, and lobby wrappers.
|
|
38
|
+
* `autoJoin=true` (Optional): Skips the lobby settings stage and joins automatically.
|
|
39
|
+
* `displayName` (Optional): Prepopulates the user's name (required if `autoJoin` is active).
|
|
40
|
+
* `passcode` (Optional): Prepopulates the password.
|
|
41
|
+
* `mic=on|off` (Optional): Defaults the starting microphone state.
|
|
42
|
+
* `cam=on|off` (Optional): Defaults the starting camera state.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Option B: Headless SDK
|
|
47
|
+
|
|
48
|
+
Orchestrate the WebRTC media transports programmatically to build a custom UI.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { CoveKitClient, CoveKitRoom } from 'covekit';
|
|
52
|
+
|
|
53
|
+
// Initialize the client with your custom API and Media server endpoints
|
|
54
|
+
const client = new CoveKitClient(
|
|
55
|
+
'https://api.covekit.com', // Control Plane API
|
|
56
|
+
'wss://media.covekit.com/v1/media/ws' // Real-Time Signaling WS
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 1. Get meeting token
|
|
60
|
+
const session = await client.joinPublicMeeting('lobby-room', 'Alice');
|
|
61
|
+
|
|
62
|
+
// 2. Initialize connection
|
|
63
|
+
const room = new CoveKitRoom(client, {
|
|
64
|
+
onConnected: (info) => {
|
|
65
|
+
console.log('Connected! Session ID:', info.sessionId);
|
|
66
|
+
// Request permission & publish mic/cam tracks
|
|
67
|
+
room.publishLocalMedia(true, true);
|
|
68
|
+
},
|
|
69
|
+
onLocalStream: (stream) => {
|
|
70
|
+
// Render local preview player
|
|
71
|
+
const localVideo = document.getElementById('local-video') as HTMLVideoElement;
|
|
72
|
+
localVideo.srcObject = stream;
|
|
73
|
+
},
|
|
74
|
+
onRemoteTrack: (track, peerId, kind) => {
|
|
75
|
+
// Handle remote participant video/audio tracks
|
|
76
|
+
const mediaStream = new MediaStream([track]);
|
|
77
|
+
const remoteVideo = document.getElementById(`peer-${peerId}`) as HTMLVideoElement;
|
|
78
|
+
if (remoteVideo) {
|
|
79
|
+
remoteVideo.srcObject = mediaStream;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
onPeerJoin: (peer) => {
|
|
83
|
+
console.log(`${peer.displayName} joined the call`);
|
|
84
|
+
},
|
|
85
|
+
onPeerLeave: (peerId) => {
|
|
86
|
+
console.log(`Peer ${peerId} left`);
|
|
87
|
+
},
|
|
88
|
+
onPeerMuteStateChanged: (peerId, kind, muted) => {
|
|
89
|
+
console.log(`Peer ${peerId} ${kind} is ${muted ? 'muted' : 'unmuted'}`);
|
|
90
|
+
},
|
|
91
|
+
onWhiteboardUpdate: (peerId, elements) => {
|
|
92
|
+
console.log('Received whiteboard vector sync chunk:', elements);
|
|
93
|
+
},
|
|
94
|
+
onError: (error) => {
|
|
95
|
+
console.error('Room signaling error:', error);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// 3. Connect WebRTC media loops
|
|
100
|
+
await room.join(session.token, session.ice_servers);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## API Reference
|
|
106
|
+
|
|
107
|
+
### `CoveKitClient`
|
|
108
|
+
Main client class for talking to the CoveKit Control Plane.
|
|
109
|
+
|
|
110
|
+
#### Constructor
|
|
111
|
+
```typescript
|
|
112
|
+
new CoveKitClient(apiBaseUrl: string, mediaWsUrl: string)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Methods
|
|
116
|
+
* `joinPublicMeeting(publicId: string, displayName: string, passcode?: string, hostToken?: string)`: Establishes a session token and connects to waiting rooms.
|
|
117
|
+
* `getPublicMeeting(publicId: string)`: Retrieves meeting metadata (e.g. title, status, meeting mode).
|
|
118
|
+
* `pollSessionStatus(publicId: string, sessionId: string)`: Checks if the host has admitted the user from the waiting room.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
### `CoveKitRoom`
|
|
123
|
+
Handles room-wide media track lifecycle events and WebRTC connection pipelines.
|
|
124
|
+
|
|
125
|
+
#### Constructor
|
|
126
|
+
```typescript
|
|
127
|
+
new CoveKitRoom(client: CoveKitClient, events: CoveKitRoomEvents)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Methods
|
|
131
|
+
* `join(token: string, iceServers?: any[])`: Connects the WebRTC signaling WebSocket.
|
|
132
|
+
* `leave()`: Stops all local streams, closes active producers, and tears down signaling.
|
|
133
|
+
* `publishLocalMedia(audio: boolean, video: boolean, audioDeviceId?: string, existingStream?: MediaStream)`: Invokes the user's browser device permissions and publishes selected tracks.
|
|
134
|
+
* `publishTrack(kind: 'audio' | 'video', track: MediaStreamTrack, appData?: any)`: Publishes a custom track.
|
|
135
|
+
* `replaceTrack(kind: 'audio' | 'video', track: MediaStreamTrack | null, appData?: any)`: Swaps an active track in-place on the current WebRTC producer.
|
|
136
|
+
* `toggleMute(kind: 'audio' | 'video', muted: boolean)`: Mutes/unmutes media tracks and broadcasts state to peers.
|
|
137
|
+
* `sendWhiteboardUpdate(elements: any)`: Broadcasts real-time collaborative whiteboard updates to peers.
|
|
138
|
+
* `kickParticipant(peerId: string)`: *[Host Only]* Evicts a participant from the room.
|
|
139
|
+
* `endMeetingRoom()`: *[Host Only]* Terminates the meeting session for everyone.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ServerMessage } from './protocol';
|
|
2
|
+
export declare class CoveKitClient {
|
|
3
|
+
private apiBaseUrl;
|
|
4
|
+
private mediaWsUrl;
|
|
5
|
+
private ws;
|
|
6
|
+
constructor(apiBaseUrl: string, mediaWsUrl: string);
|
|
7
|
+
/**
|
|
8
|
+
* Control Plane REST: Fetch public meeting minimal info
|
|
9
|
+
*/
|
|
10
|
+
getPublicMeeting(publicId: string): Promise<any>;
|
|
11
|
+
/**
|
|
12
|
+
* Control Plane REST: Creates an ephemeral anonymous participant session
|
|
13
|
+
*/
|
|
14
|
+
joinPublicMeeting(publicId: string, displayName: string, passcode?: string, hostToken?: string): Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
* Control Plane REST: Polls session status for waiting room approval
|
|
17
|
+
*/
|
|
18
|
+
pollSessionStatus(publicId: string, sessionId: string): Promise<any>;
|
|
19
|
+
/**
|
|
20
|
+
* Control Plane REST: Fetch waiting room participants (Host only)
|
|
21
|
+
*/
|
|
22
|
+
getWaitingRoom(publicId: string, hostToken: string): Promise<any>;
|
|
23
|
+
/**
|
|
24
|
+
* Control Plane REST: Admit participant (Host only)
|
|
25
|
+
*/
|
|
26
|
+
admitParticipant(publicId: string, sessionId: string, hostToken: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Control Plane REST: Deny participant (Host only)
|
|
29
|
+
*/
|
|
30
|
+
denyParticipant(publicId: string, sessionId: string, hostToken: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Real-Time Plane WS: Initiates native signaling channel loops
|
|
33
|
+
*/
|
|
34
|
+
connectMediaServer(token: string, onMessage: (msg: ServerMessage) => void): Promise<WebSocket>;
|
|
35
|
+
private sendSignal;
|
|
36
|
+
requestCapabilities(): void;
|
|
37
|
+
createWebRtcTransport(direction: 'send' | 'recv'): void;
|
|
38
|
+
connectWebRtcTransport(transportId: string, dtlsParameters: any): void;
|
|
39
|
+
produce(transportId: string, kind: 'audio' | 'video', rtpParameters: any, appData?: any): void;
|
|
40
|
+
consume(producerId: string, rtpCapabilities: any): void;
|
|
41
|
+
grantScreenShare(peerId: string): void;
|
|
42
|
+
revokeScreenShare(peerId: string): void;
|
|
43
|
+
closeProducer(producerId: string): void;
|
|
44
|
+
pauseConsumer(consumerId: string): void;
|
|
45
|
+
resumeConsumer(consumerId: string): void;
|
|
46
|
+
endMeetingRoom(): void;
|
|
47
|
+
kickParticipant(peerId: string): void;
|
|
48
|
+
toggleMute(kind: 'audio' | 'video', muted: boolean): void;
|
|
49
|
+
sendTranscriptChunk(text: string): void;
|
|
50
|
+
sendWhiteboardUpdate(elements: any): void;
|
|
51
|
+
sendPing(): void;
|
|
52
|
+
disconnect(): void;
|
|
53
|
+
}
|
|
54
|
+
export { CoveKitRoom, CoveKitRoomEvents } from './room';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rust serde's `rename_all = "camelCase"` on an enum only renames variant
|
|
3
|
+
* names, NOT the field names within struct variants. The server therefore
|
|
4
|
+
* sends snake_case keys inside every payload object (e.g. `rtp_capabilities`,
|
|
5
|
+
* `session_id`, `ice_parameters`). This helper does a shallow conversion so
|
|
6
|
+
* the rest of the client can use the camelCase names defined in protocol.ts.
|
|
7
|
+
*/
|
|
8
|
+
function shallowCamelizeKeys(obj) {
|
|
9
|
+
const out = {};
|
|
10
|
+
for (const key of Object.keys(obj)) {
|
|
11
|
+
const camel = key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase());
|
|
12
|
+
out[camel] = obj[key];
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export class CoveKitClient {
|
|
17
|
+
constructor(apiBaseUrl, mediaWsUrl) {
|
|
18
|
+
this.ws = null;
|
|
19
|
+
this.apiBaseUrl = apiBaseUrl;
|
|
20
|
+
this.mediaWsUrl = mediaWsUrl;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Control Plane REST: Fetch public meeting minimal info
|
|
24
|
+
*/
|
|
25
|
+
async getPublicMeeting(publicId) {
|
|
26
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/public/meetings/${publicId}`, {
|
|
27
|
+
method: 'GET',
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Failed to fetch public meeting details: ${response.statusText}`);
|
|
31
|
+
}
|
|
32
|
+
return response.json();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Control Plane REST: Creates an ephemeral anonymous participant session
|
|
36
|
+
*/
|
|
37
|
+
async joinPublicMeeting(publicId, displayName, passcode, hostToken) {
|
|
38
|
+
const url = new URL(`${this.apiBaseUrl}/v1/public/meetings/${publicId}/join`);
|
|
39
|
+
if (hostToken) {
|
|
40
|
+
url.searchParams.set('host_token', hostToken);
|
|
41
|
+
}
|
|
42
|
+
const response = await fetch(url.toString(), {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ display_name: displayName, passcode }),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errPayload = await response.json().catch(() => ({}));
|
|
49
|
+
throw new Error(errPayload.error || `Failed public room entry: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
// Returns payload containing short-lived JWT Join Token
|
|
52
|
+
return response.json();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Control Plane REST: Polls session status for waiting room approval
|
|
56
|
+
*/
|
|
57
|
+
async pollSessionStatus(publicId, sessionId) {
|
|
58
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/public/meetings/${publicId}/session/${sessionId}/status`, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
const errPayload = await response.json().catch(() => ({}));
|
|
63
|
+
throw new Error(errPayload.error || `Failed to check session status: ${response.statusText}`);
|
|
64
|
+
}
|
|
65
|
+
return response.json();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Control Plane REST: Fetch waiting room participants (Host only)
|
|
69
|
+
*/
|
|
70
|
+
async getWaitingRoom(publicId, hostToken) {
|
|
71
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/public/meetings/${publicId}/waiting-room?host_token=${hostToken}`, {
|
|
72
|
+
method: 'GET',
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
throw new Error(`Failed to fetch waiting room: ${response.statusText}`);
|
|
76
|
+
return response.json();
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Control Plane REST: Admit participant (Host only)
|
|
80
|
+
*/
|
|
81
|
+
async admitParticipant(publicId, sessionId, hostToken) {
|
|
82
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/public/meetings/${publicId}/waiting-room/${sessionId}/admit?host_token=${hostToken}`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok)
|
|
86
|
+
throw new Error(`Failed to admit participant: ${response.statusText}`);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Control Plane REST: Deny participant (Host only)
|
|
90
|
+
*/
|
|
91
|
+
async denyParticipant(publicId, sessionId, hostToken) {
|
|
92
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/public/meetings/${publicId}/waiting-room/${sessionId}/deny?host_token=${hostToken}`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
});
|
|
95
|
+
if (!response.ok)
|
|
96
|
+
throw new Error(`Failed to deny participant: ${response.statusText}`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Real-Time Plane WS: Initiates native signaling channel loops
|
|
100
|
+
*/
|
|
101
|
+
connectMediaServer(token, onMessage) {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.ws = new WebSocket(this.mediaWsUrl);
|
|
104
|
+
this.ws.onopen = () => {
|
|
105
|
+
console.log('Signaling pipe open. Launching cryptographic token validation handshake.');
|
|
106
|
+
this.sendSignal('init', { token });
|
|
107
|
+
resolve(this.ws);
|
|
108
|
+
};
|
|
109
|
+
this.ws.onmessage = (event) => {
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(event.data);
|
|
112
|
+
// Shallow-camelize the payload so snake_case Rust field names
|
|
113
|
+
// (rtp_capabilities, session_id, …) map to the camelCase names
|
|
114
|
+
// used in protocol.ts and room.ts.
|
|
115
|
+
const camelPayload = parsed.payload && typeof parsed.payload === 'object'
|
|
116
|
+
? shallowCamelizeKeys(parsed.payload)
|
|
117
|
+
: null;
|
|
118
|
+
const flatMsg = camelPayload
|
|
119
|
+
? { type: parsed.type, ...camelPayload }
|
|
120
|
+
: { type: parsed.type };
|
|
121
|
+
onMessage(flatMsg);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
console.error('Failed processing raw incoming WebSocket event frame:', err);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
this.ws.onerror = (err) => reject(err);
|
|
128
|
+
this.ws.onclose = () => {
|
|
129
|
+
console.log('Signaling WebSocket closed.');
|
|
130
|
+
// If this.ws is still set, it means we didn't call .disconnect() manually
|
|
131
|
+
if (this.ws) {
|
|
132
|
+
onMessage({ type: 'error', message: 'Connection to server lost.' });
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
sendSignal(type, payload) {
|
|
138
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
139
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
requestCapabilities() {
|
|
143
|
+
this.sendSignal('getRouterRtpCapabilities');
|
|
144
|
+
}
|
|
145
|
+
createWebRtcTransport(direction) {
|
|
146
|
+
this.sendSignal('createWebRtcTransport', { direction });
|
|
147
|
+
}
|
|
148
|
+
connectWebRtcTransport(transportId, dtlsParameters) {
|
|
149
|
+
this.sendSignal('connectWebRtcTransport', { transportId, dtlsParameters });
|
|
150
|
+
}
|
|
151
|
+
produce(transportId, kind, rtpParameters, appData) {
|
|
152
|
+
this.sendSignal('produce', { transportId, kind, rtpParameters, appData });
|
|
153
|
+
}
|
|
154
|
+
consume(producerId, rtpCapabilities) {
|
|
155
|
+
this.sendSignal('consume', { producerId, rtpCapabilities });
|
|
156
|
+
}
|
|
157
|
+
grantScreenShare(peerId) {
|
|
158
|
+
this.sendSignal('grantScreenShare', { peerId });
|
|
159
|
+
}
|
|
160
|
+
revokeScreenShare(peerId) {
|
|
161
|
+
this.sendSignal('revokeScreenShare', { peerId });
|
|
162
|
+
}
|
|
163
|
+
closeProducer(producerId) {
|
|
164
|
+
this.sendSignal('closeProducer', { producerId });
|
|
165
|
+
}
|
|
166
|
+
pauseConsumer(consumerId) {
|
|
167
|
+
this.sendSignal('pauseConsumer', { consumerId });
|
|
168
|
+
}
|
|
169
|
+
resumeConsumer(consumerId) {
|
|
170
|
+
this.sendSignal('resumeConsumer', { consumerId });
|
|
171
|
+
}
|
|
172
|
+
endMeetingRoom() {
|
|
173
|
+
this.sendSignal('endMeeting');
|
|
174
|
+
}
|
|
175
|
+
kickParticipant(peerId) {
|
|
176
|
+
this.sendSignal('kickPeer', { peerId });
|
|
177
|
+
}
|
|
178
|
+
toggleMute(kind, muted) {
|
|
179
|
+
this.sendSignal('toggleMute', { kind, muted });
|
|
180
|
+
}
|
|
181
|
+
sendTranscriptChunk(text) {
|
|
182
|
+
this.sendSignal('transcriptChunk', { text });
|
|
183
|
+
}
|
|
184
|
+
sendWhiteboardUpdate(elements) {
|
|
185
|
+
this.sendSignal('whiteboardUpdate', { elements });
|
|
186
|
+
}
|
|
187
|
+
sendPing() {
|
|
188
|
+
this.sendSignal('ping');
|
|
189
|
+
}
|
|
190
|
+
disconnect() {
|
|
191
|
+
if (this.ws) {
|
|
192
|
+
this.ws.close();
|
|
193
|
+
this.ws = null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
export { CoveKitRoom } from './room';
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type ClientMessage = {
|
|
2
|
+
type: 'init';
|
|
3
|
+
token: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'getRouterRtpCapabilities';
|
|
6
|
+
} | {
|
|
7
|
+
type: 'createWebRtcTransport';
|
|
8
|
+
direction: 'send' | 'recv';
|
|
9
|
+
} | {
|
|
10
|
+
type: 'connectWebRtcTransport';
|
|
11
|
+
transportId: string;
|
|
12
|
+
dtlsParameters: any;
|
|
13
|
+
} | {
|
|
14
|
+
type: 'produce';
|
|
15
|
+
transportId: string;
|
|
16
|
+
kind: 'audio' | 'video';
|
|
17
|
+
rtpParameters: any;
|
|
18
|
+
appData?: any;
|
|
19
|
+
} | {
|
|
20
|
+
type: 'consume';
|
|
21
|
+
producerId: string;
|
|
22
|
+
rtpCapabilities: any;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'endMeeting';
|
|
25
|
+
} | {
|
|
26
|
+
type: 'kickPeer';
|
|
27
|
+
peerId: string;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'toggleMute';
|
|
30
|
+
kind: 'audio' | 'video';
|
|
31
|
+
muted: boolean;
|
|
32
|
+
} | {
|
|
33
|
+
type: 'grantScreenShare';
|
|
34
|
+
peerId: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: 'revokeScreenShare';
|
|
37
|
+
peerId: string;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'closeProducer';
|
|
40
|
+
producerId: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'pauseConsumer';
|
|
43
|
+
consumerId: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'resumeConsumer';
|
|
46
|
+
consumerId: string;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'transcriptChunk';
|
|
49
|
+
text: string;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'whiteboardUpdate';
|
|
52
|
+
elements: any;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'ping';
|
|
55
|
+
};
|
|
56
|
+
export type ServerMessage = {
|
|
57
|
+
type: 'connected';
|
|
58
|
+
sessionId: string;
|
|
59
|
+
role: 'host' | 'participant';
|
|
60
|
+
displayName: string;
|
|
61
|
+
isRecording: boolean;
|
|
62
|
+
autoRecord: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
type: 'routerRtpCapabilities';
|
|
65
|
+
rtpCapabilities: any;
|
|
66
|
+
} | {
|
|
67
|
+
type: 'transportCreated';
|
|
68
|
+
id: string;
|
|
69
|
+
direction: 'send' | 'recv';
|
|
70
|
+
iceParameters: any;
|
|
71
|
+
iceCandidates: any[];
|
|
72
|
+
dtlsParameters: any;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'producerCreated';
|
|
75
|
+
id: string;
|
|
76
|
+
} | {
|
|
77
|
+
type: 'consumerCreated';
|
|
78
|
+
id: string;
|
|
79
|
+
producerId: string;
|
|
80
|
+
kind: 'audio' | 'video';
|
|
81
|
+
rtpParameters: any;
|
|
82
|
+
appData?: any;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'newProducer';
|
|
85
|
+
producerId: string;
|
|
86
|
+
peerId: string;
|
|
87
|
+
kind: 'audio' | 'video';
|
|
88
|
+
appData?: any;
|
|
89
|
+
} | {
|
|
90
|
+
type: 'producerClosed';
|
|
91
|
+
producerId: string;
|
|
92
|
+
peerId: string;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'peerJoined';
|
|
95
|
+
peerId: string;
|
|
96
|
+
displayName: string;
|
|
97
|
+
role: 'host' | 'participant';
|
|
98
|
+
} | {
|
|
99
|
+
type: 'peerLeft';
|
|
100
|
+
peerId: string;
|
|
101
|
+
} | {
|
|
102
|
+
type: 'peerMuteStateChanged';
|
|
103
|
+
peerId: string;
|
|
104
|
+
kind: 'audio' | 'video';
|
|
105
|
+
muted: boolean;
|
|
106
|
+
} | {
|
|
107
|
+
type: 'screenSharePermissionUpdated';
|
|
108
|
+
peerId: string;
|
|
109
|
+
canShare: boolean;
|
|
110
|
+
} | {
|
|
111
|
+
type: 'recordingStarted';
|
|
112
|
+
} | {
|
|
113
|
+
type: 'recordingStopped';
|
|
114
|
+
} | {
|
|
115
|
+
type: 'whiteboardUpdate';
|
|
116
|
+
peerId: string;
|
|
117
|
+
elements: any;
|
|
118
|
+
} | {
|
|
119
|
+
type: 'error';
|
|
120
|
+
message: string;
|
|
121
|
+
} | {
|
|
122
|
+
type: 'pong';
|
|
123
|
+
};
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/room.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { CoveKitClient } from './index';
|
|
2
|
+
export interface CoveKitRoomEvents {
|
|
3
|
+
onConnected?: (info: {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
role: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
isRecording: boolean;
|
|
8
|
+
autoRecord: boolean;
|
|
9
|
+
}) => void;
|
|
10
|
+
onLocalStream?: (stream: MediaStream) => void;
|
|
11
|
+
onRemoteTrack?: (track: MediaStreamTrack, peerId: string, kind: 'audio' | 'video', appData?: any) => void;
|
|
12
|
+
onPeerJoin?: (peer: {
|
|
13
|
+
peerId: string;
|
|
14
|
+
displayName: string;
|
|
15
|
+
role: string;
|
|
16
|
+
}) => void;
|
|
17
|
+
onPeerLeave?: (peerId: string) => void;
|
|
18
|
+
onPeerMuteStateChanged?: (peerId: string, kind: 'audio' | 'video', muted: boolean) => void;
|
|
19
|
+
onScreenSharePermissionUpdated?: (peerId: string, canShare: boolean) => void;
|
|
20
|
+
onRecordingStarted?: () => void;
|
|
21
|
+
onRecordingStopped?: () => void;
|
|
22
|
+
onWhiteboardUpdate?: (peerId: string, elements: any) => void;
|
|
23
|
+
onError?: (err: Error) => void;
|
|
24
|
+
}
|
|
25
|
+
export declare class CoveKitRoom {
|
|
26
|
+
private client;
|
|
27
|
+
private device;
|
|
28
|
+
private sendTransport;
|
|
29
|
+
private recvTransport;
|
|
30
|
+
private producers;
|
|
31
|
+
private consumers;
|
|
32
|
+
private producerToPeerMap;
|
|
33
|
+
private events;
|
|
34
|
+
private pendingProduceCallback;
|
|
35
|
+
private pendingPublishAudio;
|
|
36
|
+
private pendingPublishVideo;
|
|
37
|
+
private pendingPublishAudioDeviceId;
|
|
38
|
+
private pendingExistingStream;
|
|
39
|
+
private transportsReady;
|
|
40
|
+
private pendingConsumes;
|
|
41
|
+
private iceServers?;
|
|
42
|
+
private pingInterval;
|
|
43
|
+
constructor(client: CoveKitClient, events: CoveKitRoomEvents);
|
|
44
|
+
join(token: string, iceServers?: any[]): Promise<void>;
|
|
45
|
+
publishLocalMedia(audio: boolean, video: boolean, audioDeviceId?: string, existingStream?: MediaStream): Promise<void>;
|
|
46
|
+
private handleSignal;
|
|
47
|
+
private initializeTransport;
|
|
48
|
+
private consumeMutex;
|
|
49
|
+
private initializeConsumer;
|
|
50
|
+
endMeeting(): void;
|
|
51
|
+
sendTranscriptChunk(text: string): void;
|
|
52
|
+
sendWhiteboardUpdate(elements: any): void;
|
|
53
|
+
kickParticipant(peerId: string): void;
|
|
54
|
+
toggleMute(kind: 'audio' | 'video', muted: boolean): void;
|
|
55
|
+
publishTrack(kind: 'audio' | 'video', track: MediaStreamTrack, appData?: any): Promise<void>;
|
|
56
|
+
replaceTrack(kind: 'audio' | 'video', track: MediaStreamTrack | null, appData?: any): Promise<void>;
|
|
57
|
+
unpublishTrack(kind: 'audio' | 'video', appData?: any): Promise<void>;
|
|
58
|
+
closeProducer(producerId: string): void;
|
|
59
|
+
grantScreenShare(peerId: string): void;
|
|
60
|
+
revokeScreenShare(peerId: string): void;
|
|
61
|
+
pausePeerVideo(peerId: string): void;
|
|
62
|
+
resumePeerVideo(peerId: string): void;
|
|
63
|
+
resumeAllVideos(): void;
|
|
64
|
+
leave(): void;
|
|
65
|
+
}
|
package/dist/room.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { Device } from 'mediasoup-client';
|
|
2
|
+
export class CoveKitRoom {
|
|
3
|
+
constructor(client, events) {
|
|
4
|
+
this.device = null;
|
|
5
|
+
this.sendTransport = null;
|
|
6
|
+
this.recvTransport = null;
|
|
7
|
+
this.producers = new Map();
|
|
8
|
+
this.consumers = new Map();
|
|
9
|
+
this.producerToPeerMap = new Map();
|
|
10
|
+
this.pendingProduceCallback = null;
|
|
11
|
+
this.pendingPublishAudio = false;
|
|
12
|
+
this.pendingPublishVideo = false;
|
|
13
|
+
this.pendingPublishAudioDeviceId = undefined;
|
|
14
|
+
this.pendingExistingStream = undefined;
|
|
15
|
+
this.transportsReady = { send: false, recv: false };
|
|
16
|
+
// newProducer events that arrive before the device/transports are ready
|
|
17
|
+
// are queued here and flushed once both transports are initialized.
|
|
18
|
+
this.pendingConsumes = [];
|
|
19
|
+
this.pingInterval = null;
|
|
20
|
+
this.consumeMutex = Promise.resolve();
|
|
21
|
+
this.client = client;
|
|
22
|
+
this.events = events;
|
|
23
|
+
}
|
|
24
|
+
async join(token, iceServers) {
|
|
25
|
+
try {
|
|
26
|
+
this.iceServers = iceServers;
|
|
27
|
+
this.device = new Device();
|
|
28
|
+
// Connect to WebSocket signaling server
|
|
29
|
+
await this.client.connectMediaServer(token, (msg) => this.handleSignal(msg));
|
|
30
|
+
// Start heartbeat every 15 seconds
|
|
31
|
+
this.pingInterval = window.setInterval(() => {
|
|
32
|
+
this.client.sendPing();
|
|
33
|
+
}, 15000);
|
|
34
|
+
// Request Router RTP capabilities
|
|
35
|
+
this.client.requestCapabilities();
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
this.events.onError?.(err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async publishLocalMedia(audio, video, audioDeviceId, existingStream) {
|
|
42
|
+
// If the send transport isn't ready yet, queue the request — it will be
|
|
43
|
+
// fulfilled automatically once both transports have been initialized.
|
|
44
|
+
if (!this.sendTransport) {
|
|
45
|
+
this.pendingPublishAudio = audio;
|
|
46
|
+
this.pendingPublishVideo = video;
|
|
47
|
+
this.pendingPublishAudioDeviceId = audioDeviceId;
|
|
48
|
+
this.pendingExistingStream = existingStream;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const stream = existingStream || await navigator.mediaDevices.getUserMedia({
|
|
53
|
+
audio: audio
|
|
54
|
+
? (audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true)
|
|
55
|
+
: false,
|
|
56
|
+
video: video ? { width: 1280, height: 720 } : false,
|
|
57
|
+
});
|
|
58
|
+
this.events.onLocalStream?.(stream);
|
|
59
|
+
if (!this.sendTransport) {
|
|
60
|
+
throw new Error('Send transport not initialized yet.');
|
|
61
|
+
}
|
|
62
|
+
if (audio) {
|
|
63
|
+
const audioTrack = stream.getAudioTracks()[0];
|
|
64
|
+
if (audioTrack) {
|
|
65
|
+
const producer = await this.sendTransport.produce({ track: audioTrack });
|
|
66
|
+
this.producers.set('audio-camera', producer);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (video) {
|
|
70
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
71
|
+
if (videoTrack) {
|
|
72
|
+
const producer = await this.sendTransport.produce({
|
|
73
|
+
track: videoTrack,
|
|
74
|
+
encodings: [
|
|
75
|
+
{ maxBitrate: 100000, scaleResolutionDownBy: 4 }, // Low: 180p
|
|
76
|
+
{ maxBitrate: 300000, scaleResolutionDownBy: 2 }, // Medium: 360p
|
|
77
|
+
{ maxBitrate: 1500000 }, // High: 720p
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
this.producers.set('video-camera', producer);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
this.events.onError?.(err);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async handleSignal(msg) {
|
|
89
|
+
try {
|
|
90
|
+
switch (msg.type) {
|
|
91
|
+
case 'connected':
|
|
92
|
+
this.events.onConnected?.({
|
|
93
|
+
sessionId: msg.sessionId,
|
|
94
|
+
role: msg.role,
|
|
95
|
+
displayName: msg.displayName,
|
|
96
|
+
isRecording: msg.isRecording,
|
|
97
|
+
autoRecord: msg.autoRecord,
|
|
98
|
+
});
|
|
99
|
+
break;
|
|
100
|
+
case 'recordingStarted':
|
|
101
|
+
this.events.onRecordingStarted?.();
|
|
102
|
+
break;
|
|
103
|
+
case 'recordingStopped':
|
|
104
|
+
this.events.onRecordingStopped?.();
|
|
105
|
+
break;
|
|
106
|
+
case 'whiteboardUpdate':
|
|
107
|
+
this.events.onWhiteboardUpdate?.(msg.peerId, msg.elements);
|
|
108
|
+
break;
|
|
109
|
+
case 'routerRtpCapabilities': {
|
|
110
|
+
const caps = msg.rtpCapabilities;
|
|
111
|
+
if (!caps || typeof caps !== 'object') {
|
|
112
|
+
throw new Error(`Invalid routerRtpCapabilities received from server: ${JSON.stringify(caps)}`);
|
|
113
|
+
}
|
|
114
|
+
await this.device?.load({ routerRtpCapabilities: caps });
|
|
115
|
+
// Create send and recv transports
|
|
116
|
+
this.client.createWebRtcTransport('send');
|
|
117
|
+
this.client.createWebRtcTransport('recv');
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case 'transportCreated':
|
|
121
|
+
await this.initializeTransport(msg);
|
|
122
|
+
break;
|
|
123
|
+
case 'producerCreated':
|
|
124
|
+
if (this.pendingProduceCallback) {
|
|
125
|
+
this.pendingProduceCallback.callback({ id: msg.id });
|
|
126
|
+
this.pendingProduceCallback = null;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
case 'consumerCreated':
|
|
130
|
+
await this.initializeConsumer(msg);
|
|
131
|
+
break;
|
|
132
|
+
case 'newProducer':
|
|
133
|
+
// Map producerId to peerId
|
|
134
|
+
this.producerToPeerMap.set(msg.producerId, msg.peerId);
|
|
135
|
+
// If transports aren't ready yet (e.g. this is a late-joiner sync
|
|
136
|
+
// notification that arrived before device.load / createWebRtcTransport
|
|
137
|
+
// completed), queue the consume for later.
|
|
138
|
+
if (!this.transportsReady.send || !this.transportsReady.recv) {
|
|
139
|
+
this.pendingConsumes.push({ producerId: msg.producerId, peerId: msg.peerId });
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
this.client.consume(msg.producerId, this.device?.rtpCapabilities);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case 'peerJoined':
|
|
146
|
+
this.events.onPeerJoin?.(msg);
|
|
147
|
+
break;
|
|
148
|
+
case 'peerLeft':
|
|
149
|
+
this.events.onPeerLeave?.(msg.peerId);
|
|
150
|
+
break;
|
|
151
|
+
case 'peerMuteStateChanged':
|
|
152
|
+
this.events.onPeerMuteStateChanged?.(msg.peerId, msg.kind, msg.muted);
|
|
153
|
+
break;
|
|
154
|
+
case 'screenSharePermissionUpdated':
|
|
155
|
+
this.events.onScreenSharePermissionUpdated?.(msg.peerId, msg.canShare);
|
|
156
|
+
break;
|
|
157
|
+
case 'producerClosed': {
|
|
158
|
+
const consumer = Array.from(this.consumers.values()).find(c => c.producerId === msg.producerId);
|
|
159
|
+
if (consumer) {
|
|
160
|
+
consumer.close();
|
|
161
|
+
this.consumers.delete(consumer.id);
|
|
162
|
+
const peerId = this.producerToPeerMap.get(msg.producerId) || msg.peerId;
|
|
163
|
+
this.events.onRemoteTrack?.(null, peerId, consumer.kind, consumer.appData);
|
|
164
|
+
}
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
case 'error':
|
|
168
|
+
this.events.onError?.(new Error(msg.message));
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
this.events.onError?.(err);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async initializeTransport(msg) {
|
|
177
|
+
if (!this.device)
|
|
178
|
+
return;
|
|
179
|
+
const { id, direction, iceParameters, iceCandidates, dtlsParameters } = msg;
|
|
180
|
+
const transportOptions = {
|
|
181
|
+
id,
|
|
182
|
+
iceParameters,
|
|
183
|
+
iceCandidates,
|
|
184
|
+
dtlsParameters,
|
|
185
|
+
iceServers: this.iceServers,
|
|
186
|
+
};
|
|
187
|
+
const transport = direction === 'send'
|
|
188
|
+
? this.device.createSendTransport(transportOptions)
|
|
189
|
+
: this.device.createRecvTransport(transportOptions);
|
|
190
|
+
transport.on('connect', ({ dtlsParameters }, callback, errback) => {
|
|
191
|
+
try {
|
|
192
|
+
this.client.connectWebRtcTransport(transport.id, dtlsParameters);
|
|
193
|
+
callback();
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
errback(err);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
if (direction === 'send') {
|
|
200
|
+
transport.on('produce', ({ kind, rtpParameters, appData }, callback, errback) => {
|
|
201
|
+
try {
|
|
202
|
+
this.client.produce(transport.id, kind, rtpParameters, appData);
|
|
203
|
+
this.pendingProduceCallback = { callback, errback };
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
errback(err);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
this.sendTransport = transport;
|
|
210
|
+
this.transportsReady.send = true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
this.recvTransport = transport;
|
|
214
|
+
this.transportsReady.recv = true;
|
|
215
|
+
}
|
|
216
|
+
// Once both transports are up, flush queued publish and consume requests.
|
|
217
|
+
if (this.transportsReady.send && this.transportsReady.recv) {
|
|
218
|
+
// Flush pending local media publish
|
|
219
|
+
const audio = this.pendingPublishAudio;
|
|
220
|
+
const video = this.pendingPublishVideo;
|
|
221
|
+
const audioDeviceId = this.pendingPublishAudioDeviceId;
|
|
222
|
+
const existingStream = this.pendingExistingStream;
|
|
223
|
+
this.pendingPublishAudio = false;
|
|
224
|
+
this.pendingPublishVideo = false;
|
|
225
|
+
this.pendingPublishAudioDeviceId = undefined;
|
|
226
|
+
this.pendingExistingStream = undefined;
|
|
227
|
+
if (audio || video) {
|
|
228
|
+
await this.publishLocalMedia(audio, video, audioDeviceId, existingStream);
|
|
229
|
+
}
|
|
230
|
+
// Flush newProducer events that arrived before transports were ready
|
|
231
|
+
// (late-joiner sync from the server)
|
|
232
|
+
const toConsume = this.pendingConsumes.splice(0);
|
|
233
|
+
for (const { producerId } of toConsume) {
|
|
234
|
+
this.client.consume(producerId, this.device?.rtpCapabilities);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async initializeConsumer(msg) {
|
|
239
|
+
if (!this.recvTransport)
|
|
240
|
+
return;
|
|
241
|
+
const { id, producerId, kind, rtpParameters, appData } = msg;
|
|
242
|
+
// Queue the consume calls to avoid duplicate mid SDP errors when multiple consumers are created simultaneously
|
|
243
|
+
const consumer = await new Promise((resolve, reject) => {
|
|
244
|
+
this.consumeMutex = this.consumeMutex.then(async () => {
|
|
245
|
+
try {
|
|
246
|
+
if (!this.recvTransport)
|
|
247
|
+
throw new Error("No recv transport");
|
|
248
|
+
const c = await this.recvTransport.consume({
|
|
249
|
+
id,
|
|
250
|
+
producerId,
|
|
251
|
+
kind,
|
|
252
|
+
rtpParameters,
|
|
253
|
+
appData,
|
|
254
|
+
});
|
|
255
|
+
resolve(c);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
reject(err);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
this.consumers.set(consumer.id, consumer);
|
|
263
|
+
const peerId = this.producerToPeerMap.get(producerId) || producerId;
|
|
264
|
+
this.events.onRemoteTrack?.(consumer.track, peerId, kind, appData);
|
|
265
|
+
}
|
|
266
|
+
endMeeting() {
|
|
267
|
+
this.client.endMeetingRoom();
|
|
268
|
+
}
|
|
269
|
+
sendTranscriptChunk(text) {
|
|
270
|
+
this.client.sendTranscriptChunk(text);
|
|
271
|
+
}
|
|
272
|
+
sendWhiteboardUpdate(elements) {
|
|
273
|
+
this.client.sendWhiteboardUpdate(elements);
|
|
274
|
+
}
|
|
275
|
+
kickParticipant(peerId) {
|
|
276
|
+
this.client.kickParticipant(peerId);
|
|
277
|
+
}
|
|
278
|
+
toggleMute(kind, muted) {
|
|
279
|
+
this.client.toggleMute(kind, muted);
|
|
280
|
+
}
|
|
281
|
+
async publishTrack(kind, track, appData) {
|
|
282
|
+
if (!this.sendTransport) {
|
|
283
|
+
throw new Error('Send transport not initialized yet.');
|
|
284
|
+
}
|
|
285
|
+
const producerKey = `${kind}-${appData?.source || 'camera'}`;
|
|
286
|
+
const existing = this.producers.get(producerKey);
|
|
287
|
+
if (existing) {
|
|
288
|
+
await existing.replaceTrack({ track });
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
const producer = await this.sendTransport.produce({
|
|
292
|
+
track,
|
|
293
|
+
encodings: kind === 'video'
|
|
294
|
+
? (appData?.source === 'screen'
|
|
295
|
+
? [{ maxBitrate: 3500000 }] // Single high-quality layer for screen sharing (up to 3.5 Mbps)
|
|
296
|
+
: [
|
|
297
|
+
{ maxBitrate: 100000, scaleResolutionDownBy: 4 }, // Low: 180p
|
|
298
|
+
{ maxBitrate: 300000, scaleResolutionDownBy: 2 }, // Medium: 360p
|
|
299
|
+
{ maxBitrate: 1500000 }, // High: 720p
|
|
300
|
+
])
|
|
301
|
+
: undefined,
|
|
302
|
+
appData,
|
|
303
|
+
});
|
|
304
|
+
this.producers.set(producerKey, producer);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async replaceTrack(kind, track, appData) {
|
|
308
|
+
const producerKey = `${kind}-${appData?.source || 'camera'}`;
|
|
309
|
+
const producer = this.producers.get(producerKey);
|
|
310
|
+
if (producer) {
|
|
311
|
+
await producer.replaceTrack({ track });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async unpublishTrack(kind, appData) {
|
|
315
|
+
const producerKey = `${kind}-${appData?.source || 'camera'}`;
|
|
316
|
+
const producer = this.producers.get(producerKey);
|
|
317
|
+
if (producer) {
|
|
318
|
+
this.closeProducer(producer.id);
|
|
319
|
+
producer.close();
|
|
320
|
+
this.producers.delete(producerKey);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
closeProducer(producerId) {
|
|
324
|
+
this.client.closeProducer(producerId);
|
|
325
|
+
}
|
|
326
|
+
grantScreenShare(peerId) {
|
|
327
|
+
this.client.grantScreenShare(peerId);
|
|
328
|
+
}
|
|
329
|
+
revokeScreenShare(peerId) {
|
|
330
|
+
this.client.revokeScreenShare(peerId);
|
|
331
|
+
}
|
|
332
|
+
pausePeerVideo(peerId) {
|
|
333
|
+
const consumer = Array.from(this.consumers.values()).find(c => {
|
|
334
|
+
const pId = this.producerToPeerMap.get(c.producerId);
|
|
335
|
+
return pId === peerId && c.kind === 'video' && c.appData?.source !== 'screen';
|
|
336
|
+
});
|
|
337
|
+
if (consumer && !consumer.paused) {
|
|
338
|
+
this.client.pauseConsumer(consumer.id);
|
|
339
|
+
consumer.pause();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
resumePeerVideo(peerId) {
|
|
343
|
+
const consumer = Array.from(this.consumers.values()).find(c => {
|
|
344
|
+
const pId = this.producerToPeerMap.get(c.producerId);
|
|
345
|
+
return pId === peerId && c.kind === 'video' && c.appData?.source !== 'screen';
|
|
346
|
+
});
|
|
347
|
+
if (consumer && consumer.paused) {
|
|
348
|
+
this.client.resumeConsumer(consumer.id);
|
|
349
|
+
consumer.resume();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
resumeAllVideos() {
|
|
353
|
+
for (const consumer of this.consumers.values()) {
|
|
354
|
+
if (consumer.kind === 'video' && consumer.appData?.source !== 'screen') {
|
|
355
|
+
if (consumer.paused) {
|
|
356
|
+
this.client.resumeConsumer(consumer.id);
|
|
357
|
+
consumer.resume();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
leave() {
|
|
363
|
+
if (this.pingInterval) {
|
|
364
|
+
window.clearInterval(this.pingInterval);
|
|
365
|
+
this.pingInterval = null;
|
|
366
|
+
}
|
|
367
|
+
this.client.disconnect();
|
|
368
|
+
// Transport.close() throws AwaitQueueStoppedError if called while
|
|
369
|
+
// produce/consume operations are still pending in the internal queue.
|
|
370
|
+
// Swallow it — the transport is being torn down regardless.
|
|
371
|
+
try {
|
|
372
|
+
this.sendTransport?.close();
|
|
373
|
+
}
|
|
374
|
+
catch (_) { }
|
|
375
|
+
try {
|
|
376
|
+
this.recvTransport?.close();
|
|
377
|
+
}
|
|
378
|
+
catch (_) { }
|
|
379
|
+
this.sendTransport = null;
|
|
380
|
+
this.recvTransport = null;
|
|
381
|
+
this.producers.clear();
|
|
382
|
+
this.consumers.clear();
|
|
383
|
+
this.producerToPeerMap.clear();
|
|
384
|
+
this.transportsReady = { send: false, recv: false };
|
|
385
|
+
}
|
|
386
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "covekit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"mediasoup-client": "^3.7.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.0.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch"
|
|
21
|
+
}
|
|
22
|
+
}
|