openrtc-netcode 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/LICENSE +92 -0
- package/README.md +46 -0
- package/dist/client.d.ts +112 -0
- package/dist/client.js +1184 -0
- package/dist/events.d.ts +6 -0
- package/dist/events.js +21 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/objects.d.ts +28 -0
- package/dist/objects.js +187 -0
- package/dist/protocol.d.ts +50 -0
- package/dist/protocol.js +53 -0
- package/dist/state.d.ts +16 -0
- package/dist/state.js +94 -0
- package/dist/types.d.ts +258 -0
- package/dist/types.js +1 -0
- package/dist/voice.d.ts +35 -0
- package/dist/voice.js +227 -0
- package/package.json +53 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { OpenRTC } from 'openrtc';
|
|
2
|
+
import type { RuntimeClient } from 'openrtc/runtime';
|
|
3
|
+
import type { MetadataBody, NetcodeEnvelope, ObjectRemoveBody, ObjectSnapshotBody, ObjectUpsertBody, StatePatchBody, StateSnapshotBody, UserMessageBody, VoiceSignalBody } from './protocol.js';
|
|
4
|
+
type OpenrtcClientOptions = Parameters<typeof OpenRTC>[0];
|
|
5
|
+
export type NetcodeRuntime = RuntimeClient;
|
|
6
|
+
export type NetcodeReliability = 'reliable';
|
|
7
|
+
export interface NetcodeOptions {
|
|
8
|
+
runtime?: NetcodeRuntime;
|
|
9
|
+
apiKey?: OpenrtcClientOptions['apiKey'];
|
|
10
|
+
projectId?: OpenrtcClientOptions['projectId'];
|
|
11
|
+
authMode?: OpenrtcClientOptions['authMode'];
|
|
12
|
+
discoveryMode?: OpenrtcClientOptions['discoveryMode'];
|
|
13
|
+
allowAnonymousHostedDefaults?: OpenrtcClientOptions['allowAnonymousHostedDefaults'];
|
|
14
|
+
space?: OpenrtcClientOptions['space'];
|
|
15
|
+
spaceKey?: OpenrtcClientOptions['spaceKey'];
|
|
16
|
+
spaceTokenProvider?: NonNullable<OpenrtcClientOptions>['spaceTokenProvider'];
|
|
17
|
+
storagePrefix?: OpenrtcClientOptions['storagePrefix'];
|
|
18
|
+
nodeIdPersistence?: OpenrtcClientOptions['nodeIdPersistence'];
|
|
19
|
+
transports?: OpenrtcClientOptions['transports'];
|
|
20
|
+
transportPriority?: OpenrtcClientOptions['transportPriority'];
|
|
21
|
+
preferredPayloadTransports?: RuntimeApplicationPayloadReadinessOptions['preferredTransports'];
|
|
22
|
+
strictMode?: OpenrtcClientOptions['strictMode'];
|
|
23
|
+
turnCredentialsProvider?: NonNullable<OpenrtcClientOptions>['turnCredentialsProvider'];
|
|
24
|
+
displayName?: string;
|
|
25
|
+
maxPeers?: number;
|
|
26
|
+
localFallback?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface CreateLobbyOptions {
|
|
29
|
+
id?: string;
|
|
30
|
+
metadata?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
export interface JoinLobbyOptions {
|
|
33
|
+
metadata?: Record<string, string>;
|
|
34
|
+
}
|
|
35
|
+
export interface NetcodeLobby {
|
|
36
|
+
id: string;
|
|
37
|
+
localPeerId: string;
|
|
38
|
+
ownerId: string | null;
|
|
39
|
+
members: NetcodePeer[];
|
|
40
|
+
metadata: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
export interface NetcodePeer {
|
|
43
|
+
id: string;
|
|
44
|
+
userId?: string;
|
|
45
|
+
ticket?: string;
|
|
46
|
+
joinedAt?: number;
|
|
47
|
+
connected: boolean;
|
|
48
|
+
metadata: Record<string, string>;
|
|
49
|
+
stats?: NetcodePeerStats;
|
|
50
|
+
}
|
|
51
|
+
export interface NetcodePeerStats {
|
|
52
|
+
peerId: string;
|
|
53
|
+
transport: string;
|
|
54
|
+
latencyMs: number | null;
|
|
55
|
+
connected: boolean;
|
|
56
|
+
updatedAt: number;
|
|
57
|
+
}
|
|
58
|
+
export interface SendOptions {
|
|
59
|
+
reliability?: NetcodeReliability;
|
|
60
|
+
}
|
|
61
|
+
export interface NetVarOptions {
|
|
62
|
+
owner?: 'local' | 'lobby-owner' | string;
|
|
63
|
+
}
|
|
64
|
+
export interface NetVar<T = unknown> {
|
|
65
|
+
readonly key: string;
|
|
66
|
+
readonly owner: string;
|
|
67
|
+
readonly value: T;
|
|
68
|
+
set(value: T): Promise<void>;
|
|
69
|
+
onChange(callback: (value: T, previous: T | undefined) => void): () => void;
|
|
70
|
+
}
|
|
71
|
+
export interface NetcodeState {
|
|
72
|
+
define<T>(key: string, initial: T, options?: NetVarOptions): NetVar<T>;
|
|
73
|
+
get<T = unknown>(key: string): T | undefined;
|
|
74
|
+
snapshot(): Record<string, unknown>;
|
|
75
|
+
}
|
|
76
|
+
export interface NetcodeVector3 {
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
z: number;
|
|
80
|
+
}
|
|
81
|
+
export interface NetcodeQuaternion {
|
|
82
|
+
x: number;
|
|
83
|
+
y: number;
|
|
84
|
+
z: number;
|
|
85
|
+
w: number;
|
|
86
|
+
}
|
|
87
|
+
export interface NetcodeTransform {
|
|
88
|
+
position: NetcodeVector3;
|
|
89
|
+
rotation?: NetcodeQuaternion;
|
|
90
|
+
scale?: NetcodeVector3;
|
|
91
|
+
}
|
|
92
|
+
export interface NetcodePhysicsState {
|
|
93
|
+
linearVelocity?: NetcodeVector3;
|
|
94
|
+
angularVelocity?: NetcodeVector3;
|
|
95
|
+
sleeping?: boolean;
|
|
96
|
+
}
|
|
97
|
+
export interface NetcodeObjectState<TMetadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
98
|
+
id: string;
|
|
99
|
+
kind: string;
|
|
100
|
+
ownerId: string;
|
|
101
|
+
transform: NetcodeTransform;
|
|
102
|
+
physics?: NetcodePhysicsState;
|
|
103
|
+
metadata?: TMetadata;
|
|
104
|
+
updatedAt: number;
|
|
105
|
+
}
|
|
106
|
+
export type NetcodeObjectUpsert<TMetadata extends Record<string, unknown> = Record<string, unknown>> = Partial<Omit<NetcodeObjectState<TMetadata>, 'transform'>> & {
|
|
107
|
+
kind?: string;
|
|
108
|
+
transform: Partial<NetcodeTransform> & {
|
|
109
|
+
position: NetcodeVector3;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
export type NetcodeObjectChange = {
|
|
113
|
+
type: 'snapshot';
|
|
114
|
+
objects: NetcodeObjectState[];
|
|
115
|
+
from: string;
|
|
116
|
+
} | {
|
|
117
|
+
type: 'upsert';
|
|
118
|
+
object: NetcodeObjectState;
|
|
119
|
+
from: string;
|
|
120
|
+
} | {
|
|
121
|
+
type: 'remove';
|
|
122
|
+
id: string;
|
|
123
|
+
from: string;
|
|
124
|
+
};
|
|
125
|
+
export interface NetcodeObjectStore {
|
|
126
|
+
list(): NetcodeObjectState[];
|
|
127
|
+
get(id: string): NetcodeObjectState | undefined;
|
|
128
|
+
snapshot(): Record<string, NetcodeObjectState>;
|
|
129
|
+
spawn(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
|
|
130
|
+
upsert(input: NetcodeObjectUpsert): Promise<NetcodeObjectState>;
|
|
131
|
+
update(id: string, patch: Partial<Omit<NetcodeObjectState, 'id'>>): Promise<NetcodeObjectState>;
|
|
132
|
+
remove(id: string): Promise<void>;
|
|
133
|
+
onChange(listener: (change: NetcodeObjectChange) => void): () => void;
|
|
134
|
+
}
|
|
135
|
+
export interface NetcodeVoiceSignal {
|
|
136
|
+
description?: RTCSessionDescriptionInit;
|
|
137
|
+
candidate?: RTCIceCandidateInit;
|
|
138
|
+
}
|
|
139
|
+
export interface NetcodeVoiceOptions {
|
|
140
|
+
mediaStream?: MediaStream;
|
|
141
|
+
mediaDevices?: Pick<MediaDevices, 'getUserMedia'>;
|
|
142
|
+
audio?: boolean | MediaTrackConstraints;
|
|
143
|
+
rtcConfig?: RTCConfiguration;
|
|
144
|
+
createPeerConnection?: (config?: RTCConfiguration) => RTCPeerConnection;
|
|
145
|
+
stopTracksOnStop?: boolean;
|
|
146
|
+
}
|
|
147
|
+
export interface NetcodeVoicePeer {
|
|
148
|
+
peerId: string;
|
|
149
|
+
stream: MediaStream | null;
|
|
150
|
+
connectionState: RTCPeerConnectionState;
|
|
151
|
+
iceConnectionState: RTCIceConnectionState;
|
|
152
|
+
}
|
|
153
|
+
export interface NetcodeVoiceEvents {
|
|
154
|
+
'local:stream': MediaStream;
|
|
155
|
+
'peer:stream': {
|
|
156
|
+
peerId: string;
|
|
157
|
+
stream: MediaStream;
|
|
158
|
+
};
|
|
159
|
+
'peer:left': {
|
|
160
|
+
peerId: string;
|
|
161
|
+
};
|
|
162
|
+
peers: NetcodeVoicePeer[];
|
|
163
|
+
mute: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface NetcodeVoice {
|
|
166
|
+
readonly enabled: boolean;
|
|
167
|
+
readonly localStream: MediaStream | null;
|
|
168
|
+
readonly isMuted: boolean;
|
|
169
|
+
listPeers(): NetcodeVoicePeer[];
|
|
170
|
+
start(options?: NetcodeVoiceOptions): Promise<MediaStream>;
|
|
171
|
+
stop(): Promise<void>;
|
|
172
|
+
setMuted(muted: boolean): void;
|
|
173
|
+
on<K extends keyof NetcodeVoiceEvents>(event: K, handler: (payload: NetcodeVoiceEvents[K]) => void): () => void;
|
|
174
|
+
}
|
|
175
|
+
export interface NetcodeEvents {
|
|
176
|
+
lobby: NetcodeLobby | null;
|
|
177
|
+
'peer:joined': NetcodePeer;
|
|
178
|
+
'peer:left': NetcodePeer;
|
|
179
|
+
'peer:connected': NetcodePeer;
|
|
180
|
+
'peer:disconnected': NetcodePeer;
|
|
181
|
+
'peer:stats': NetcodePeerStats;
|
|
182
|
+
message: NetcodeMessage;
|
|
183
|
+
envelope: NetcodeEnvelope;
|
|
184
|
+
'state:snapshot': StateSnapshotBody;
|
|
185
|
+
'state:patch': StatePatchBody;
|
|
186
|
+
'object:snapshot': ObjectSnapshotBody;
|
|
187
|
+
'object:upsert': ObjectUpsertBody;
|
|
188
|
+
'object:remove': ObjectRemoveBody;
|
|
189
|
+
'lobby:metadata': MetadataBody;
|
|
190
|
+
'member:metadata': MetadataBody & {
|
|
191
|
+
peerId: string;
|
|
192
|
+
};
|
|
193
|
+
'voice:signal': VoiceSignalBody & {
|
|
194
|
+
peerId: string;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
export type NetcodeEventName = keyof NetcodeEvents;
|
|
198
|
+
export type NetcodeEventHandler<K extends NetcodeEventName> = (payload: NetcodeEvents[K]) => void;
|
|
199
|
+
export type NetcodeMessageHandler<T = unknown> = (message: NetcodeMessage<T>) => void;
|
|
200
|
+
export interface NetcodeMessage<T = unknown> {
|
|
201
|
+
type: string;
|
|
202
|
+
payload: T;
|
|
203
|
+
from: string;
|
|
204
|
+
target?: string;
|
|
205
|
+
sentAt: number;
|
|
206
|
+
}
|
|
207
|
+
export interface NetcodeClient {
|
|
208
|
+
readonly peerId: string | null;
|
|
209
|
+
readonly lobby: NetcodeLobby | null;
|
|
210
|
+
readonly peers: NetcodePeer[];
|
|
211
|
+
readonly peerStats: NetcodePeerStats[];
|
|
212
|
+
readonly state: NetcodeState;
|
|
213
|
+
readonly objects: NetcodeObjectStore;
|
|
214
|
+
readonly voice: NetcodeVoice;
|
|
215
|
+
start(): Promise<void>;
|
|
216
|
+
stop(): Promise<void>;
|
|
217
|
+
createLobby(options?: CreateLobbyOptions): Promise<NetcodeLobby>;
|
|
218
|
+
joinLobby(lobbyId: string, options?: JoinLobbyOptions): Promise<NetcodeLobby>;
|
|
219
|
+
joinOrCreateLobby(lobbyId: string, options?: JoinLobbyOptions): Promise<NetcodeLobby>;
|
|
220
|
+
leaveLobby(): Promise<void>;
|
|
221
|
+
send(peerId: string, type: string, payload: unknown, options?: SendOptions): Promise<void>;
|
|
222
|
+
broadcast(type: string, payload: unknown, options?: SendOptions): Promise<void>;
|
|
223
|
+
on<K extends NetcodeEventName>(event: K, handler: NetcodeEventHandler<K>): () => void;
|
|
224
|
+
onMessage<T = unknown>(type: string, handler: NetcodeMessageHandler<T>): () => void;
|
|
225
|
+
setLobbyData(key: string, value: string): Promise<void>;
|
|
226
|
+
setMemberData(key: string, value: string): Promise<void>;
|
|
227
|
+
}
|
|
228
|
+
export type RuntimeConnectionLike = {
|
|
229
|
+
id?: string;
|
|
230
|
+
deviceId?: string;
|
|
231
|
+
remoteNodeId?: string;
|
|
232
|
+
isClosed?: boolean;
|
|
233
|
+
getTransportStatus?(): {
|
|
234
|
+
activeTransport: string;
|
|
235
|
+
parallelTransport?: string | null;
|
|
236
|
+
};
|
|
237
|
+
getAvailableTransports?(): string[];
|
|
238
|
+
requestWebRTCUpgrade?(reason?: string): void;
|
|
239
|
+
send(message: unknown): Promise<void> | void;
|
|
240
|
+
onMessage(callback: (message: unknown) => void): void | (() => void);
|
|
241
|
+
onDisconnect(callback: () => void): void | (() => void);
|
|
242
|
+
disconnect?(): Promise<void> | void;
|
|
243
|
+
};
|
|
244
|
+
export type RuntimeApplicationPayloadReadinessOptions = {
|
|
245
|
+
preferredTransports?: string[];
|
|
246
|
+
allowFallbackAfterPreferredTransportFailure?: boolean;
|
|
247
|
+
};
|
|
248
|
+
export type RuntimeMemberLike = {
|
|
249
|
+
nodeId?: string;
|
|
250
|
+
userId?: string;
|
|
251
|
+
ticket?: string;
|
|
252
|
+
joinedAt?: number;
|
|
253
|
+
lastSeenAt?: number;
|
|
254
|
+
expiresAt?: number;
|
|
255
|
+
metadata?: string | Record<string, unknown> | null;
|
|
256
|
+
};
|
|
257
|
+
export type RuntimeEnvelopeBody = UserMessageBody | StateSnapshotBody | StatePatchBody | ObjectSnapshotBody | ObjectUpsertBody | ObjectRemoveBody | MetadataBody | VoiceSignalBody | Record<string, unknown>;
|
|
258
|
+
export {};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/voice.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NetcodePeer, NetcodeVoice, NetcodeVoiceEvents, NetcodeVoiceOptions, NetcodeVoicePeer, NetcodeVoiceSignal } from './types.js';
|
|
2
|
+
export declare class NetcodeVoiceController implements NetcodeVoice {
|
|
3
|
+
private readonly localPeerId;
|
|
4
|
+
private readonly peers;
|
|
5
|
+
private readonly shouldOffer;
|
|
6
|
+
private readonly sendSignal;
|
|
7
|
+
private readonly emitter;
|
|
8
|
+
private readonly sessions;
|
|
9
|
+
private readonly pendingSignals;
|
|
10
|
+
private localMediaStream;
|
|
11
|
+
private options;
|
|
12
|
+
private active;
|
|
13
|
+
private muted;
|
|
14
|
+
constructor(localPeerId: () => string | null, peers: () => NetcodePeer[], shouldOffer: (localPeerId: string, remotePeerId: string) => boolean, sendSignal: (peerId: string, signal: NetcodeVoiceSignal) => Promise<void>);
|
|
15
|
+
get enabled(): boolean;
|
|
16
|
+
get localStream(): MediaStream | null;
|
|
17
|
+
get isMuted(): boolean;
|
|
18
|
+
listPeers(): NetcodeVoicePeer[];
|
|
19
|
+
start(options?: NetcodeVoiceOptions): Promise<MediaStream>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
setMuted(muted: boolean): void;
|
|
22
|
+
on<K extends keyof NetcodeVoiceEvents>(event: K, handler: (payload: NetcodeVoiceEvents[K]) => void): () => void;
|
|
23
|
+
handlePeerConnected(peerId: string): Promise<void>;
|
|
24
|
+
handlePeerDisconnected(peerId: string): void;
|
|
25
|
+
handleSignal(peerId: string, signal: NetcodeVoiceSignal): Promise<void>;
|
|
26
|
+
private createAndSendOffer;
|
|
27
|
+
private startPeerSession;
|
|
28
|
+
private flushPendingSignals;
|
|
29
|
+
private applySignal;
|
|
30
|
+
private flushPendingCandidates;
|
|
31
|
+
private ensureSession;
|
|
32
|
+
private createPeerConnection;
|
|
33
|
+
private captureAudio;
|
|
34
|
+
private applyMuteState;
|
|
35
|
+
}
|
package/dist/voice.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { TypedEmitter } from './events.js';
|
|
2
|
+
export class NetcodeVoiceController {
|
|
3
|
+
constructor(localPeerId, peers, shouldOffer, sendSignal) {
|
|
4
|
+
this.localPeerId = localPeerId;
|
|
5
|
+
this.peers = peers;
|
|
6
|
+
this.shouldOffer = shouldOffer;
|
|
7
|
+
this.sendSignal = sendSignal;
|
|
8
|
+
this.emitter = new TypedEmitter();
|
|
9
|
+
this.sessions = new Map();
|
|
10
|
+
this.pendingSignals = new Map();
|
|
11
|
+
this.localMediaStream = null;
|
|
12
|
+
this.options = {};
|
|
13
|
+
this.active = false;
|
|
14
|
+
this.muted = false;
|
|
15
|
+
}
|
|
16
|
+
get enabled() {
|
|
17
|
+
return this.active;
|
|
18
|
+
}
|
|
19
|
+
get localStream() {
|
|
20
|
+
return this.localMediaStream;
|
|
21
|
+
}
|
|
22
|
+
get isMuted() {
|
|
23
|
+
return this.muted;
|
|
24
|
+
}
|
|
25
|
+
listPeers() {
|
|
26
|
+
return Array.from(this.sessions.values()).map((session) => ({
|
|
27
|
+
peerId: session.peerId,
|
|
28
|
+
stream: session.remoteStream,
|
|
29
|
+
connectionState: session.connection.connectionState,
|
|
30
|
+
iceConnectionState: session.connection.iceConnectionState,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
async start(options = {}) {
|
|
34
|
+
if (this.active && this.localMediaStream) {
|
|
35
|
+
return this.localMediaStream;
|
|
36
|
+
}
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.localMediaStream = options.mediaStream ?? await this.captureAudio(options);
|
|
39
|
+
this.active = true;
|
|
40
|
+
this.applyMuteState();
|
|
41
|
+
this.emitter.emit('local:stream', this.localMediaStream);
|
|
42
|
+
const localPeerId = this.localPeerId();
|
|
43
|
+
if (localPeerId) {
|
|
44
|
+
for (const peer of this.peers().filter((candidate) => candidate.connected)) {
|
|
45
|
+
void this.startPeerSession(peer.id, localPeerId);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return this.localMediaStream;
|
|
49
|
+
}
|
|
50
|
+
async stop() {
|
|
51
|
+
this.active = false;
|
|
52
|
+
for (const session of this.sessions.values()) {
|
|
53
|
+
session.connection.close();
|
|
54
|
+
}
|
|
55
|
+
this.sessions.clear();
|
|
56
|
+
this.pendingSignals.clear();
|
|
57
|
+
if (this.options.stopTracksOnStop !== false) {
|
|
58
|
+
for (const track of this.localMediaStream?.getTracks() ?? []) {
|
|
59
|
+
track.stop();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
this.localMediaStream = null;
|
|
63
|
+
this.emitter.emit('peers', []);
|
|
64
|
+
}
|
|
65
|
+
setMuted(muted) {
|
|
66
|
+
this.muted = muted;
|
|
67
|
+
this.applyMuteState();
|
|
68
|
+
this.emitter.emit('mute', muted);
|
|
69
|
+
}
|
|
70
|
+
on(event, handler) {
|
|
71
|
+
return this.emitter.on(event, handler);
|
|
72
|
+
}
|
|
73
|
+
async handlePeerConnected(peerId) {
|
|
74
|
+
if (!this.active) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const localPeerId = this.localPeerId();
|
|
78
|
+
if (!localPeerId) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
await this.startPeerSession(peerId, localPeerId);
|
|
82
|
+
}
|
|
83
|
+
handlePeerDisconnected(peerId) {
|
|
84
|
+
const session = this.sessions.get(peerId);
|
|
85
|
+
if (!session) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
session.connection.close();
|
|
89
|
+
this.sessions.delete(peerId);
|
|
90
|
+
this.emitter.emit('peer:left', { peerId });
|
|
91
|
+
this.emitter.emit('peers', this.listPeers());
|
|
92
|
+
}
|
|
93
|
+
async handleSignal(peerId, signal) {
|
|
94
|
+
if (!this.active) {
|
|
95
|
+
const pending = this.pendingSignals.get(peerId) ?? [];
|
|
96
|
+
pending.push(signal);
|
|
97
|
+
this.pendingSignals.set(peerId, pending);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const session = this.ensureSession(peerId);
|
|
101
|
+
await this.applySignal(session, signal);
|
|
102
|
+
}
|
|
103
|
+
async createAndSendOffer(session) {
|
|
104
|
+
const offer = await session.connection.createOffer();
|
|
105
|
+
await session.connection.setLocalDescription(offer);
|
|
106
|
+
if (session.connection.localDescription) {
|
|
107
|
+
await this.sendSignal(session.peerId, { description: session.connection.localDescription.toJSON() });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async startPeerSession(peerId, localPeerId) {
|
|
111
|
+
try {
|
|
112
|
+
const session = this.ensureSession(peerId);
|
|
113
|
+
await this.flushPendingSignals(peerId);
|
|
114
|
+
if (this.shouldOffer(localPeerId, peerId)) {
|
|
115
|
+
await this.createAndSendOffer(session);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn('[openrtc-netcode] voice peer setup failed', error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async flushPendingSignals(peerId) {
|
|
123
|
+
const pending = this.pendingSignals.get(peerId);
|
|
124
|
+
if (!pending?.length) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.pendingSignals.delete(peerId);
|
|
128
|
+
const session = this.ensureSession(peerId);
|
|
129
|
+
for (const signal of pending) {
|
|
130
|
+
await this.applySignal(session, signal);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async applySignal(session, signal) {
|
|
134
|
+
if (signal.description) {
|
|
135
|
+
await session.connection.setRemoteDescription(signal.description);
|
|
136
|
+
await this.flushPendingCandidates(session);
|
|
137
|
+
if (signal.description.type === 'offer') {
|
|
138
|
+
const answer = await session.connection.createAnswer();
|
|
139
|
+
await session.connection.setLocalDescription(answer);
|
|
140
|
+
if (session.connection.localDescription) {
|
|
141
|
+
await this.sendSignal(session.peerId, { description: session.connection.localDescription.toJSON() });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (signal.candidate) {
|
|
146
|
+
if (!session.connection.remoteDescription) {
|
|
147
|
+
session.pendingCandidates.push(signal.candidate);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
await session.connection.addIceCandidate(signal.candidate);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async flushPendingCandidates(session) {
|
|
154
|
+
if (!session.connection.remoteDescription || !session.pendingCandidates.length) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const pending = session.pendingCandidates.splice(0);
|
|
158
|
+
for (const candidate of pending) {
|
|
159
|
+
await session.connection.addIceCandidate(candidate);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
ensureSession(peerId) {
|
|
163
|
+
const existing = this.sessions.get(peerId);
|
|
164
|
+
if (existing) {
|
|
165
|
+
return existing;
|
|
166
|
+
}
|
|
167
|
+
const connection = this.createPeerConnection();
|
|
168
|
+
const session = {
|
|
169
|
+
peerId,
|
|
170
|
+
connection,
|
|
171
|
+
remoteStream: null,
|
|
172
|
+
pendingCandidates: [],
|
|
173
|
+
};
|
|
174
|
+
const localStream = this.localMediaStream;
|
|
175
|
+
if (localStream) {
|
|
176
|
+
for (const track of localStream.getAudioTracks()) {
|
|
177
|
+
connection.addTrack(track, localStream);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
connection.onicecandidate = (event) => {
|
|
181
|
+
if (event.candidate) {
|
|
182
|
+
void this.sendSignal(peerId, { candidate: event.candidate.toJSON() });
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
connection.ontrack = (event) => {
|
|
186
|
+
session.remoteStream = event.streams[0] ?? null;
|
|
187
|
+
if (session.remoteStream) {
|
|
188
|
+
this.emitter.emit('peer:stream', { peerId, stream: session.remoteStream });
|
|
189
|
+
}
|
|
190
|
+
this.emitter.emit('peers', this.listPeers());
|
|
191
|
+
};
|
|
192
|
+
connection.onconnectionstatechange = () => {
|
|
193
|
+
this.emitter.emit('peers', this.listPeers());
|
|
194
|
+
};
|
|
195
|
+
this.sessions.set(peerId, session);
|
|
196
|
+
this.emitter.emit('peers', this.listPeers());
|
|
197
|
+
return session;
|
|
198
|
+
}
|
|
199
|
+
createPeerConnection() {
|
|
200
|
+
if (this.options.createPeerConnection) {
|
|
201
|
+
return this.options.createPeerConnection(this.options.rtcConfig);
|
|
202
|
+
}
|
|
203
|
+
if (typeof RTCPeerConnection === 'undefined') {
|
|
204
|
+
throw new Error('Voice chat requires RTCPeerConnection support in this environment.');
|
|
205
|
+
}
|
|
206
|
+
return new RTCPeerConnection(this.options.rtcConfig);
|
|
207
|
+
}
|
|
208
|
+
async captureAudio(options) {
|
|
209
|
+
const mediaDevices = options.mediaDevices ?? globalThis.navigator?.mediaDevices;
|
|
210
|
+
if (!mediaDevices?.getUserMedia) {
|
|
211
|
+
throw new Error('Voice chat requires navigator.mediaDevices.getUserMedia.');
|
|
212
|
+
}
|
|
213
|
+
return mediaDevices.getUserMedia({
|
|
214
|
+
audio: options.audio ?? {
|
|
215
|
+
echoCancellation: true,
|
|
216
|
+
noiseSuppression: true,
|
|
217
|
+
autoGainControl: true,
|
|
218
|
+
},
|
|
219
|
+
video: false,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
applyMuteState() {
|
|
223
|
+
for (const track of this.localMediaStream?.getAudioTracks() ?? []) {
|
|
224
|
+
track.enabled = !this.muted;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openrtc-netcode",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"description": "A small lobby, mesh, message, and replicated-state netcode layer built on OpenRTC.",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"openrtc",
|
|
10
|
+
"netcode",
|
|
11
|
+
"p2p",
|
|
12
|
+
"multiplayer",
|
|
13
|
+
"lobby",
|
|
14
|
+
"mesh"
|
|
15
|
+
],
|
|
16
|
+
"author": "Bryant Hargreaves",
|
|
17
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
18
|
+
"homepage": "https://openrtc.app",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/bluestarburst/openrtc.git",
|
|
22
|
+
"directory": "packages/openrtc-netcode"
|
|
23
|
+
},
|
|
24
|
+
"main": "dist/index.js",
|
|
25
|
+
"types": "dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./package.json": "./package.json"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "rm -rf dist && tsc",
|
|
40
|
+
"prepublishOnly": "pnpm run build",
|
|
41
|
+
"test": "vitest run tests",
|
|
42
|
+
"test:watch": "vitest"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"openrtc": "^0.2.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^24.10.1",
|
|
49
|
+
"happy-dom": "^20.5.0",
|
|
50
|
+
"typescript": "^5.8.3",
|
|
51
|
+
"vitest": "^4.0.18"
|
|
52
|
+
}
|
|
53
|
+
}
|