@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.
- package/README.md +3 -2
- package/dist/index.d.ts +693 -155
- package/dist/talkflow-sdk.esm.js +1 -1
- package/dist/talkflow-sdk.esm.js.map +1 -1
- package/dist/talkflow-sdk.standalone.js +1 -1
- package/dist/talkflow-sdk.standalone.js.map +1 -1
- package/dist/talkflow-sdk.umd.js +1 -1
- package/dist/talkflow-sdk.umd.js.map +1 -1
- package/package.json +4 -2
- package/src/TalkFlowClient.js +13 -5
- package/src/chat/ChatClient.js +155 -0
- package/src/constants.js +39 -41
- package/src/core/ConnectionManager.js +11 -1
- package/src/index.js +4 -4
- package/src/talkflow/delegates.js +19 -23
- package/src/talkflow/eventForwarding.js +11 -23
- package/src/talkflow/session.js +37 -4
- package/src/utils/ApiClient.js +45 -41
- package/src/webrtc/CallLedger.js +135 -0
- package/src/webrtc/LiveKitMediaSession.js +1568 -0
- package/src/webrtc/WebRTCClient.js +909 -1068
- package/types/__typetests__/call.types.ts +101 -0
- package/types/__typetests__/scheduledReports.types.ts +138 -0
- package/types/index.d.ts +693 -155
- package/src/webrtc/MediaStreamManager.js +0 -478
- package/src/webrtc/PeerConnectionManager.js +0 -476
|
@@ -1,478 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MediaStreamManager
|
|
3
|
-
* 로컬 미디어 스트림 관리 (카메라/마이크/화면공유)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import EventEmitter from '../utils/EventEmitter.js';
|
|
7
|
-
import Logger from '../utils/Logger.js';
|
|
8
|
-
import { ErrorTypes, LogLevel } from '../constants.js';
|
|
9
|
-
|
|
10
|
-
class MediaStreamManager extends EventEmitter {
|
|
11
|
-
/**
|
|
12
|
-
* @param {Object} options
|
|
13
|
-
* @param {number} [options.logLevel] - 로그 레벨
|
|
14
|
-
*/
|
|
15
|
-
constructor(options = {}) {
|
|
16
|
-
super();
|
|
17
|
-
|
|
18
|
-
this.localStream = null;
|
|
19
|
-
this.screenStream = null;
|
|
20
|
-
this.videoEnabled = true;
|
|
21
|
-
this.audioEnabled = true;
|
|
22
|
-
|
|
23
|
-
this.logger = new Logger(options.logLevel || LogLevel.WARN, 'MediaStreamManager');
|
|
24
|
-
this._deviceChangeHandler = null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 사용자 미디어 가져오기
|
|
29
|
-
* @param {Object} constraints - 미디어 제약조건
|
|
30
|
-
* @returns {Promise<MediaStream>}
|
|
31
|
-
*/
|
|
32
|
-
async getUserMedia(constraints = { video: true, audio: true }) {
|
|
33
|
-
try {
|
|
34
|
-
this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
35
|
-
this.videoEnabled = constraints.video !== false;
|
|
36
|
-
this.audioEnabled = constraints.audio !== false;
|
|
37
|
-
|
|
38
|
-
this.emit('streamStarted', { stream: this.localStream });
|
|
39
|
-
this.logger.info('User media stream started');
|
|
40
|
-
return this.localStream;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
this.logger.error('Failed to get user media:', error);
|
|
43
|
-
this.emit('error', {
|
|
44
|
-
type: ErrorTypes.MEDIA_ACCESS_DENIED,
|
|
45
|
-
message: this._getMediaErrorMessage(error),
|
|
46
|
-
error
|
|
47
|
-
});
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 미디어 에러 메시지 변환
|
|
54
|
-
* @private
|
|
55
|
-
* @param {Error} error - 에러 객체
|
|
56
|
-
* @returns {string} 변환된 에러 메시지
|
|
57
|
-
*/
|
|
58
|
-
_getMediaErrorMessage(error) {
|
|
59
|
-
switch (error.name) {
|
|
60
|
-
case 'NotAllowedError':
|
|
61
|
-
return 'Camera/Microphone permission denied';
|
|
62
|
-
case 'NotFoundError':
|
|
63
|
-
return 'Camera/Microphone not found';
|
|
64
|
-
case 'NotReadableError':
|
|
65
|
-
return 'Camera/Microphone is already in use';
|
|
66
|
-
case 'OverconstrainedError':
|
|
67
|
-
return 'Camera/Microphone constraints cannot be satisfied';
|
|
68
|
-
case 'AbortError':
|
|
69
|
-
return 'Media access aborted';
|
|
70
|
-
default:
|
|
71
|
-
return error.message || 'Unknown media error';
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 화면 공유 가져오기
|
|
77
|
-
* @param {Object} [options] - 화면 공유 옵션
|
|
78
|
-
* @returns {Promise<MediaStream>}
|
|
79
|
-
*/
|
|
80
|
-
async getDisplayMedia(options = { video: true, audio: false }) {
|
|
81
|
-
try {
|
|
82
|
-
this.screenStream = await navigator.mediaDevices.getDisplayMedia(options);
|
|
83
|
-
|
|
84
|
-
// 화면 공유 종료 감지
|
|
85
|
-
this.screenStream.getVideoTracks()[0].onended = () => {
|
|
86
|
-
this.emit('screenShareEnded', {});
|
|
87
|
-
this.screenStream = null;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
this.emit('screenShareStarted', { stream: this.screenStream });
|
|
91
|
-
this.logger.info('Screen share stream started');
|
|
92
|
-
return this.screenStream;
|
|
93
|
-
} catch (error) {
|
|
94
|
-
this.logger.error('Failed to get display media:', error);
|
|
95
|
-
this.emit('error', {
|
|
96
|
-
type: ErrorTypes.SCREEN_SHARE_DENIED,
|
|
97
|
-
message: error.name === 'NotAllowedError'
|
|
98
|
-
? 'Screen share permission denied'
|
|
99
|
-
: error.message,
|
|
100
|
-
error
|
|
101
|
-
});
|
|
102
|
-
throw error;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* 비디오 토글
|
|
108
|
-
* @returns {boolean} 현재 상태
|
|
109
|
-
*/
|
|
110
|
-
toggleVideo() {
|
|
111
|
-
if (!this.localStream) {
|
|
112
|
-
this.logger.warn('No local stream available');
|
|
113
|
-
return this.videoEnabled;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const videoTracks = this.localStream.getVideoTracks();
|
|
117
|
-
if (videoTracks.length === 0) {
|
|
118
|
-
this.logger.warn('No video track available');
|
|
119
|
-
return this.videoEnabled;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this.videoEnabled = !this.videoEnabled;
|
|
123
|
-
videoTracks.forEach(track => {
|
|
124
|
-
track.enabled = this.videoEnabled;
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
this.emit('videoToggled', { enabled: this.videoEnabled });
|
|
128
|
-
this.logger.debug(`Video toggled: ${this.videoEnabled}`);
|
|
129
|
-
return this.videoEnabled;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* 오디오 토글
|
|
134
|
-
* @returns {boolean} 현재 상태
|
|
135
|
-
*/
|
|
136
|
-
toggleAudio() {
|
|
137
|
-
if (!this.localStream) {
|
|
138
|
-
this.logger.warn('No local stream available');
|
|
139
|
-
return this.audioEnabled;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const audioTracks = this.localStream.getAudioTracks();
|
|
143
|
-
if (audioTracks.length === 0) {
|
|
144
|
-
this.logger.warn('No audio track available');
|
|
145
|
-
return this.audioEnabled;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.audioEnabled = !this.audioEnabled;
|
|
149
|
-
audioTracks.forEach(track => {
|
|
150
|
-
track.enabled = this.audioEnabled;
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
this.emit('audioToggled', { enabled: this.audioEnabled });
|
|
154
|
-
this.logger.debug(`Audio toggled: ${this.audioEnabled}`);
|
|
155
|
-
return this.audioEnabled;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* 비디오 상태 설정
|
|
160
|
-
* @param {boolean} enabled
|
|
161
|
-
*/
|
|
162
|
-
setVideoEnabled(enabled) {
|
|
163
|
-
if (!this.localStream) {
|
|
164
|
-
this.logger.warn('No local stream available');
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const videoTracks = this.localStream.getVideoTracks();
|
|
169
|
-
videoTracks.forEach(track => {
|
|
170
|
-
track.enabled = enabled;
|
|
171
|
-
});
|
|
172
|
-
this.videoEnabled = enabled;
|
|
173
|
-
this.emit('videoToggled', { enabled });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 오디오 상태 설정
|
|
178
|
-
* @param {boolean} enabled
|
|
179
|
-
*/
|
|
180
|
-
setAudioEnabled(enabled) {
|
|
181
|
-
if (!this.localStream) {
|
|
182
|
-
this.logger.warn('No local stream available');
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const audioTracks = this.localStream.getAudioTracks();
|
|
187
|
-
audioTracks.forEach(track => {
|
|
188
|
-
track.enabled = enabled;
|
|
189
|
-
});
|
|
190
|
-
this.audioEnabled = enabled;
|
|
191
|
-
this.emit('audioToggled', { enabled });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* 로컬 스트림 가져오기
|
|
196
|
-
* @returns {MediaStream|null}
|
|
197
|
-
*/
|
|
198
|
-
getLocalStream() {
|
|
199
|
-
return this.localStream;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* 화면 공유 스트림 가져오기
|
|
204
|
-
* @returns {MediaStream|null}
|
|
205
|
-
*/
|
|
206
|
-
getScreenStream() {
|
|
207
|
-
return this.screenStream;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 현재 비디오 상태
|
|
212
|
-
* @returns {boolean}
|
|
213
|
-
*/
|
|
214
|
-
isVideoEnabled() {
|
|
215
|
-
return this.videoEnabled;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* 현재 오디오 상태
|
|
220
|
-
* @returns {boolean}
|
|
221
|
-
*/
|
|
222
|
-
isAudioEnabled() {
|
|
223
|
-
return this.audioEnabled;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 모든 트랙 중지
|
|
228
|
-
*/
|
|
229
|
-
stopAll() {
|
|
230
|
-
if (this.localStream) {
|
|
231
|
-
this.localStream.getTracks().forEach(track => track.stop());
|
|
232
|
-
this.localStream = null;
|
|
233
|
-
this.emit('streamStopped', {});
|
|
234
|
-
this.logger.info('All media tracks stopped');
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (this.screenStream) {
|
|
238
|
-
this.screenStream.getTracks().forEach(track => track.stop());
|
|
239
|
-
this.screenStream = null;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* 특정 트랙 교체 (내부용 - switchDevice에서 호출)
|
|
245
|
-
* @private
|
|
246
|
-
* @param {MediaStreamTrack} oldTrack
|
|
247
|
-
* @param {MediaStreamTrack} newTrack
|
|
248
|
-
*/
|
|
249
|
-
replaceTrack(oldTrack, newTrack) {
|
|
250
|
-
if (!this.localStream) {
|
|
251
|
-
this.logger.warn('No local stream available');
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
this.localStream.removeTrack(oldTrack);
|
|
256
|
-
this.localStream.addTrack(newTrack);
|
|
257
|
-
oldTrack.stop();
|
|
258
|
-
|
|
259
|
-
this.emit('trackReplaced', { oldTrack, newTrack });
|
|
260
|
-
this.logger.debug(`Track replaced: ${oldTrack.kind}`);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* 특정 종류의 트랙 가져오기
|
|
265
|
-
* @param {string} kind - 'video' or 'audio'
|
|
266
|
-
* @returns {MediaStreamTrack|null}
|
|
267
|
-
*/
|
|
268
|
-
getTrack(kind) {
|
|
269
|
-
if (!this.localStream) return null;
|
|
270
|
-
|
|
271
|
-
const tracks = kind === 'video'
|
|
272
|
-
? this.localStream.getVideoTracks()
|
|
273
|
-
: this.localStream.getAudioTracks();
|
|
274
|
-
|
|
275
|
-
return tracks.length > 0 ? tracks[0] : null;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* 디바이스 목록 가져오기
|
|
280
|
-
* @returns {Promise<Object>}
|
|
281
|
-
*/
|
|
282
|
-
async getDevices() {
|
|
283
|
-
try {
|
|
284
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
285
|
-
|
|
286
|
-
return {
|
|
287
|
-
videoInputs: devices.filter(d => d.kind === 'videoinput'),
|
|
288
|
-
audioInputs: devices.filter(d => d.kind === 'audioinput'),
|
|
289
|
-
audioOutputs: devices.filter(d => d.kind === 'audiooutput')
|
|
290
|
-
};
|
|
291
|
-
} catch (error) {
|
|
292
|
-
this.logger.error('Failed to enumerate devices:', error);
|
|
293
|
-
this.emit('error', {
|
|
294
|
-
type: ErrorTypes.ENUMERATE_DEVICES_FAILED,
|
|
295
|
-
message: error.message,
|
|
296
|
-
error
|
|
297
|
-
});
|
|
298
|
-
throw error;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* 특정 디바이스로 전환
|
|
304
|
-
* @param {string} deviceId - 디바이스 ID
|
|
305
|
-
* @param {string} kind - 'video' or 'audio'
|
|
306
|
-
* @returns {Promise<MediaStreamTrack>}
|
|
307
|
-
* @throws {Error} 스트림이 없거나 디바이스 전환 실패 시
|
|
308
|
-
*/
|
|
309
|
-
async switchDevice(deviceId, kind) {
|
|
310
|
-
if (!this.localStream) {
|
|
311
|
-
throw new Error('No local stream available');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
try {
|
|
315
|
-
const constraints = kind === 'video'
|
|
316
|
-
? { video: { deviceId: { exact: deviceId } }, audio: false }
|
|
317
|
-
: { video: false, audio: { deviceId: { exact: deviceId } } };
|
|
318
|
-
|
|
319
|
-
const newStream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
320
|
-
const newTrack = newStream.getTracks()[0];
|
|
321
|
-
|
|
322
|
-
const oldTrack = kind === 'video'
|
|
323
|
-
? this.localStream.getVideoTracks()[0]
|
|
324
|
-
: this.localStream.getAudioTracks()[0];
|
|
325
|
-
|
|
326
|
-
if (oldTrack) {
|
|
327
|
-
this.replaceTrack(oldTrack, newTrack);
|
|
328
|
-
|
|
329
|
-
this.emit('deviceSwitched', {
|
|
330
|
-
kind,
|
|
331
|
-
deviceId,
|
|
332
|
-
newTrack
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
this.logger.info(`${kind} device switched to ${deviceId}`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return newTrack;
|
|
339
|
-
|
|
340
|
-
} catch (error) {
|
|
341
|
-
this.logger.error(`Failed to switch ${kind} device:`, error);
|
|
342
|
-
this.emit('error', {
|
|
343
|
-
type: ErrorTypes.DEVICE_SWITCH_FAILED,
|
|
344
|
-
message: error.message,
|
|
345
|
-
error
|
|
346
|
-
});
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* 디바이스 변경 감지 시작
|
|
353
|
-
*/
|
|
354
|
-
startDeviceChangeDetection() {
|
|
355
|
-
if (this._deviceChangeHandler) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
this._deviceChangeHandler = async () => {
|
|
360
|
-
try {
|
|
361
|
-
const devices = await this.getDevices();
|
|
362
|
-
this.emit('deviceChange', devices);
|
|
363
|
-
this.logger.debug('Device change detected');
|
|
364
|
-
} catch (error) {
|
|
365
|
-
this.logger.warn('Failed to get devices on change:', error);
|
|
366
|
-
}
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
navigator.mediaDevices.addEventListener('devicechange', this._deviceChangeHandler);
|
|
370
|
-
this.logger.debug('Device change detection started');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* 디바이스 변경 감지 중지
|
|
375
|
-
*/
|
|
376
|
-
stopDeviceChangeDetection() {
|
|
377
|
-
if (this._deviceChangeHandler) {
|
|
378
|
-
navigator.mediaDevices.removeEventListener('devicechange', this._deviceChangeHandler);
|
|
379
|
-
this._deviceChangeHandler = null;
|
|
380
|
-
this.logger.debug('Device change detection stopped');
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* 스트림 정보
|
|
386
|
-
* @returns {Object}
|
|
387
|
-
*/
|
|
388
|
-
getStreamInfo() {
|
|
389
|
-
if (!this.localStream) {
|
|
390
|
-
return {
|
|
391
|
-
hasStream: false,
|
|
392
|
-
videoEnabled: this.videoEnabled,
|
|
393
|
-
audioEnabled: this.audioEnabled,
|
|
394
|
-
tracks: []
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return {
|
|
399
|
-
hasStream: true,
|
|
400
|
-
videoEnabled: this.videoEnabled,
|
|
401
|
-
audioEnabled: this.audioEnabled,
|
|
402
|
-
tracks: this.localStream.getTracks().map(track => ({
|
|
403
|
-
kind: track.kind,
|
|
404
|
-
id: track.id,
|
|
405
|
-
label: track.label,
|
|
406
|
-
enabled: track.enabled,
|
|
407
|
-
readyState: track.readyState,
|
|
408
|
-
muted: track.muted
|
|
409
|
-
}))
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* 비디오 해상도 변경
|
|
415
|
-
* @param {Object} constraints - { width, height, frameRate }
|
|
416
|
-
*/
|
|
417
|
-
async applyVideoConstraints(constraints) {
|
|
418
|
-
if (!this.localStream) {
|
|
419
|
-
throw new Error('No local stream available');
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const videoTrack = this.localStream.getVideoTracks()[0];
|
|
423
|
-
if (!videoTrack) {
|
|
424
|
-
throw new Error('No video track available');
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
try {
|
|
428
|
-
await videoTrack.applyConstraints(constraints);
|
|
429
|
-
this.emit('videoConstraintsApplied', { constraints });
|
|
430
|
-
this.logger.info('Video constraints applied:', constraints);
|
|
431
|
-
} catch (error) {
|
|
432
|
-
this.logger.error('Failed to apply video constraints:', error);
|
|
433
|
-
throw error;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* 현재 비디오 설정 가져오기
|
|
439
|
-
* @returns {MediaTrackSettings|null}
|
|
440
|
-
*/
|
|
441
|
-
getVideoSettings() {
|
|
442
|
-
if (!this.localStream) return null;
|
|
443
|
-
|
|
444
|
-
const videoTrack = this.localStream.getVideoTracks()[0];
|
|
445
|
-
return videoTrack ? videoTrack.getSettings() : null;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* 현재 오디오 설정 가져오기
|
|
450
|
-
* @returns {MediaTrackSettings|null}
|
|
451
|
-
*/
|
|
452
|
-
getAudioSettings() {
|
|
453
|
-
if (!this.localStream) return null;
|
|
454
|
-
|
|
455
|
-
const audioTrack = this.localStream.getAudioTracks()[0];
|
|
456
|
-
return audioTrack ? audioTrack.getSettings() : null;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* 로그 레벨 설정
|
|
461
|
-
* @param {number} level
|
|
462
|
-
*/
|
|
463
|
-
setLogLevel(level) {
|
|
464
|
-
this.logger.setLevel(level);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
/**
|
|
468
|
-
* 리소스 정리
|
|
469
|
-
*/
|
|
470
|
-
destroy() {
|
|
471
|
-
this.stopAll();
|
|
472
|
-
this.stopDeviceChangeDetection();
|
|
473
|
-
this.removeAllListeners();
|
|
474
|
-
this.logger.info('MediaStreamManager destroyed');
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
export default MediaStreamManager;
|