@vibexnpm/talkx 8.13.0 → 9.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.
@@ -1,476 +0,0 @@
1
- /**
2
- * PeerConnectionManager
3
- * WebRTC Peer 연결 관리 (Perfect Negotiation 패턴)
4
- */
5
-
6
- import EventEmitter from '../utils/EventEmitter.js';
7
- import Logger from '../utils/Logger.js';
8
- import { ErrorTypes, DefaultConfig, LogLevel } from '../constants.js';
9
-
10
- class PeerConnectionManager extends EventEmitter {
11
- /**
12
- * @param {Object} options
13
- * @param {Object[]} [options.iceServers] - ICE 서버 설정
14
- * @param {number} [options.logLevel] - 로그 레벨
15
- */
16
- constructor(options = {}) {
17
- super();
18
-
19
- this.iceServers = options.iceServers || DefaultConfig.iceServers;
20
- this.logger = new Logger(options.logLevel || LogLevel.WARN, 'PeerConnectionManager');
21
-
22
- // 피어 연결 맵: peerId -> { connection, polite, makingOffer, ignoreOffer }
23
- this.peers = new Map();
24
-
25
- // 로컬 스트림
26
- this.localStream = null;
27
- }
28
-
29
- /**
30
- * 로컬 스트림 설정
31
- * @param {MediaStream} stream
32
- */
33
- setLocalStream(stream) {
34
- this.localStream = stream;
35
-
36
- // 기존 연결에 트랙 추가
37
- this.peers.forEach((peer) => {
38
- this._addTracksToConnection(peer.connection);
39
- });
40
- }
41
-
42
- /**
43
- * ICE 서버 설정
44
- * @param {Object[]} servers
45
- */
46
- setIceServers(servers) {
47
- this.iceServers = servers;
48
- }
49
-
50
- /**
51
- * 피어 연결 생성
52
- * @param {string} peerId - 피어 ID
53
- * @param {boolean} polite - Polite 피어 여부 (Perfect Negotiation)
54
- * @param {Object} [options] - 추가 옵션
55
- * @param {boolean} [options.skipAutoNegotiation=false] - 자동 negotiation 건너뛰기
56
- * @returns {RTCPeerConnection}
57
- */
58
- createPeerConnection(peerId, polite = false, options = {}) {
59
- if (this.peers.has(peerId)) {
60
- this.logger.warn(`Peer connection already exists: ${peerId}`);
61
- return this.peers.get(peerId).connection;
62
- }
63
-
64
- const config = {
65
- iceServers: this.iceServers,
66
- iceCandidatePoolSize: 10
67
- };
68
-
69
- const connection = new RTCPeerConnection(config);
70
-
71
- const peerState = {
72
- connection,
73
- polite,
74
- makingOffer: false,
75
- ignoreOffer: false,
76
- isSettingRemoteAnswerPending: false,
77
- skipAutoNegotiation: options.skipAutoNegotiation || false
78
- };
79
-
80
- this.peers.set(peerId, peerState);
81
-
82
- // 이벤트 핸들러 설정
83
- this._setupConnectionHandlers(peerId, connection, peerState);
84
-
85
- // 로컬 트랙 추가
86
- if (this.localStream) {
87
- this._addTracksToConnection(connection);
88
- }
89
-
90
- this.logger.info(`Peer connection created: ${peerId} (polite: ${polite}, skipAutoNegotiation: ${peerState.skipAutoNegotiation})`);
91
- return connection;
92
- }
93
-
94
- /** 그룹 시작 중 answer를 먼저 보낸 peer에 로컬 트랙을 보낼 협상을 재개한다. */
95
- async resumeAutoNegotiation(peerId) {
96
- const peer = this.peers.get(peerId);
97
- if (!peer || peer.connection.signalingState !== 'stable') return false;
98
- peer.skipAutoNegotiation = false;
99
- await peer.connection.onnegotiationneeded();
100
- return true;
101
- }
102
-
103
- /**
104
- * 연결 이벤트 핸들러 설정
105
- * @private
106
- * @param {string} peerId - 피어 ID
107
- * @param {RTCPeerConnection} connection - 피어 연결
108
- * @param {Object} peerState - 피어 상태 객체
109
- */
110
- _setupConnectionHandlers(peerId, connection, peerState) {
111
- // ICE Candidate 이벤트
112
- connection.onicecandidate = (event) => {
113
- if (event.candidate) {
114
- this.emit('iceCandidate', {
115
- peerId,
116
- candidate: event.candidate
117
- });
118
- }
119
- };
120
-
121
- // ICE 연결 상태 변경
122
- connection.oniceconnectionstatechange = () => {
123
- this.logger.debug(`ICE connection state (${peerId}): ${connection.iceConnectionState}`);
124
-
125
- this.emit('iceConnectionStateChange', {
126
- peerId,
127
- state: connection.iceConnectionState
128
- });
129
-
130
- if (connection.iceConnectionState === 'failed') {
131
- this.emit('error', {
132
- type: ErrorTypes.ICE_CONNECTION_FAILED,
133
- peerId,
134
- message: 'ICE connection failed'
135
- });
136
- }
137
- };
138
-
139
- // 연결 상태 변경
140
- connection.onconnectionstatechange = () => {
141
- this.logger.debug(`Connection state (${peerId}): ${connection.connectionState}`);
142
-
143
- this.emit('connectionStateChange', {
144
- peerId,
145
- state: connection.connectionState
146
- });
147
-
148
- if (connection.connectionState === 'connected') {
149
- this.emit('peerConnected', { peerId });
150
- } else if (connection.connectionState === 'disconnected' || connection.connectionState === 'failed') {
151
- this.emit('peerDisconnected', { peerId });
152
- }
153
- };
154
-
155
- // 협상 필요 (Perfect Negotiation)
156
- connection.onnegotiationneeded = async () => {
157
- // 1:1 통화에서 수동으로 offer를 생성할 때는 자동 negotiation 건너뛰기
158
- if (peerState.skipAutoNegotiation || peerState.makingOffer || connection.signalingState !== 'stable') {
159
- this.logger.debug(`Skipping auto negotiation for ${peerId} (manual offer will be sent)`);
160
- return;
161
- }
162
-
163
- try {
164
- peerState.makingOffer = true;
165
- await connection.setLocalDescription();
166
-
167
- this.emit('negotiationNeeded', {
168
- peerId,
169
- description: connection.localDescription
170
- });
171
-
172
- } catch (error) {
173
- this.logger.error(`Negotiation error (${peerId}):`, error);
174
- } finally {
175
- peerState.makingOffer = false;
176
- }
177
- };
178
-
179
- // 원격 트랙 수신
180
- connection.ontrack = (event) => {
181
- this.logger.info(`Remote track received from ${peerId}:`, event.track.kind);
182
-
183
- this.emit('remoteTrack', {
184
- peerId,
185
- track: event.track,
186
- streams: event.streams
187
- });
188
- };
189
-
190
- // 데이터 채널 수신
191
- connection.ondatachannel = (event) => {
192
- this.logger.info(`Data channel received from ${peerId}`);
193
- this.emit('dataChannel', {
194
- peerId,
195
- channel: event.channel
196
- });
197
- };
198
- }
199
-
200
- /**
201
- * 연결에 트랙 추가
202
- * @private
203
- * @param {RTCPeerConnection} connection - 피어 연결
204
- */
205
- _addTracksToConnection(connection) {
206
- if (!this.localStream) return;
207
-
208
- const existingSenders = connection.getSenders();
209
-
210
- this.localStream.getTracks().forEach(track => {
211
- // 이미 추가된 트랙인지 확인
212
- const existingSender = existingSenders.find(sender =>
213
- sender.track && sender.track.kind === track.kind
214
- );
215
-
216
- if (!existingSender) {
217
- connection.addTrack(track, this.localStream);
218
- }
219
- });
220
- }
221
-
222
- /**
223
- * Offer 생성 및 전송
224
- * @param {string} peerId
225
- * @returns {Promise<RTCSessionDescription>}
226
- */
227
- async createOffer(peerId) {
228
- const peer = this.peers.get(peerId);
229
- if (!peer) {
230
- throw new Error(`Peer not found: ${peerId}`);
231
- }
232
-
233
- try {
234
- const offer = await peer.connection.createOffer();
235
- await peer.connection.setLocalDescription(offer);
236
-
237
- this.logger.debug(`Offer created for ${peerId}`);
238
- return peer.connection.localDescription;
239
-
240
- } catch (error) {
241
- this.logger.error(`Failed to create offer for ${peerId}:`, error);
242
- throw error;
243
- }
244
- }
245
-
246
- /**
247
- * Answer 생성
248
- * @param {string} peerId
249
- * @returns {Promise<RTCSessionDescription>}
250
- */
251
- async createAnswer(peerId) {
252
- const peer = this.peers.get(peerId);
253
- if (!peer) {
254
- throw new Error(`Peer not found: ${peerId}`);
255
- }
256
-
257
- try {
258
- const answer = await peer.connection.createAnswer();
259
- await peer.connection.setLocalDescription(answer);
260
-
261
- this.logger.debug(`Answer created for ${peerId}`);
262
- return peer.connection.localDescription;
263
-
264
- } catch (error) {
265
- this.logger.error(`Failed to create answer for ${peerId}:`, error);
266
- throw error;
267
- }
268
- }
269
-
270
- /**
271
- * 원격 Description 설정 (Perfect Negotiation)
272
- * @param {string} peerId
273
- * @param {RTCSessionDescription} description
274
- */
275
- async handleRemoteDescription(peerId, description) {
276
- const peer = this.peers.get(peerId);
277
- if (!peer) {
278
- throw new Error(`Peer not found: ${peerId}`);
279
- }
280
-
281
- const { connection, polite, makingOffer } = peer;
282
-
283
- // Perfect Negotiation: offer 충돌 감지
284
- const offerCollision = description.type === 'offer' &&
285
- (makingOffer || connection.signalingState !== 'stable');
286
-
287
- // Impolite는 충돌 시 상대 offer 무시
288
- peer.ignoreOffer = !polite && offerCollision;
289
-
290
- if (peer.ignoreOffer) {
291
- this.logger.debug(`Ignoring offer collision from ${peerId} (I am impolite)`);
292
- return;
293
- }
294
-
295
- try {
296
- // Perfect Negotiation: Polite가 충돌 시 자신의 offer를 roll back
297
- if (offerCollision && polite) {
298
- this.logger.debug(`Offer collision detected, rolling back local offer for ${peerId} (I am polite)`);
299
- await connection.setLocalDescription({ type: 'rollback' });
300
- }
301
-
302
- peer.isSettingRemoteAnswerPending = description.type === 'answer';
303
- await connection.setRemoteDescription(description);
304
- peer.isSettingRemoteAnswerPending = false;
305
-
306
- this.logger.debug(`Remote ${description.type} set for ${peerId}`);
307
-
308
- // Offer를 받았으면 Answer 생성
309
- if (description.type === 'offer') {
310
- const answer = await this.createAnswer(peerId);
311
- this.emit('answerCreated', { peerId, answer });
312
- }
313
-
314
- } catch (error) {
315
- this.logger.error(`Failed to set remote description for ${peerId}:`, error);
316
- throw error;
317
- }
318
- }
319
-
320
- /**
321
- * ICE Candidate 추가
322
- * @param {string} peerId
323
- * @param {RTCIceCandidate} candidate
324
- * @returns {Promise<boolean>} 추가 성공 여부 (false면 나중에 다시 시도 필요)
325
- */
326
- async addIceCandidate(peerId, candidate) {
327
- const peer = this.peers.get(peerId);
328
- if (!peer) {
329
- this.logger.warn(`Peer not found for ICE candidate: ${peerId}`);
330
- return false;
331
- }
332
-
333
- // remoteDescription이 설정되지 않았으면 추가 불가
334
- if (!peer.connection.remoteDescription) {
335
- this.logger.debug(`Remote description not set yet for ${peerId}, ICE candidate queued`);
336
- return false;
337
- }
338
-
339
- try {
340
- await peer.connection.addIceCandidate(candidate);
341
- this.logger.debug(`ICE candidate added for ${peerId}`);
342
- return true;
343
- } catch (error) {
344
- if (!peer.ignoreOffer) {
345
- this.logger.error(`Failed to add ICE candidate for ${peerId}:`, error);
346
- }
347
- return false;
348
- }
349
- }
350
-
351
- /**
352
- * 특정 피어 연결 가져오기
353
- * @param {string} peerId
354
- * @returns {RTCPeerConnection|null}
355
- */
356
- getPeerConnection(peerId) {
357
- const peer = this.peers.get(peerId);
358
- return peer ? peer.connection : null;
359
- }
360
-
361
- /**
362
- * 모든 피어 ID 목록
363
- * @returns {string[]}
364
- */
365
- getPeerIds() {
366
- return Array.from(this.peers.keys());
367
- }
368
-
369
- /**
370
- * 피어 연결 종료
371
- * @param {string} peerId
372
- */
373
- closePeerConnection(peerId) {
374
- const peer = this.peers.get(peerId);
375
- if (!peer) {
376
- return;
377
- }
378
-
379
- peer.connection.close();
380
- this.peers.delete(peerId);
381
-
382
- this.emit('peerClosed', { peerId });
383
- this.logger.info(`Peer connection closed: ${peerId}`);
384
- }
385
-
386
- /**
387
- * 모든 피어 연결 종료
388
- */
389
- closeAllPeerConnections() {
390
- this.peers.forEach((peer, peerId) => {
391
- peer.connection.close();
392
- this.logger.debug(`Peer connection closed: ${peerId}`);
393
- });
394
- this.peers.clear();
395
- this.emit('allPeersClosed', {});
396
- }
397
-
398
- /**
399
- * 트랙 교체
400
- * @param {MediaStreamTrack} oldTrack - 교체할 기존 트랙
401
- * @param {MediaStreamTrack} newTrack - 새 트랙
402
- * @returns {Promise<void>}
403
- */
404
- async replaceTrack(oldTrack, newTrack) {
405
- const promises = [];
406
-
407
- this.peers.forEach((peer, peerId) => {
408
- const sender = peer.connection.getSenders().find(s =>
409
- s.track && s.track.kind === oldTrack.kind
410
- );
411
-
412
- if (sender) {
413
- promises.push(
414
- sender.replaceTrack(newTrack)
415
- .then(() => this.logger.debug(`Track replaced for ${peerId}`))
416
- .catch(error => this.logger.error(`Failed to replace track for ${peerId}:`, error))
417
- );
418
- }
419
- });
420
-
421
- await Promise.all(promises);
422
- }
423
-
424
- /**
425
- * 연결 통계 가져오기
426
- * @param {string} peerId
427
- * @returns {Promise<RTCStatsReport|null>}
428
- */
429
- async getStats(peerId) {
430
- const peer = this.peers.get(peerId);
431
- if (!peer) {
432
- return null;
433
- }
434
-
435
- return peer.connection.getStats();
436
- }
437
-
438
- /**
439
- * 모든 연결 상태 요약
440
- * @returns {Object}
441
- */
442
- getConnectionSummary() {
443
- const summary = {};
444
-
445
- this.peers.forEach((peer, peerId) => {
446
- summary[peerId] = {
447
- connectionState: peer.connection.connectionState,
448
- iceConnectionState: peer.connection.iceConnectionState,
449
- signalingState: peer.connection.signalingState,
450
- polite: peer.polite
451
- };
452
- });
453
-
454
- return summary;
455
- }
456
-
457
- /**
458
- * 로그 레벨 설정
459
- * @param {number} level
460
- */
461
- setLogLevel(level) {
462
- this.logger.setLevel(level);
463
- }
464
-
465
- /**
466
- * 리소스 정리
467
- */
468
- destroy() {
469
- this.closeAllPeerConnections();
470
- this.localStream = null;
471
- this.removeAllListeners();
472
- this.logger.info('PeerConnectionManager destroyed');
473
- }
474
- }
475
-
476
- export default PeerConnectionManager;