@ermis-network/ermis-chat-sdk 1.0.2 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ermis-network/ermis-chat-sdk",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Ermis Chat SDK",
5
5
  "author": "Ermis",
6
6
  "homepage": "https://ermis.network/",
@@ -26,6 +26,9 @@
26
26
  "require": "./dist/index.cjs"
27
27
  }
28
28
  },
29
+ "bin": {
30
+ "ermis-init-call": "./bin/init-call.js"
31
+ },
29
32
  "jsdelivr": "./dist/index.browser.full-bundle.min.js",
30
33
  "license": "SEE LICENSE IN LICENSE",
31
34
  "keywords": [
@@ -40,7 +43,9 @@
40
43
  "/dist",
41
44
  "/src",
42
45
  "readme.md",
43
- "license"
46
+ "license",
47
+ "/bin",
48
+ "/public"
44
49
  ],
45
50
  "dependencies": {
46
51
  "@types/event-source-polyfill": "^1.0.5",
Binary file
Binary file
@@ -335,28 +335,46 @@ export class ErmisCallNode<ErmisChatGenerics extends ExtendableGenerics = Defaul
335
335
  const mediaConstraints = await this.getMediaConstraints();
336
336
 
337
337
  try {
338
- // Request the media stream with the determined constraints
339
338
  const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
340
- if (this.callStatus === CallStatus.ENDED) {
341
- // If the call has ended, stop the local stream tracks
342
- stream.getTracks().forEach((track) => track.stop());
343
- this.destroy();
344
- return;
339
+ return this.applyLocalStream(stream);
340
+ } catch (error: any) {
341
+ console.warn('Error getting user media:', error?.message);
342
+
343
+ // Video call: try fallback to audio-only (camera not available)
344
+ if (this.callType === 'video' && mediaConstraints.video) {
345
+ try {
346
+ const audioOnlyStream = await navigator.mediaDevices.getUserMedia({
347
+ audio: mediaConstraints.audio,
348
+ video: false,
349
+ });
350
+ this.setConnectionMessage('Camera not available, using audio only');
351
+ return this.applyLocalStream(audioOnlyStream);
352
+ } catch {
353
+ // Audio fallback also failed
354
+ }
345
355
  }
346
- if (this.onLocalStream) {
347
- this.onLocalStream(stream);
356
+
357
+ // No device found at all — report error
358
+ if (typeof this.onError === 'function') {
359
+ this.onError('No microphone or camera found. Please check your device.');
348
360
  }
349
- this.localStream = stream;
350
- return stream;
351
- } catch (error) {
352
- console.error('Error getting user media:', error);
353
- // if (typeof this.onError === 'function') {
354
- // this.onError('Unable to access microphone/camera');
355
- // }
356
361
  return null;
357
362
  }
358
363
  }
359
364
 
365
+ private applyLocalStream(stream: MediaStream) {
366
+ if (this.callStatus === CallStatus.ENDED) {
367
+ stream.getTracks().forEach((track) => track.stop());
368
+ this.destroy();
369
+ return;
370
+ }
371
+ if (this.onLocalStream) {
372
+ this.onLocalStream(stream);
373
+ }
374
+ this.localStream = stream;
375
+ return stream;
376
+ }
377
+
360
378
  private setConnectionMessage(message: string | null) {
361
379
  if (typeof this.onConnectionMessageChange === 'function') {
362
380
  this.onConnectionMessageChange(message);
@@ -415,23 +433,12 @@ export class ErmisCallNode<ErmisChatGenerics extends ExtendableGenerics = Defaul
415
433
  this.isDestroyed = false;
416
434
  this.callStatus = '';
417
435
  this.callType = is_video ? 'video' : 'audio';
418
- await this.startLocalStream();
419
- if (this.callStatus === CallStatus.ENDED) return;
436
+
420
437
  this.setUserInfo(cid, eventUserId);
421
438
  this.setCallStatus(CallStatus.RINGING);
422
439
  this.cid = cid || '';
423
440
  this.metadata = metadata || {};
424
441
 
425
- console.log('----metadata---', metadata);
426
- if (eventUserId !== this.userID) {
427
- await this.initialize();
428
- }
429
-
430
- if (this.localStream && this.mediaSender && this.mediaReceiver) {
431
- this.mediaSender?.initEncoders(this.localStream);
432
- this.mediaReceiver?.initDecoders(this.callType);
433
- }
434
-
435
442
  if (typeof this.onCallEvent === 'function') {
436
443
  this.onCallEvent({
437
444
  type: eventUserId !== this.userID ? 'incoming' : 'outgoing',
@@ -443,6 +450,18 @@ export class ErmisCallNode<ErmisChatGenerics extends ExtendableGenerics = Defaul
443
450
  });
444
451
  }
445
452
 
453
+ await this.startLocalStream();
454
+ if (this.callStatus === CallStatus.ENDED) return;
455
+
456
+ if (eventUserId !== this.userID) {
457
+ await this.initialize();
458
+ }
459
+
460
+ if (this.localStream && this.mediaSender && this.mediaReceiver) {
461
+ this.mediaSender?.initEncoders(this.localStream);
462
+ this.mediaReceiver?.initDecoders(this.callType);
463
+ }
464
+
446
465
  if (eventUserId === this.userID) {
447
466
  // Set missCall timeout if no connection after 60s
448
467
  if (this.missCallTimeout) clearTimeout(this.missCallTimeout);
@@ -782,29 +801,40 @@ export class ErmisCallNode<ErmisChatGenerics extends ExtendableGenerics = Defaul
782
801
  public async stopScreenShare() {
783
802
  const mediaConstraints = await this.getMediaConstraints();
784
803
 
785
- const cameraStream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
786
- const cameraTrack = cameraStream.getVideoTracks()[0];
804
+ try {
805
+ // Only request video; we already have an active audio track in localStream
806
+ const cameraStream = await navigator.mediaDevices.getUserMedia({
807
+ video: mediaConstraints.video,
808
+ audio: false,
809
+ });
810
+ const cameraTrack = cameraStream.getVideoTracks()[0];
787
811
 
788
- // Replace video track in localStream
789
- if (this.localStream) {
790
- // Stop old track (screen)
791
- this.localStream.getVideoTracks().forEach((track) => track.stop());
792
- // Replace with camera track
793
- this.localStream.removeTrack(this.localStream.getVideoTracks()[0]);
794
- this.localStream.addTrack(cameraTrack);
795
- } else {
796
- this.localStream = cameraStream;
797
- }
812
+ // Replace video track in localStream
813
+ if (this.localStream) {
814
+ // Stop old screen tracks
815
+ this.localStream.getVideoTracks().forEach((track) => {
816
+ track.stop();
817
+ this.localStream?.removeTrack(track);
818
+ });
819
+
820
+ // Add new camera track
821
+ this.localStream.addTrack(cameraTrack);
822
+ } else {
823
+ this.localStream = cameraStream;
824
+ }
798
825
 
799
- // Call callback if UI needs to update
800
- if (this.onLocalStream) {
801
- this.onLocalStream(this.localStream);
802
- this.mediaSender?.replaceVideoTrack(this.localStream.getVideoTracks()[0]);
803
- }
826
+ // Call callback if UI needs to update
827
+ if (this.onLocalStream) {
828
+ this.onLocalStream(this.localStream);
829
+ this.mediaSender?.replaceVideoTrack(this.localStream.getVideoTracks()[0]);
830
+ }
804
831
 
805
- // Call callback when screen sharing stops
806
- if (typeof this.onScreenShareChange === 'function') {
807
- this.onScreenShareChange(false);
832
+ // Call callback when screen sharing stops
833
+ if (typeof this.onScreenShareChange === 'function') {
834
+ this.onScreenShareChange(false);
835
+ }
836
+ } catch (error) {
837
+ console.error('Error stopping screen share and reverting to camera:', error);
808
838
  }
809
839
  }
810
840
 
package/src/index.ts CHANGED
@@ -10,6 +10,7 @@ export * from './ermis_call_node';
10
10
  export * from './auth';
11
11
  export { chatCodes, logChatPromiseExecution, formatMessage, createForwardMessagePayload } from './utils';
12
12
  export { parseSystemMessage } from './system_message';
13
- export { parseSignalMessage } from './signal_message';
13
+ export { parseSignalMessage, CallType } from './signal_message';
14
+ export type { SignalMessageResult, CallTypeValue } from './signal_message';
14
15
  export { normalizeFileName, getAttachmentCategory, isVideoFile, isHeicFile, buildAttachmentPayload } from './attachment_utils';
15
16
  export type { VoiceRecordingMeta } from './attachment_utils';
@@ -1,96 +1,146 @@
1
1
  /**
2
- * Convert duration in seconds to mm:ss format.
2
+ * Call type constants for signal messages.
3
3
  */
4
- function formatDuration(durationSec: string): string {
5
- const sec = parseInt(durationSec, 10);
6
- if (isNaN(sec) || sec < 0) return durationSec;
7
- const minutes = Math.floor(sec / 60);
8
- const seconds = sec % 60;
9
- return `${minutes}:${seconds.toString().padStart(2, '0')}`;
4
+ export const CallType = {
5
+ AUDIO: 'audio',
6
+ VIDEO: 'video',
7
+ } as const;
8
+
9
+ export type CallTypeValue = (typeof CallType)[keyof typeof CallType];
10
+
11
+ /**
12
+ * Result of parsing a signal message.
13
+ */
14
+ export interface SignalMessageResult {
15
+ text: string;
16
+ duration: string;
17
+ callType: CallTypeValue | '';
18
+ color: string;
10
19
  }
11
20
 
12
21
  /**
13
- * Resolve a user ID to a display name using the provided map.
14
- * Falls back to the raw userId if no entry is found.
22
+ * Format duration from milliseconds to "X min, Y sec" format.
15
23
  */
16
- function resolveUser(userId: string, userMap: Record<string, string>): string {
17
- return userMap[userId] ?? userId;
24
+ function formatDuration(durationMs: string): string {
25
+ if (!durationMs) return '';
26
+ const ms = parseInt(durationMs, 10);
27
+ if (isNaN(ms) || ms <= 0) return '';
28
+ const totalSeconds = Math.floor(ms / 1000);
29
+ const minutes = Math.floor(totalSeconds / 60);
30
+ const seconds = totalSeconds % 60;
31
+ return `${minutes} min, ${seconds} sec`;
18
32
  }
19
33
 
20
34
  /**
21
- * Parse a raw signal message string into a human-readable English sentence.
35
+ * Parse a raw signal message string into a structured object
36
+ * containing text, duration, call type, and color.
22
37
  *
23
38
  * Signal messages represent call events. The raw format is:
24
- * `"<formatId> <userID> [<param1> <param2> ...]"`
39
+ * `"<formatId> <callerId> [<enderId> <duration>]"`
25
40
  *
26
- * @param value - Raw signal message string from the server
27
- * @param userMap - Mapping of user IDs display names
28
- * @returns Parsed English text, or the original string if unknown
41
+ * @param value - Raw signal message string from the server
42
+ * @param myUserId - The current user's ID (from client.userID)
43
+ * @returns Parsed signal message object, or null if input is empty
29
44
  */
30
45
  export function parseSignalMessage(
31
46
  value: string,
32
- userMap: Record<string, string>,
33
- ): string {
34
- if (!value || typeof value !== 'string') return value ?? '';
47
+ myUserId: string,
48
+ ): SignalMessageResult | null {
49
+ if (!value || typeof value !== 'string') return null;
35
50
 
36
51
  const trimmed = value.trim();
37
- if (!trimmed) return '';
52
+ if (!trimmed) return null;
38
53
 
39
54
  const parts = trimmed.split(' ');
40
- const formatId = parts[0];
41
- const userId = parts[1] ?? '';
42
- const userName = userId ? resolveUser(userId, userMap) : 'User';
43
-
44
- switch (formatId) {
45
- // 1: Audio call started
46
- case '1':
47
- return `📞 ${userName} started an audio call.`;
48
-
49
- // 2: Audio call missed
50
- case '2':
51
- return `📞 Missed audio call from ${userName}.`;
52
-
53
- // 3: Audio call ended (caller_id ender_id duration)
54
- case '3': {
55
- const enderId = parts[2] ?? '';
56
- const duration = parts[3] ?? '0';
57
- const enderName = enderId ? resolveUser(enderId, userMap) : 'User';
58
- return `📞 Audio call by ${userName}, ended by ${enderName}. Duration: ${formatDuration(duration)}.`;
59
- }
55
+ const number = parseInt(parts[0], 10);
56
+ const callerId = parts[1] ?? '';
57
+ const isMe = myUserId === callerId;
60
58
 
61
- // 4: Video call started
62
- case '4':
63
- return `📹 ${userName} started a video call.`;
59
+ let enderId = '';
60
+ let duration = '';
61
+ let callType: CallTypeValue | '' = '';
62
+ let color = '';
64
63
 
65
- // 5: Video call missed
66
- case '5':
67
- return `📹 Missed video call from ${userName}.`;
68
-
69
- // 6: Video call ended (caller_id ender_id duration)
70
- case '6': {
71
- const enderId = parts[2] ?? '';
72
- const duration = parts[3] ?? '0';
73
- const enderName = enderId ? resolveUser(enderId, userMap) : 'User';
74
- return `📹 Video call by ${userName}, ended by ${enderName}. Duration: ${formatDuration(duration)}.`;
75
- }
76
-
77
- // 7: Audio call rejected
78
- case '7':
79
- return `📞 Audio call from ${userName} was rejected.`;
80
-
81
- // 8: Video call rejected
82
- case '8':
83
- return `📹 Video call from ${userName} was rejected.`;
84
-
85
- // 9: Audio call busy
86
- case '9':
87
- return `📞 Audio call from ${userName} — recipient was busy.`;
88
-
89
- // 10: Video call busy
90
- case '10':
91
- return `📹 Video call from ${userName} — recipient was busy.`;
64
+ if (number === 3 || number === 6) {
65
+ enderId = parts[2] ?? '';
66
+ duration = parts[3] === '0' ? '' : (parts[3] ?? '');
67
+ }
92
68
 
69
+ let text: string;
70
+ switch (number) {
71
+ case 1: // AudioCallStarted
72
+ text = isMe ? 'Calling...' : 'Incoming audio call...';
73
+ callType = CallType.AUDIO;
74
+ color = '#54D62C';
75
+ break;
76
+ case 2: // AudioCallMissed
77
+ text = isMe ? 'Outgoing audio call' : 'You missed audio call';
78
+ callType = CallType.AUDIO;
79
+ color = '#FF4842';
80
+ break;
81
+ case 3: // AudioCallEnded
82
+ if (duration) {
83
+ text = isMe ? 'Outgoing audio call' : 'Incoming audio call';
84
+ color = '#54D62C';
85
+ } else {
86
+ if (enderId === myUserId) {
87
+ text = 'You cancel audio call';
88
+ } else {
89
+ text = 'You missed audio call';
90
+ }
91
+ color = '#FF4842';
92
+ }
93
+ callType = CallType.AUDIO;
94
+ break;
95
+ case 4: // VideoCallStarted
96
+ text = isMe ? 'Calling...' : 'Incoming video call...';
97
+ callType = CallType.VIDEO;
98
+ color = '#54D62C';
99
+ break;
100
+ case 5: // VideoCallMissed
101
+ text = isMe ? 'Outgoing video call' : 'You missed video call';
102
+ callType = CallType.VIDEO;
103
+ color = '#FF4842';
104
+ break;
105
+ case 6: // VideoCallEnded
106
+ if (duration) {
107
+ text = isMe ? 'Outgoing video call' : 'Incoming video call';
108
+ color = '#54D62C';
109
+ } else {
110
+ if (enderId === myUserId) {
111
+ text = 'You cancel video call';
112
+ } else {
113
+ text = 'You missed video call';
114
+ }
115
+ color = '#FF4842';
116
+ }
117
+ callType = CallType.VIDEO;
118
+ break;
119
+ case 7: // AudioCallRejected
120
+ text = isMe ? 'Recipient rejected audio call' : 'You rejected audio call';
121
+ callType = CallType.AUDIO;
122
+ color = '#FF4842';
123
+ break;
124
+ case 8: // VideoCallRejected
125
+ text = isMe ? 'Recipient rejected video call' : 'You rejected video call';
126
+ callType = CallType.VIDEO;
127
+ color = '#FF4842';
128
+ break;
129
+ case 9: // AudioCallBusy
130
+ text = isMe ? 'Recipient was busy' : 'You missed audio call';
131
+ callType = CallType.AUDIO;
132
+ color = '#FF4842';
133
+ break;
134
+ case 10: // VideoCallBusy
135
+ text = isMe ? 'Recipient was busy' : 'You missed video call';
136
+ callType = CallType.VIDEO;
137
+ color = '#FF4842';
138
+ break;
93
139
  default:
94
- return trimmed;
140
+ text = trimmed;
141
+ callType = '';
142
+ color = '';
95
143
  }
144
+
145
+ return { text, duration: formatDuration(duration), callType, color };
96
146
  }