@softtynet/zca-js 2.3.4 → 2.3.5

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.
Files changed (123) hide show
  1. package/README.md +47 -11
  2. package/dist/apis/call/addUserGroupCall.d.ts +11 -0
  3. package/dist/apis/call/addUserGroupCall.js +48 -0
  4. package/dist/apis/call/answerAckVoiceCall.d.ts +5 -0
  5. package/dist/apis/call/answerAckVoiceCall.js +38 -0
  6. package/dist/apis/call/answerGroupCall.d.ts +10 -0
  7. package/dist/apis/call/answerGroupCall.js +45 -0
  8. package/dist/apis/call/answerVoiceCall.d.ts +11 -0
  9. package/dist/apis/call/answerVoiceCall.js +50 -0
  10. package/dist/apis/call/broadcastGroupCallState.d.ts +10 -0
  11. package/dist/apis/call/broadcastGroupCallState.js +45 -0
  12. package/dist/apis/call/cancelGroupCall.d.ts +10 -0
  13. package/dist/apis/call/cancelGroupCall.js +45 -0
  14. package/dist/apis/call/cancelVoiceCall.d.ts +1 -1
  15. package/dist/apis/call/cancelVoiceCall.js +5 -1
  16. package/dist/apis/call/createController.d.ts +7 -0
  17. package/dist/apis/call/createController.js +9 -0
  18. package/dist/apis/call/createRtcSession.d.ts +8 -0
  19. package/dist/apis/call/createRtcSession.js +10 -0
  20. package/dist/apis/call/endGroupCall.d.ts +10 -0
  21. package/dist/apis/call/endGroupCall.js +45 -0
  22. package/dist/apis/call/finishGroupCall.d.ts +12 -0
  23. package/dist/apis/call/finishGroupCall.js +47 -0
  24. package/dist/apis/call/getGroupCallState.d.ts +3 -0
  25. package/dist/apis/call/getGroupCallState.js +38 -0
  26. package/dist/apis/call/holdVoiceCall.d.ts +5 -0
  27. package/dist/apis/call/holdVoiceCall.js +34 -0
  28. package/dist/apis/call/joinGroupCall.d.ts +11 -0
  29. package/dist/apis/call/joinGroupCall.js +46 -0
  30. package/dist/apis/call/logEndVoiceCall.d.ts +8 -0
  31. package/dist/apis/call/logEndVoiceCall.js +46 -0
  32. package/dist/apis/call/notifyGroupCall.d.ts +9 -0
  33. package/dist/apis/call/notifyGroupCall.js +44 -0
  34. package/dist/apis/call/pingGroupCall.d.ts +10 -0
  35. package/dist/apis/call/pingGroupCall.js +45 -0
  36. package/dist/apis/call/requestAudioToVideoCall.d.ts +5 -0
  37. package/dist/apis/call/requestAudioToVideoCall.js +37 -0
  38. package/dist/apis/call/requestGroupCall.d.ts +9 -0
  39. package/dist/apis/call/requestGroupCall.js +46 -0
  40. package/dist/apis/call/requestShareScreenCall.d.ts +5 -0
  41. package/dist/apis/call/requestShareScreenCall.js +37 -0
  42. package/dist/apis/call/requestVoiceCall.d.ts +10 -0
  43. package/dist/apis/call/requestVoiceCall.js +47 -0
  44. package/dist/apis/call/ringRingGroupCall.d.ts +10 -0
  45. package/dist/apis/call/ringRingGroupCall.js +45 -0
  46. package/dist/apis/call/ringRingVoiceCall.d.ts +5 -0
  47. package/dist/apis/call/ringRingVoiceCall.js +38 -0
  48. package/dist/apis/call/sendRequestGroupCall.d.ts +11 -0
  49. package/dist/apis/call/sendRequestGroupCall.js +48 -0
  50. package/dist/apis/call/sendRequestVoiceCall.d.ts +11 -0
  51. package/dist/apis/call/sendRequestVoiceCall.js +51 -0
  52. package/dist/apis/call/sendVoiceCall.d.ts +8 -1
  53. package/dist/apis/call/sendVoiceCall.js +10 -8
  54. package/dist/apis/call/shareScreenGroupCall.d.ts +10 -0
  55. package/dist/apis/call/shareScreenGroupCall.js +45 -0
  56. package/dist/apis/call/unholdVoiceCall.d.ts +5 -0
  57. package/dist/apis/call/unholdVoiceCall.js +34 -0
  58. package/dist/apis/call/updateCallMediaState.d.ts +7 -0
  59. package/dist/apis/call/updateCallMediaState.js +61 -0
  60. package/dist/apis.d.ts +81 -0
  61. package/dist/apis.js +135 -0
  62. package/dist/call-engine/ZRTPPacketCodec.d.ts +262 -0
  63. package/dist/call-engine/ZRTPPacketCodec.js +506 -0
  64. package/dist/call-engine/ZaloCallController.d.ts +134 -0
  65. package/dist/call-engine/ZaloCallController.js +638 -0
  66. package/dist/call-engine/ZaloCallTransport.d.ts +83 -0
  67. package/dist/call-engine/ZaloCallTransport.js +233 -0
  68. package/dist/call-engine/ZaloRtcSession.d.ts +609 -0
  69. package/dist/call-engine/ZaloRtcSession.js +2931 -0
  70. package/dist/call-engine/ZaloSrtpSession.d.ts +80 -0
  71. package/dist/call-engine/ZaloSrtpSession.js +278 -0
  72. package/dist/call-engine/index.d.ts +18 -0
  73. package/dist/call-engine/index.js +24 -0
  74. package/dist/cjs/apis/call/addUserGroupCall.cjs +53 -0
  75. package/dist/cjs/apis/call/answerAckVoiceCall.cjs +43 -0
  76. package/dist/cjs/apis/call/answerGroupCall.cjs +50 -0
  77. package/dist/cjs/apis/call/answerVoiceCall.cjs +55 -0
  78. package/dist/cjs/apis/call/broadcastGroupCallState.cjs +50 -0
  79. package/dist/cjs/apis/call/cancelGroupCall.cjs +50 -0
  80. package/dist/cjs/apis/call/cancelVoiceCall.cjs +5 -1
  81. package/dist/cjs/apis/call/createController.cjs +14 -0
  82. package/dist/cjs/apis/call/createRtcSession.cjs +15 -0
  83. package/dist/cjs/apis/call/endGroupCall.cjs +50 -0
  84. package/dist/cjs/apis/call/finishGroupCall.cjs +52 -0
  85. package/dist/cjs/apis/call/getGroupCallState.cjs +43 -0
  86. package/dist/cjs/apis/call/holdVoiceCall.cjs +39 -0
  87. package/dist/cjs/apis/call/joinGroupCall.cjs +51 -0
  88. package/dist/cjs/apis/call/logEndVoiceCall.cjs +51 -0
  89. package/dist/cjs/apis/call/notifyGroupCall.cjs +49 -0
  90. package/dist/cjs/apis/call/pingGroupCall.cjs +50 -0
  91. package/dist/cjs/apis/call/requestAudioToVideoCall.cjs +42 -0
  92. package/dist/cjs/apis/call/requestGroupCall.cjs +51 -0
  93. package/dist/cjs/apis/call/requestShareScreenCall.cjs +42 -0
  94. package/dist/cjs/apis/call/requestVoiceCall.cjs +52 -0
  95. package/dist/cjs/apis/call/ringRingGroupCall.cjs +50 -0
  96. package/dist/cjs/apis/call/ringRingVoiceCall.cjs +43 -0
  97. package/dist/cjs/apis/call/sendRequestGroupCall.cjs +53 -0
  98. package/dist/cjs/apis/call/sendRequestVoiceCall.cjs +56 -0
  99. package/dist/cjs/apis/call/sendVoiceCall.cjs +10 -8
  100. package/dist/cjs/apis/call/shareScreenGroupCall.cjs +50 -0
  101. package/dist/cjs/apis/call/unholdVoiceCall.cjs +39 -0
  102. package/dist/cjs/apis/call/updateCallMediaState.cjs +66 -0
  103. package/dist/cjs/apis.cjs +135 -0
  104. package/dist/cjs/call-engine/ZRTPPacketCodec.cjs +525 -0
  105. package/dist/cjs/call-engine/ZaloCallController.cjs +643 -0
  106. package/dist/cjs/call-engine/ZaloCallTransport.cjs +239 -0
  107. package/dist/cjs/call-engine/ZaloRtcSession.cjs +2937 -0
  108. package/dist/cjs/call-engine/ZaloSrtpSession.cjs +287 -0
  109. package/dist/cjs/constants/protocol.cjs +1 -1
  110. package/dist/cjs/index.cjs +61 -11
  111. package/dist/cjs/models/CallEvent.cjs +95 -1
  112. package/dist/cjs/realtime/handlers/control.handler.cjs +17 -3
  113. package/dist/cjs/realtime/listener.cjs +19 -0
  114. package/dist/constants/protocol.d.ts +1 -1
  115. package/dist/constants/protocol.js +1 -1
  116. package/dist/index.d.ts +32 -4
  117. package/dist/index.js +2 -0
  118. package/dist/models/CallEvent.d.ts +15 -0
  119. package/dist/models/CallEvent.js +95 -1
  120. package/dist/realtime/handlers/control.handler.js +17 -3
  121. package/dist/realtime/listener.d.ts +5 -0
  122. package/dist/realtime/listener.js +19 -0
  123. package/package.json +2 -1
@@ -0,0 +1,2937 @@
1
+ 'use strict';
2
+
3
+ var dgram = require('node:dgram');
4
+ var EventEmitter = require('node:events');
5
+ var node_buffer = require('node:buffer');
6
+ var sendVoiceCall = require('../apis/call/sendVoiceCall.cjs');
7
+ var ZaloApiError = require('../Errors/ZaloApiError.cjs');
8
+ var CallEvent = require('../models/CallEvent.cjs');
9
+ var ZRTPPacketCodec = require('./ZRTPPacketCodec.cjs');
10
+ var ZaloSrtpSession = require('./ZaloSrtpSession.cjs');
11
+
12
+ function startsWithAnnexBStartCode(buf) {
13
+ if (!buf || buf.length < 3)
14
+ return false;
15
+ let zeroCount = 0;
16
+ let i = 0;
17
+ while (i < buf.length && buf[i] === 0x00) {
18
+ zeroCount++;
19
+ i++;
20
+ }
21
+ return zeroCount >= 2 && i < buf.length && buf[i] === 0x01;
22
+ }
23
+ function stripLeadingAnnexBStartCode(buf) {
24
+ if (!buf || buf.length === 0)
25
+ return buf;
26
+ let zeroCount = 0;
27
+ let i = 0;
28
+ while (i < buf.length && buf[i] === 0x00) {
29
+ zeroCount++;
30
+ i++;
31
+ }
32
+ if (zeroCount >= 2 && i < buf.length && buf[i] === 0x01) {
33
+ return buf.subarray(i + 1);
34
+ }
35
+ return buf;
36
+ }
37
+ function splitAnnexBNals(buf) {
38
+ const nals = [];
39
+ let i = 0;
40
+ let nalStart = -1;
41
+ while (i < buf.length) {
42
+ if (buf[i] === 0x00 && i + 2 < buf.length && buf[i + 1] === 0x00) {
43
+ let j = i;
44
+ while (j < buf.length && buf[j] === 0x00) {
45
+ j++;
46
+ }
47
+ if (j < buf.length && buf[j] === 0x01 && j - i >= 2) {
48
+ if (nalStart !== -1 && i > nalStart) {
49
+ nals.push(buf.subarray(nalStart, i));
50
+ }
51
+ nalStart = j + 1;
52
+ i = j + 1;
53
+ continue;
54
+ }
55
+ }
56
+ i++;
57
+ }
58
+ if (nalStart !== -1 && nalStart < buf.length) {
59
+ nals.push(buf.subarray(nalStart));
60
+ }
61
+ else if (nals.length === 0 && buf.length > 0) {
62
+ nals.push(buf);
63
+ }
64
+ return nals;
65
+ }
66
+ function tryParseAggregatedNals(buf, startOffset) {
67
+ if (!buf || startOffset >= buf.length)
68
+ return null;
69
+ const chunks = [];
70
+ let offset = startOffset;
71
+ while (offset + 2 <= buf.length) {
72
+ const nalSize = buf.readUInt16BE(offset);
73
+ offset += 2;
74
+ if (nalSize <= 0 || offset + nalSize > buf.length) {
75
+ return null;
76
+ }
77
+ chunks.push(buf.subarray(offset, offset + nalSize));
78
+ offset += nalSize;
79
+ }
80
+ return offset === buf.length && chunks.length > 0 ? chunks : null;
81
+ }
82
+ function isZaloVideoPayloadType(pt) {
83
+ return pt === 97 || pt === 98 || pt === 121 || pt === 122 || pt === 125 || pt === 126;
84
+ }
85
+ function isZaloKeyframePayloadType(pt) {
86
+ return pt === 98 || pt === 121 || pt === 126;
87
+ }
88
+ function parseEndpoint(addr) {
89
+ if (!addr)
90
+ return { address: "", host: "127.0.0.1", port: 0 };
91
+ const idx = addr.lastIndexOf(":");
92
+ if (idx === -1)
93
+ return { address: addr, host: addr, port: 0 };
94
+ const host = addr.slice(0, idx);
95
+ const port = Number.parseInt(addr.slice(idx + 1), 10) || 0;
96
+ return { address: addr, host, port };
97
+ }
98
+ function safeJsonParseRecord(value) {
99
+ if (!value)
100
+ return {};
101
+ if (typeof value === "object" && !Array.isArray(value)) {
102
+ return value;
103
+ }
104
+ if (typeof value === "string") {
105
+ try {
106
+ const parsed = JSON.parse(value);
107
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
108
+ return parsed;
109
+ }
110
+ }
111
+ catch {
112
+ // Ignore invalid JSON string
113
+ }
114
+ }
115
+ return {};
116
+ }
117
+ /**
118
+ * Giải mã chuẩn xác sự kiện cuộc gọi đến (`act: "request"` 1-1 hoặc `act: "group_request"` Nhóm)
119
+ * khớp 100% với `zrtc_call::ZMessageDeserializer::_parseActRequest` (`0x10001c824`),
120
+ * `_parseActRequestDetail` (`0x10001dfa0`), `_parseActRequestGroup` (`0x10001ffc4`) và
121
+ * `ZCallInfo::_prepareCallConfig` (`0x1000cd588`).
122
+ */
123
+ function parseIncomingCallEvent(callEvent, localUid) {
124
+ const rawData = "data" in callEvent && typeof callEvent.data === "object" && callEvent.data !== null
125
+ ? callEvent.data
126
+ : callEvent;
127
+ const act = "act" in callEvent && typeof callEvent.act === "string"
128
+ ? callEvent.act
129
+ : String(rawData.act ?? "");
130
+ const parsedParams = rawData.params !== undefined
131
+ ? safeJsonParseRecord(rawData.params)
132
+ : rawData.data !== undefined
133
+ ? safeJsonParseRecord(rawData.data)
134
+ : rawData;
135
+ const extendRaw = parsedParams.extendData !== undefined
136
+ ? safeJsonParseRecord(parsedParams.extendData)
137
+ : rawData.extendData !== undefined
138
+ ? safeJsonParseRecord(rawData.extendData)
139
+ : parsedParams;
140
+ const callSetting = safeJsonParseRecord(rawData.callSetting ?? parsedParams.callSetting);
141
+ const isGroupCall = act.startsWith("group_") ||
142
+ Boolean(rawData.groupId) ||
143
+ Boolean(rawData.groupNoiseId) ||
144
+ Boolean(rawData.hostCall) ||
145
+ Number(rawData.callType) === sendVoiceCall.CallType.GroupAudio ||
146
+ Number(rawData.callType) === sendVoiceCall.CallType.GroupVideo;
147
+ const callId = Number(rawData.callId ??
148
+ rawData.call_id ??
149
+ rawData.id ??
150
+ parsedParams.callId ??
151
+ parsedParams.call_id ??
152
+ parsedParams.id ??
153
+ extendRaw.callId ??
154
+ extendRaw.id ??
155
+ 0);
156
+ const callerId = String(rawData.uidN ??
157
+ rawData.callerId ??
158
+ parsedParams.uidN ??
159
+ parsedParams.callerId ??
160
+ rawData.uidFrom ??
161
+ rawData.fromId ??
162
+ parsedParams.uidFrom ??
163
+ parsedParams.fromId ??
164
+ rawData.hostCall ??
165
+ "");
166
+ const hostCall = String(rawData.hostCall ??
167
+ parsedParams.hostCall ??
168
+ rawData.groupNoiseId ??
169
+ rawData.groupId ??
170
+ callerId);
171
+ const groupId = isGroupCall
172
+ ? String(rawData.groupNoiseId ?? rawData.groupId ?? parsedParams.groupId ?? hostCall)
173
+ : "";
174
+ const groupName = String(rawData.groupName ?? parsedParams.groupName ?? "");
175
+ const groupAvatar = String(rawData.groupAvatar ?? parsedParams.groupAvatar ?? "");
176
+ const callerName = String(parsedParams.Dname ??
177
+ rawData.Dname ??
178
+ parsedParams.callerName ??
179
+ rawData.callerName ??
180
+ parsedParams.name ??
181
+ rawData.name ??
182
+ groupName ??
183
+ callerId);
184
+ const callerAvatar = String(parsedParams.avatar ??
185
+ rawData.avatar ??
186
+ parsedParams.callerAvatar ??
187
+ rawData.callerAvatar ??
188
+ groupAvatar ??
189
+ "");
190
+ // Trong ZCallInfo::_prepareCallConfig (0x1000cd5c4):
191
+ // - Khi nhận cuộc gọi (Callee): localUid32 = uidTo (hoặc receiverId), partnerUid32 = uidFrom (hoặc fromId)
192
+ const fromUid32 = Number(rawData.uidTo ??
193
+ rawData.receiverId ??
194
+ parsedParams.uidTo ??
195
+ parsedParams.receiverId ??
196
+ 0) || ZRTPPacketCodec.uidToUint32(localUid ?? "");
197
+ const toUid32 = Number(rawData.uidFrom ?? rawData.fromId ?? parsedParams.uidFrom ?? parsedParams.fromId ?? 0) || ZRTPPacketCodec.uidToUint32(callerId);
198
+ const serversList = Array.isArray(parsedParams.servers)
199
+ ? parsedParams.servers
200
+ : Array.isArray(rawData.servers)
201
+ ? rawData.servers
202
+ : [];
203
+ const firstServersEntry = serversList[0];
204
+ const serverAddrList = Array.isArray(extendRaw.serverAddr)
205
+ ? extendRaw.serverAddr
206
+ : Array.isArray(parsedParams.serverAddr)
207
+ ? parsedParams.serverAddr
208
+ : [];
209
+ const firstServerAddr = serverAddrList[0];
210
+ const rtpArray = Array.isArray(extendRaw.rtp)
211
+ ? extendRaw.rtp
212
+ : Array.isArray(parsedParams.rtp)
213
+ ? parsedParams.rtp
214
+ : [];
215
+ const rtcpArray = Array.isArray(extendRaw.rtcp)
216
+ ? extendRaw.rtcp
217
+ : Array.isArray(parsedParams.rtcp)
218
+ ? parsedParams.rtcp
219
+ : [];
220
+ const rtpAddr = String(rawData.rtpAddress ??
221
+ rawData.rtpIP ??
222
+ rawData.rtpServer ??
223
+ parsedParams.rtpAddress ??
224
+ parsedParams.rtpIP ??
225
+ parsedParams.rtpServer ??
226
+ firstServersEntry?.rtpaddr ??
227
+ firstServersEntry?.rtp ??
228
+ firstServerAddr?.rtp ??
229
+ firstServerAddr?.rtpaddr ??
230
+ (rtpArray[0]?.ip && rtpArray[0]?.port ? `${rtpArray[0].ip}:${rtpArray[0].port}` : ""));
231
+ const rtcpFromArray = rtcpArray[0]?.ip && rtcpArray[0]?.port ? `${rtcpArray[0].ip}:${rtcpArray[0].port}` : "";
232
+ const rtcpAddr = String(rawData.rtcpAddress ??
233
+ rawData.rtcpIP ??
234
+ rawData.rtcpServer ??
235
+ parsedParams.rtcpAddress ??
236
+ parsedParams.rtcpIP ??
237
+ parsedParams.rtcpServer ??
238
+ firstServersEntry?.rtcpaddr ??
239
+ firstServersEntry?.rtcp ??
240
+ firstServerAddr?.rtcp ??
241
+ firstServerAddr?.rtcpaddr ??
242
+ (rtcpFromArray || rtpAddr));
243
+ const rtpEp = parseEndpoint(rtpAddr);
244
+ const rtcpEp = parseEndpoint(rtcpAddr || rtpAddr);
245
+ const rawCallType = Number(extendRaw.callType ?? parsedParams.callType ?? rawData.callType ?? 0);
246
+ const isVideo = rawCallType === 1 ||
247
+ rawCallType === sendVoiceCall.CallType.Video ||
248
+ rawCallType === sendVoiceCall.CallType.GroupVideo ||
249
+ Boolean(extendRaw.video);
250
+ const callType = isGroupCall
251
+ ? isVideo
252
+ ? sendVoiceCall.CallType.GroupVideo
253
+ : sendVoiceCall.CallType.GroupAudio
254
+ : isVideo
255
+ ? sendVoiceCall.CallType.Video
256
+ : sendVoiceCall.CallType.Audio;
257
+ const sessId = String(rawData.session ??
258
+ rawData.sessId ??
259
+ callSetting.session ??
260
+ parsedParams.sessId ??
261
+ parsedParams.session ??
262
+ extendRaw.sessId ??
263
+ (isGroupCall
264
+ ? `${hostCall || callerId}_${callId}_501`
265
+ : `${callerId}_${callId}_401`));
266
+ const rawPartners = Array.isArray(rawData.partners)
267
+ ? rawData.partners
268
+ : Array.isArray(rawData.partnerInfo)
269
+ ? rawData.partnerInfo
270
+ : Array.isArray(parsedParams.partners)
271
+ ? parsedParams.partners
272
+ : [];
273
+ const partners = rawPartners
274
+ .map((p) => {
275
+ if (typeof p === "string" || typeof p === "number")
276
+ return String(p);
277
+ if (p && typeof p === "object") {
278
+ const obj = p;
279
+ return String(obj.id ?? obj.uid ?? obj.userId ?? "");
280
+ }
281
+ return "";
282
+ })
283
+ .filter(Boolean);
284
+ const codec = typeof parsedParams.codec === "string" && parsedParams.codec !== "[]"
285
+ ? parsedParams.codec
286
+ : typeof rawData.codec === "string" && rawData.codec !== "[]"
287
+ ? rawData.codec
288
+ : ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
289
+ return {
290
+ callId,
291
+ callerId,
292
+ callerName,
293
+ callerAvatar,
294
+ fromUid32,
295
+ toUid32,
296
+ sessId,
297
+ callType,
298
+ isVideo,
299
+ isGroupCall,
300
+ groupId,
301
+ groupName,
302
+ groupAvatar,
303
+ hostCall,
304
+ partners,
305
+ rtpAddress: rtpEp.address,
306
+ rtpHost: rtpEp.host,
307
+ rtpPort: rtpEp.port,
308
+ rtcpAddress: rtcpEp.address,
309
+ rtcpHost: rtcpEp.host,
310
+ rtcpPort: rtcpEp.port,
311
+ codec,
312
+ srtpKey: typeof extendRaw.srtpKey === "string" ? extendRaw.srtpKey : undefined,
313
+ srtpMode: extendRaw.srtpMode !== undefined ? Number(extendRaw.srtpMode) : undefined,
314
+ zrtpKey: typeof parsedParams.zrtpKey === "string"
315
+ ? parsedParams.zrtpKey
316
+ : typeof extendRaw.zrtpKey === "string"
317
+ ? extendRaw.zrtpKey
318
+ : undefined,
319
+ extendData: extendRaw,
320
+ rawEvent: callEvent,
321
+ };
322
+ }
323
+ /**
324
+ * Khung Opus silence / DTX 20ms chuẩn (TOC `0xf8` = Silk-only WB 20ms mono + `0xff 0xfe`)
325
+ */
326
+ const OPUS_SILENCE_20MS_FRAME = node_buffer.Buffer.from([0xf8, 0xff, 0xfe]);
327
+ /**
328
+ * Khung H.264 Baseline Profile Level 3.0 16x16 hợp lệ (SPS `0x67` + PPS `0x68` + IDR Slice `0x65`)
329
+ * giúp bộ giải mã phần cứng đối phương render đủ 15fps ngay cả khi chưa bật camera ngoài
330
+ * (`CallController::_checkEventLossVideoAndReason` @ `0x100182734`).
331
+ */
332
+ const H264_KEEPALIVE_ANNEXB_FRAME = node_buffer.Buffer.from("000000016742c01ed90f8888840000030004000003003c8f162e480000000168cb83cb20000000016588840033ff", "hex");
333
+ /**
334
+ * Bộ máy xử lý phiên gọi Thoại & Video thuần TypeScript / Node.js (`node:dgram` UDP + `ZRTPPacket` + `SRTP`).
335
+ *
336
+ * Hoạt động độc lập 100% trên Worker, Linux Server, Docker, hoặc Desktop mà **không cần cài đặt Zalo PC**:
337
+ * - Thực hiện trọn vẹn luồng Signaling 401 -> Bắt tay UDP `0x01 0x7e` (`cmd=11/12`) -> Nhận `routingToken` -> Signaling 416 / 402 -> Duy trì UDP Ping (`cmd=2/5/32`) & RTCP SR/RR.
338
+ * - Hỗ trợ chế độ **Direct Mobile Media Handoff** (`signalingOnly: true` / `handoffUdpToExternal()`): Worker chỉ tạo/nhận cuộc gọi qua HTTPS/WebSocket rồi trả gói `mobileDirectHandoff` cho Mobile App tự kết nối UDP trực tiếp tới máy chủ Relay của Zalo mà không cần đi qua trung gian.
339
+ * - Hỗ trợ đầy đủ cả **Chiều gọi đi (Outgoing)** lẫn **Chiều gọi đến (Incoming 1-1 & Nhóm)**: giải mã chuẩn `act: "request"` / `"group_request"`, báo đổ chuông (`ringIncomingCall`), bắt máy (`acceptIncomingCall` / `acceptIncomingGroupCall`), và từ chối (`rejectIncomingCall`).
340
+ */
341
+ class ZaloRtcSession extends EventEmitter {
342
+ api;
343
+ state = "idle";
344
+ udpSocket = null;
345
+ pingTimer = null;
346
+ groupPingTimer = null;
347
+ audioKeepaliveTimer = null;
348
+ videoKeepaliveTimer = null;
349
+ rtcpTimer = null;
350
+ ringingTimeoutTimer = null;
351
+ startedAtMs = 0;
352
+ activeStartedAtMs = 0;
353
+ callId = 0;
354
+ sessId = "";
355
+ isCaller = true;
356
+ partnerId = "";
357
+ groupId = "";
358
+ groupPartners = [];
359
+ fromUid32 = 0;
360
+ toUid32 = 0;
361
+ callType = sendVoiceCall.CallType.Audio;
362
+ isCameraOn = false;
363
+ isMicMuted = false;
364
+ isHold = false;
365
+ isScreenSharing = false;
366
+ rtpEndpoint = { address: "", host: "127.0.0.1", port: 0 };
367
+ rtcpEndpoint = { address: "", host: "127.0.0.1", port: 0 };
368
+ routingToken = 0;
369
+ udpHandshakeVerified = false;
370
+ hasReceivedPartnerMedia = false;
371
+ hasSentAnswerAck = false;
372
+ localUdpPort = 0;
373
+ zrtpKey;
374
+ codec = ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
375
+ extendData = {};
376
+ serverInfo;
377
+ controlSeq = 1;
378
+ forwardPingSeq = 0;
379
+ manualControlSeq = 1;
380
+ audioSeq = Math.floor(Math.random() * 30000);
381
+ audioTimestamp = Math.floor(Math.random() * 100000);
382
+ audioSsrc = (Math.floor(Math.random() * 0x7fffffff) + 1) >>> 0;
383
+ audioPacketCount = 0;
384
+ audioOctetCount = 0;
385
+ lastExternalAudioSentAtMs = 0;
386
+ videoSeq = Math.floor(Math.random() * 30000);
387
+ videoTimestamp = Math.floor(Math.random() * 100000);
388
+ videoSsrc = (Math.floor(Math.random() * 0x7fffffff) + 1) >>> 0;
389
+ videoPacketCount = 0;
390
+ videoOctetCount = 0;
391
+ lastExternalVideoSentAtMs = 0;
392
+ remoteAudioSsrc = 0;
393
+ remoteAudioMaxSeq = 0;
394
+ remoteAudioSeqCycles = 0;
395
+ lastRemoteAudioSrMiddleNtp = 0;
396
+ lastRemoteAudioSrReceivedAtMs = 0;
397
+ remoteVideoSsrc = 0;
398
+ remoteVideoMaxSeq = 0;
399
+ remoteVideoSeqCycles = 0;
400
+ lastRemoteVideoSrMiddleNtp = 0;
401
+ lastRemoteVideoSrReceivedAtMs = 0;
402
+ localSpsNal = null;
403
+ localPpsNal = null;
404
+ remoteSpsNal = null;
405
+ remotePpsNal = null;
406
+ fuaFragments = [];
407
+ currentAccessUnitNals = [];
408
+ currentAccessUnitTs = -1;
409
+ currentAccessUnitHasIdr = false;
410
+ srtpSession = null;
411
+ options;
412
+ boundCallEventListener;
413
+ constructor(api, options = {}) {
414
+ super();
415
+ this.api = api;
416
+ this.options = {
417
+ packetMode: options.packetMode ?? 1,
418
+ autoKeepaliveMedia: options.autoKeepaliveMedia ?? true,
419
+ signalingOnly: options.signalingOnly ?? false,
420
+ enableSrtp: options.enableSrtp ?? false,
421
+ srtpSuite: options.srtpSuite ?? "AES_CM_128_HMAC_SHA1_80",
422
+ srtpMasterKeyBase64: options.srtpMasterKeyBase64,
423
+ udpHandshakeTimeoutMs: options.udpHandshakeTimeoutMs ?? 1500,
424
+ pingIntervalMs: options.pingIntervalMs ?? 1000,
425
+ autoAttachListener: options.autoAttachListener ?? true,
426
+ localUdpPort: options.localUdpPort ?? 0,
427
+ ringingTimeoutMs: options.ringingTimeoutMs ?? 60000,
428
+ };
429
+ if (this.options.enableSrtp) {
430
+ const keyB64 = this.options.srtpMasterKeyBase64 ?? ZaloSrtpSession.generateSrtpMasterKeyBase64();
431
+ this.srtpSession = new ZaloSrtpSession.ZaloSrtpSession(keyB64, this.options.srtpSuite);
432
+ }
433
+ }
434
+ getState() {
435
+ return this.state;
436
+ }
437
+ getSrtpSession() {
438
+ return this.srtpSession;
439
+ }
440
+ getCallDurationSeconds() {
441
+ if (!this.activeStartedAtMs)
442
+ return 0;
443
+ return Math.max(0, Math.floor((Date.now() - this.activeStartedAtMs) / 1000));
444
+ }
445
+ getMediaState() {
446
+ return {
447
+ callId: this.callId,
448
+ callType: this.callType,
449
+ isCameraOn: this.isCameraOn,
450
+ isMicMuted: this.isMicMuted,
451
+ isHold: this.isHold,
452
+ isScreenSharing: this.isScreenSharing,
453
+ durationSeconds: this.getCallDurationSeconds(),
454
+ groupId: this.groupId,
455
+ groupPartners: this.groupPartners,
456
+ };
457
+ }
458
+ getLocalAudioSsrc() {
459
+ return (this.fromUid32 || this.audioSsrc) >>> 0;
460
+ }
461
+ getLocalVideoSsrc() {
462
+ return (this.fromUid32 || this.videoSsrc) >>> 0;
463
+ }
464
+ /**
465
+ * Tạo bộ thông số + các gói tin nhị phân `ZRTPPacket` đã đóng gói sẵn dạng hex
466
+ * để ứng dụng Mobile có thể mở socket UDP kết nối **trực tiếp** tới máy chủ Relay của Zalo (`rtpHost:rtpPort`)
467
+ * mà không cần đẩy luồng dữ liệu âm thanh/hình ảnh qua Worker trung gian.
468
+ */
469
+ getMobileDirectHandoffBundle(routingTokenOverride) {
470
+ const role = this.isCaller ? ZRTPPacketCodec.ZRTCRole.Caller : ZRTPPacketCodec.ZRTCRole.Callee;
471
+ const token = ((routingTokenOverride ?? this.routingToken) || this.callId) >>> 0;
472
+ const fromUid = this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
473
+ const toUid = this.toUid32 || ZRTPPacketCodec.uidToUint32(this.partnerId);
474
+ const isVideo = this.callType === sendVoiceCall.CallType.Video ||
475
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
476
+ Number(this.extendData?.callType) === 1;
477
+ const initCallPkt = ZRTPPacketCodec.buildInitCallRequest({
478
+ isCaller: this.isCaller,
479
+ seqNum: 0,
480
+ headerCallId: this.callId,
481
+ callId: this.callId,
482
+ fromUid,
483
+ toUid,
484
+ flags: 1,
485
+ subFlags: toUid,
486
+ sessId: this.sessId,
487
+ });
488
+ const relayPingPkt = ZRTPPacketCodec.buildRelayPingRequest({
489
+ seqNum: 0,
490
+ callId: 0,
491
+ fromUid,
492
+ routingToken: token,
493
+ sessId: this.sessId,
494
+ });
495
+ const pingPkt = ZRTPPacketCodec.buildPingRequest({
496
+ seqNum: 1,
497
+ callId: this.callId,
498
+ fromUid,
499
+ toUid,
500
+ elapsedMs: 1000,
501
+ });
502
+ const manualCtrlPayload = node_buffer.Buffer.alloc(20);
503
+ manualCtrlPayload.writeInt32LE(isVideo ? 15 : 0, 0);
504
+ manualCtrlPayload.writeInt32LE(0, 4);
505
+ manualCtrlPayload.writeInt32LE(0, 8);
506
+ manualCtrlPayload.writeInt32LE(50, 12);
507
+ manualCtrlPayload.writeInt32LE(isVideo ? 30 : 0, 16);
508
+ const manualCtrlPkt = ZRTPPacketCodec.buildRequestForwardPacket({
509
+ seqNum: 0,
510
+ seqId: 1,
511
+ fromUid,
512
+ routingToken: token,
513
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.VideoControl,
514
+ payload: manualCtrlPayload,
515
+ });
516
+ const cameraOnPkt = ZRTPPacketCodec.buildRequestForwardPacket({
517
+ seqNum: 0,
518
+ seqId: 1,
519
+ fromUid,
520
+ routingToken: token,
521
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.OnCamera,
522
+ payload: "",
523
+ });
524
+ const cameraOffPkt = ZRTPPacketCodec.buildRequestForwardPacket({
525
+ seqNum: 0,
526
+ seqId: 1,
527
+ fromUid,
528
+ routingToken: token,
529
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.OffCamera,
530
+ payload: "",
531
+ });
532
+ const endCallPkt = ZRTPPacketCodec.buildEndCallRequest({
533
+ seqNum: 0,
534
+ callId: this.callId,
535
+ fromUid,
536
+ toUid,
537
+ routingToken: token,
538
+ flags: 0,
539
+ });
540
+ const audioRtpPrefix = (this.options.packetMode === 1
541
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.AudioRtp, token, node_buffer.Buffer.alloc(0))
542
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.AudioRtp, token, node_buffer.Buffer.alloc(0), role)).toString("hex");
543
+ const audioRtcpPrefix = (this.options.packetMode === 1
544
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.AudioRtcp, token, node_buffer.Buffer.alloc(0))
545
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.AudioRtcp, token, node_buffer.Buffer.alloc(0), role)).toString("hex");
546
+ const videoRtpPrefix = (this.options.packetMode === 1
547
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.VideoRtp, token, node_buffer.Buffer.alloc(0))
548
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.VideoRtp, token, node_buffer.Buffer.alloc(0), role)).toString("hex");
549
+ const videoRtcpPrefix = (this.options.packetMode === 1
550
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.VideoRtcp, token, node_buffer.Buffer.alloc(0))
551
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.VideoRtcp, token, node_buffer.Buffer.alloc(0), role)).toString("hex");
552
+ return {
553
+ callId: this.callId,
554
+ sessId: this.sessId,
555
+ isCaller: this.isCaller,
556
+ partnerId: this.partnerId,
557
+ fromUid32: fromUid,
558
+ toUid32: toUid,
559
+ callType: this.callType,
560
+ rtpHost: this.rtpEndpoint.host,
561
+ rtpPort: this.rtpEndpoint.port,
562
+ rtcpHost: this.rtcpEndpoint.host,
563
+ rtcpPort: this.rtcpEndpoint.port,
564
+ packetMode: this.options.packetMode,
565
+ routingToken: token,
566
+ codec: this.codec,
567
+ srtpMasterKeyBase64: this.srtpSession?.masterKeyBase64,
568
+ zrtpKey: this.zrtpKey,
569
+ initCallPacketHex: initCallPkt.toString("hex"),
570
+ relayPingPacketHex: relayPingPkt.toString("hex"),
571
+ pingPacketHex: pingPkt.toString("hex"),
572
+ manualControlPacketHex: manualCtrlPkt.toString("hex"),
573
+ cameraOnPacketHex: cameraOnPkt.toString("hex"),
574
+ cameraOffPacketHex: cameraOffPkt.toString("hex"),
575
+ endCallPacketHex: endCallPkt.toString("hex"),
576
+ audioRtpPrefixHex: audioRtpPrefix,
577
+ audioRtcpPrefixHex: audioRtcpPrefix,
578
+ videoRtpPrefixHex: videoRtpPrefix,
579
+ videoRtcpPrefixHex: videoRtcpPrefix,
580
+ };
581
+ }
582
+ /**
583
+ * Đặt lại toàn bộ trạng thái định tuyến UDP, bộ đếm RTP/RTCP, bộ đệm giải mã H.264 FU-A/SPS/PPS
584
+ * và khóa SRTP trước khi bắt đầu hoặc chấp nhận một cuộc gọi mới trên cùng một instance `ZaloRtcSession`.
585
+ */
586
+ resetSessionStateForNewCall(preserveRingingCallId = 0) {
587
+ const isContinuingSameRingingCall = preserveRingingCallId > 0 &&
588
+ this.callId === preserveRingingCallId &&
589
+ this.state === "ringing";
590
+ this.cleanupResources();
591
+ this.routingToken = 0;
592
+ this.udpHandshakeVerified = false;
593
+ this.hasReceivedPartnerMedia = false;
594
+ this.hasSentAnswerAck = false;
595
+ this.localUdpPort = 0;
596
+ this.startedAtMs = 0;
597
+ this.activeStartedAtMs = 0;
598
+ this.controlSeq = 1;
599
+ this.forwardPingSeq = 0;
600
+ this.manualControlSeq = 1;
601
+ this.audioSeq = Math.floor(Math.random() * 30000);
602
+ this.audioTimestamp = Math.floor(Math.random() * 100000);
603
+ this.audioPacketCount = 0;
604
+ this.audioOctetCount = 0;
605
+ this.lastExternalAudioSentAtMs = 0;
606
+ this.videoSeq = Math.floor(Math.random() * 30000);
607
+ this.videoTimestamp = Math.floor(Math.random() * 100000);
608
+ this.videoPacketCount = 0;
609
+ this.videoOctetCount = 0;
610
+ this.lastExternalVideoSentAtMs = 0;
611
+ this.remoteAudioSsrc = 0;
612
+ this.remoteAudioMaxSeq = 0;
613
+ this.remoteAudioSeqCycles = 0;
614
+ this.lastRemoteAudioSrMiddleNtp = 0;
615
+ this.lastRemoteAudioSrReceivedAtMs = 0;
616
+ this.remoteVideoSsrc = 0;
617
+ this.remoteVideoMaxSeq = 0;
618
+ this.remoteVideoSeqCycles = 0;
619
+ this.lastRemoteVideoSrMiddleNtp = 0;
620
+ this.lastRemoteVideoSrReceivedAtMs = 0;
621
+ this.localSpsNal = null;
622
+ this.localPpsNal = null;
623
+ this.remoteSpsNal = null;
624
+ this.remotePpsNal = null;
625
+ this.clearIncomingVideoAssemblyBuffers();
626
+ if (this.options.enableSrtp) {
627
+ const keyB64 = this.options.srtpMasterKeyBase64 ?? ZaloSrtpSession.generateSrtpMasterKeyBase64();
628
+ this.srtpSession = new ZaloSrtpSession.ZaloSrtpSession(keyB64, this.options.srtpSuite);
629
+ }
630
+ else {
631
+ this.srtpSession = null;
632
+ }
633
+ if (!isContinuingSameRingingCall) {
634
+ this.state = "idle";
635
+ }
636
+ }
637
+ clearIncomingVideoAssemblyBuffers() {
638
+ this.fuaFragments = [];
639
+ this.currentAccessUnitNals = [];
640
+ this.currentAccessUnitTs = -1;
641
+ this.currentAccessUnitHasIdr = false;
642
+ }
643
+ /**
644
+ * Tái kích hoạt (Re-activate / Re-bind) kết nối UDP và luồng Media/RTCP cho phiên gọi hiện tại:
645
+ * - Dùng khi chuyển vùng mạng (Wi-Fi <-> 4G/5G), khi socket UDP bị gián đoạn, hoặc khi muốn thu hồi lại
646
+ * quyền xử lý UDP từ Mobile về lại Worker/Desktop sau khi đã gọi `signalingOnly` / `handoffUdpToExternal()`.
647
+ * - Tự động mở cổng UDP mới, gửi lại gói bắt tay `0x01 0x7e` (`cmd=11` Caller hoặc `cmd=12` Callee),
648
+ * khởi động lại vòng lặp UDP Ping + Media Keepalive + RTCP SR/RR, đồng bộ lại trạng thái Mic/Camera
649
+ * và bắn `RTCP PLI` để khôi phục hình ảnh H.264 ngay lập tức.
650
+ */
651
+ async reconnectUdp(options = {}) {
652
+ if (!this.callId || this.rtpEndpoint.port <= 0) {
653
+ throw new ZaloApiError.ZaloApiError("Cannot reconnect UDP: no active call endpoint");
654
+ }
655
+ if (this.pingTimer) {
656
+ clearInterval(this.pingTimer);
657
+ this.pingTimer = null;
658
+ }
659
+ this.stopMediaKeepaliveLoop();
660
+ if (this.udpSocket) {
661
+ try {
662
+ this.udpSocket.removeAllListeners();
663
+ this.udpSocket.close();
664
+ }
665
+ catch {
666
+ // Ignore
667
+ }
668
+ this.udpSocket = null;
669
+ }
670
+ this.localUdpPort = 0;
671
+ this.clearIncomingVideoAssemblyBuffers();
672
+ await this.ensureUdpSocket();
673
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
674
+ await this.performUdpInitHandshake(this.isCaller);
675
+ }
676
+ else if (!this.routingToken) {
677
+ this.routingToken = this.callId >>> 0;
678
+ }
679
+ this.startUdpPingLoop();
680
+ this.startMediaKeepaliveLoop();
681
+ void this.sendMicState(!this.isMicMuted).catch(() => { });
682
+ if (this.callType === sendVoiceCall.CallType.Video ||
683
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
684
+ Number(this.extendData?.callType) === 1) {
685
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
686
+ void this.sendRtcpPli().catch(() => { });
687
+ }
688
+ const params = this.getConnectionParams();
689
+ this.emit("connection_params", params);
690
+ return params;
691
+ }
692
+ /**
693
+ * Ngắt toàn bộ socket UDP và các vòng lặp Ping/Media cục bộ trên Worker
694
+ * (nhưng vẫn giữ nguyên kết nối Signaling WebSocket để lắng nghe `answer`, `end`, `cancel`, `audio_to_video`),
695
+ * nhằm bàn giao hoàn toàn kênh UDP cho ứng dụng Mobile kết nối trực tiếp tới máy chủ Relay Zalo
696
+ * mà không bị xung đột NAT (NAT flip-flopping).
697
+ */
698
+ handoffUdpToExternal(routingTokenOverride) {
699
+ if (routingTokenOverride !== undefined && routingTokenOverride > 0) {
700
+ this.routingToken = routingTokenOverride >>> 0;
701
+ }
702
+ if (this.pingTimer) {
703
+ clearInterval(this.pingTimer);
704
+ this.pingTimer = null;
705
+ }
706
+ this.stopMediaKeepaliveLoop();
707
+ if (this.udpSocket) {
708
+ try {
709
+ this.udpSocket.removeAllListeners();
710
+ this.udpSocket.close();
711
+ }
712
+ catch {
713
+ // Ignore
714
+ }
715
+ this.udpSocket = null;
716
+ }
717
+ this.localUdpPort = 0;
718
+ return this.getMobileDirectHandoffBundle(routingTokenOverride);
719
+ }
720
+ /**
721
+ * Xuất toàn bộ thông số kết nối cuộc gọi & UDP `ZRTPPacket` sau khi thương lượng xong
722
+ * để truyền về Mobile App hoặc Media Bridge.
723
+ */
724
+ getConnectionParams() {
725
+ const handoff = this.getMobileDirectHandoffBundle();
726
+ return {
727
+ callId: this.callId,
728
+ sessId: this.sessId,
729
+ isCaller: this.isCaller,
730
+ partnerId: this.partnerId,
731
+ fromId: this.fromUid32,
732
+ toId: this.toUid32,
733
+ callType: this.callType,
734
+ rtpAddress: this.rtpEndpoint.address,
735
+ rtpHost: this.rtpEndpoint.host,
736
+ rtpPort: this.rtpEndpoint.port,
737
+ rtcpAddress: this.rtcpEndpoint.address,
738
+ rtcpHost: this.rtcpEndpoint.host,
739
+ rtcpPort: this.rtcpEndpoint.port,
740
+ routingToken: handoff.routingToken,
741
+ udpHandshakeVerified: this.udpHandshakeVerified,
742
+ localUdpPort: this.localUdpPort,
743
+ packetMode: this.options.packetMode,
744
+ codec: this.codec,
745
+ extendData: this.extendData,
746
+ zrtpKey: this.zrtpKey,
747
+ srtpMasterKeyBase64: this.srtpSession?.masterKeyBase64,
748
+ audioZrtcHeaderHex: handoff.audioRtpPrefixHex,
749
+ videoZrtcHeaderHex: handoff.videoRtpPrefixHex,
750
+ serverInfo: this.serverInfo,
751
+ mobileDirectHandoff: handoff,
752
+ };
753
+ }
754
+ /**
755
+ * Khởi tạo cuộc gọi đi (Caller) 1-1 hoàn chỉnh:
756
+ * 1. Gọi `/api/voicecall/requestcall` (401) lấy `sessId`, `rtpaddr`, `rtcpaddr`, `zrtpKey`
757
+ * 2. Mở socket UDP và gửi gói bắt tay `0x01 0x7e` (`cmd=11`) tới `rtpaddr` để nhận `routingToken` (bỏ qua nếu `signalingOnly: true`)
758
+ * 3. Gọi `/api/voicecall/request` (416) kèm codec Opus chuẩn và `extendData` để máy đối phương đổ chuông
759
+ * 4. Duy trì nhịp tim UDP Ping (`0x01 0x7e`, `cmd=2/5/32`) và sẵn sàng truyền/nhận RTP/SRTP (nếu không bật `signalingOnly`)
760
+ */
761
+ async startOutgoingCall(calleeId, options = {}) {
762
+ if (!calleeId)
763
+ throw new ZaloApiError.ZaloApiError("Missing calleeId");
764
+ this.resetSessionStateForNewCall();
765
+ const isSignalingOnly = Boolean(options.signalingOnly ?? this.options.signalingOnly);
766
+ this.isCaller = true;
767
+ this.partnerId = String(calleeId);
768
+ this.groupId = "";
769
+ this.groupPartners = [];
770
+ this.callType = options.callType ?? sendVoiceCall.CallType.Audio;
771
+ this.isCameraOn =
772
+ (this.callType === sendVoiceCall.CallType.Video || this.callType === sendVoiceCall.CallType.GroupVideo) &&
773
+ !options.offCameraAtStart;
774
+ this.isMicMuted = false;
775
+ this.isHold = false;
776
+ this.isScreenSharing = false;
777
+ this.activeStartedAtMs = 0;
778
+ this.callId = options.callId ?? Math.floor(Math.random() * 2_000_000_000);
779
+ this.codec = options.codec ?? ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
780
+ this.udpHandshakeVerified = false;
781
+ this.hasReceivedPartnerMedia = false;
782
+ this.hasSentAnswerAck = false;
783
+ this.setState("allocating");
784
+ if (this.options.autoAttachListener) {
785
+ this.attachCallEventListener();
786
+ }
787
+ // Bước 1: Xin cấp phát phiên & địa chỉ máy chủ Relay UDP (Signal 401)
788
+ const serverInfo = await this.api.call.requestVoiceCall(this.partnerId, {
789
+ callType: this.callType,
790
+ callId: this.callId,
791
+ codec: this.codec,
792
+ });
793
+ this.applyServerInfo(serverInfo);
794
+ // Bước 2: Mở UDP Socket & thực hiện bắt tay ZRTP InitCall (0x01 0x7e cmd=11)
795
+ // (Nếu chạy chế độ signalingOnly cho Mobile tự kết nối UDP trực tiếp thì bỏ qua bước mở socket tại Worker)
796
+ if (!isSignalingOnly) {
797
+ await this.ensureUdpSocket();
798
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
799
+ this.setState("handshaking_udp");
800
+ await this.performUdpInitHandshake(true);
801
+ }
802
+ else {
803
+ this.routingToken = this.callId >>> 0;
804
+ }
805
+ }
806
+ else {
807
+ this.routingToken = this.callId >>> 0;
808
+ }
809
+ // Bước 3: Gửi tín hiệu đổ chuông 416 kèm codec Opus & ZRTC extendData chuẩn
810
+ const isVideo = this.callType === sendVoiceCall.CallType.Video || this.callType === sendVoiceCall.CallType.GroupVideo;
811
+ const negotiateVideoPipeline = isVideo || Boolean(options.allowVideoUpgrade) || Boolean(options.offCameraAtStart);
812
+ this.extendData = {
813
+ ...ZRTPPacketCodec.buildZrtcExtendData({
814
+ rtpAddress: this.rtpEndpoint.address,
815
+ rtcpAddress: this.rtcpEndpoint.address,
816
+ isVideo: negotiateVideoPipeline,
817
+ packetMode: 2,
818
+ srtpMode: this.srtpSession ? 1 : 0,
819
+ srtpKey: this.srtpSession?.masterKeyBase64,
820
+ }),
821
+ rtcpDiff: 0,
822
+ rtcp: [{ ip: this.rtcpEndpoint.host, port: this.rtcpEndpoint.port }],
823
+ rtp: [{ ip: this.rtpEndpoint.host, port: this.rtpEndpoint.port }],
824
+ status: 0,
825
+ };
826
+ await this.api.call.sendRequestVoiceCall(this.partnerId, {
827
+ callId: this.callId,
828
+ session: this.sessId,
829
+ rtpAddress: this.rtpEndpoint.address,
830
+ rtcpAddress: this.rtcpEndpoint.address,
831
+ codec: this.codec,
832
+ extendData: this.extendData,
833
+ subCommand: 3,
834
+ });
835
+ // Nếu khởi tạo cuộc gọi với Camera tắt ban đầu (gọi Thoại có allowVideoUpgrade hoặc Video tắt Cam),
836
+ // đồng bộ ngay trạng thái OffCamera (cmd=32 subCmd=8 + /api/voicecall/conf isOnCam=0)
837
+ // khớp với ZCallMainWindowVideo_v2::updateIncallState (0x1000410c0).
838
+ if (negotiateVideoPipeline && !this.isCameraOn) {
839
+ if (!isSignalingOnly) {
840
+ void this.sendCameraState(false).catch(() => { });
841
+ }
842
+ void this.api.call
843
+ .updateCallMediaState(this.partnerId, this.callId, {
844
+ isOnCam: 0,
845
+ isOnMic: -1,
846
+ data: "",
847
+ })
848
+ .catch(() => { });
849
+ }
850
+ // Bước 4: Bắt đầu gửi UDP Ping & Media Keepalive định kỳ giữ phiên Relay (nếu không bật signalingOnly)
851
+ if (!isSignalingOnly) {
852
+ this.startUdpPingLoop();
853
+ this.startMediaKeepaliveLoop();
854
+ }
855
+ this.setState("ringing");
856
+ const params = this.getConnectionParams();
857
+ this.emit("connection_params", params);
858
+ return params;
859
+ }
860
+ /**
861
+ * Giải mã sự kiện cuộc gọi đến (`act: "request"` hoặc `"group_request"`) và gửi ngay tín hiệu
862
+ * báo máy đang đổ chuông (`ringRingVoiceCall` `11302` / `ringRingGroupCall` `12439`) tới phía người gọi,
863
+ * đồng thời chuyển phiên sang trạng thái `"ringing"` trong khi chờ người dùng bấm Nghe máy hoặc Từ chối.
864
+ * Khớp với `zrtc_call::ZCallInfo::receiveIncomingCall` (`0x1000ccbbc`).
865
+ */
866
+ async ringIncomingCall(callEvent) {
867
+ const info = parseIncomingCallEvent(callEvent, this.api.getContext().uid);
868
+ if (!info.callerId || !info.callId) {
869
+ throw new ZaloApiError.ZaloApiError("Invalid incoming call event: missing callerId or callId");
870
+ }
871
+ this.resetSessionStateForNewCall();
872
+ this.isCaller = false;
873
+ this.callId = info.callId;
874
+ this.partnerId = info.isGroupCall ? info.hostCall || info.callerId : info.callerId;
875
+ this.groupId = info.groupId;
876
+ this.groupPartners = info.partners;
877
+ this.fromUid32 = info.fromUid32;
878
+ this.toUid32 = info.toUid32;
879
+ this.sessId = info.sessId;
880
+ this.callType = info.callType;
881
+ this.isCameraOn = info.isVideo;
882
+ this.rtpEndpoint = {
883
+ address: info.rtpAddress,
884
+ host: info.rtpHost,
885
+ port: info.rtpPort,
886
+ };
887
+ this.rtcpEndpoint = {
888
+ address: info.rtcpAddress,
889
+ host: info.rtcpHost,
890
+ port: info.rtcpPort,
891
+ };
892
+ this.codec = info.codec;
893
+ this.zrtpKey = info.zrtpKey;
894
+ if (this.options.autoAttachListener) {
895
+ this.attachCallEventListener();
896
+ }
897
+ if (info.isGroupCall) {
898
+ await this.api.call.ringRingGroupCall(this.callId, {
899
+ hostCall: this.partnerId,
900
+ callType: this.callType,
901
+ partners: this.groupPartners,
902
+ });
903
+ }
904
+ else {
905
+ await this.api.call.ringRingVoiceCall(this.partnerId, this.callId);
906
+ }
907
+ this.setState("ringing");
908
+ this.emit("incoming_call", info);
909
+ return info;
910
+ }
911
+ /**
912
+ * Chấp nhận cuộc gọi đến (Callee) từ sự kiện `call_event` (`act: "request"` hoặc `"group_request"`):
913
+ * - Tự động giải mã chuẩn xác `params.id` (`callId`), `uidN` (`callerId`), `uidFrom` (`toUid32`), `uidTo` (`fromUid32`), `rtpIP`/`rtcpIP`/`servers`
914
+ * - Nếu là cuộc gọi Nhóm (`isGroupCall`), tự động uỷ quyền sang `acceptIncomingGroupCall`
915
+ * - Với cuộc gọi 1-1:
916
+ * 1. Gửi `/api/voicecall/ringring` (407) (nếu chưa gửi)
917
+ * 2. Nếu sự kiện thiếu địa chỉ Relay UDP, tự động gọi dự phòng `requestVoiceCall` (401) như `_trySendAcceptSignal` (`0x1000c2c9c`)
918
+ * 3. Mở socket UDP và gửi gói bắt tay `0x01 0x7e` (`cmd=12`) tới `rtpaddr` (bỏ qua nếu `signalingOnly: true`)
919
+ * 4. Gửi `/api/voicecall/answer` (402) để kết nối âm thanh/hình ảnh 2 chiều
920
+ */
921
+ async acceptIncomingCall(callEvent, options = {}) {
922
+ const info = parseIncomingCallEvent(callEvent, this.api.getContext().uid);
923
+ if (info.isGroupCall) {
924
+ return this.acceptIncomingGroupCall(callEvent, options);
925
+ }
926
+ if (!info.callerId || !info.callId) {
927
+ throw new ZaloApiError.ZaloApiError("Invalid incoming call event: missing callerId or callId");
928
+ }
929
+ this.resetSessionStateForNewCall(info.callId);
930
+ const isSignalingOnly = Boolean(options.signalingOnly ?? this.options.signalingOnly);
931
+ this.isCaller = false;
932
+ this.partnerId = info.callerId;
933
+ this.groupId = "";
934
+ this.groupPartners = [];
935
+ this.callId = info.callId;
936
+ this.fromUid32 = info.fromUid32;
937
+ this.toUid32 = info.toUid32;
938
+ this.sessId = info.sessId;
939
+ this.codec = options.codec ?? info.codec ?? ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
940
+ this.zrtpKey = info.zrtpKey;
941
+ this.hasReceivedPartnerMedia = false;
942
+ this.hasSentAnswerAck = false;
943
+ this.isScreenSharing = false;
944
+ this.activeStartedAtMs = 0;
945
+ this.rtpEndpoint = {
946
+ address: info.rtpAddress,
947
+ host: info.rtpHost,
948
+ port: info.rtpPort,
949
+ };
950
+ this.rtcpEndpoint = {
951
+ address: info.rtcpAddress,
952
+ host: info.rtcpHost,
953
+ port: info.rtcpPort,
954
+ };
955
+ this.callType = info.callType;
956
+ this.isCameraOn = this.callType === sendVoiceCall.CallType.Video && !options.offCameraAtStart;
957
+ this.isMicMuted = false;
958
+ this.isHold = false;
959
+ if (typeof info.srtpKey === "string" && info.srtpKey.length >= 40) {
960
+ this.srtpSession = new ZaloSrtpSession.ZaloSrtpSession(info.srtpKey, this.options.srtpSuite);
961
+ }
962
+ else if (info.srtpMode === 0 && !this.options.enableSrtp) {
963
+ this.srtpSession = null;
964
+ }
965
+ if (this.options.autoAttachListener) {
966
+ this.attachCallEventListener();
967
+ }
968
+ // Bước 1: Báo máy đang đổ chuông (Signal 407)
969
+ if (!options.skipRingRing) {
970
+ await this.api.call.ringRingVoiceCall(this.partnerId, this.callId);
971
+ }
972
+ // Dự phòng theo _trySendAcceptSignal (0x1000c2c9c): nếu sự kiện thiếu rtpPort, xin cấp phát qua Signal 401
973
+ if (this.rtpEndpoint.port <= 0) {
974
+ try {
975
+ const fallbackServer = await this.api.call.requestVoiceCall(this.partnerId, {
976
+ callId: this.callId,
977
+ callType: this.callType,
978
+ codec: this.codec,
979
+ });
980
+ this.applyServerInfo(fallbackServer);
981
+ }
982
+ catch {
983
+ // Ignore if mock/offline environment does not require fallback
984
+ }
985
+ }
986
+ // Bước 2: Mở UDP Socket & bắt tay Callee (0x01 0x7e cmd=12)
987
+ if (!isSignalingOnly) {
988
+ await this.ensureUdpSocket();
989
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
990
+ this.setState("handshaking_udp");
991
+ await this.performUdpInitHandshake(false);
992
+ }
993
+ else {
994
+ this.routingToken = this.callId >>> 0;
995
+ }
996
+ }
997
+ else {
998
+ this.routingToken = this.callId >>> 0;
999
+ }
1000
+ // Bước 3: Gửi tín hiệu bắt máy (Signal 402)
1001
+ const isVideo = this.callType === sendVoiceCall.CallType.Video;
1002
+ const negotiateVideoPipeline = isVideo || Boolean(options.allowVideoUpgrade) || Boolean(options.offCameraAtStart);
1003
+ this.extendData = ZRTPPacketCodec.buildZrtcExtendData({
1004
+ rtpAddress: this.rtpEndpoint.address,
1005
+ rtcpAddress: this.rtcpEndpoint.address,
1006
+ isVideo: negotiateVideoPipeline,
1007
+ packetMode: 2,
1008
+ srtpMode: this.srtpSession ? 1 : 0,
1009
+ srtpKey: this.srtpSession?.masterKeyBase64,
1010
+ });
1011
+ if (!options.skipAnswerSignal) {
1012
+ await this.api.call.answerVoiceCall(this.partnerId, this.callId, {
1013
+ sessionId: this.sessId,
1014
+ rtpAddress: this.rtpEndpoint.address,
1015
+ rtcpAddress: this.rtcpEndpoint.address,
1016
+ codec: this.codec,
1017
+ extendData: this.extendData,
1018
+ status: 0,
1019
+ });
1020
+ }
1021
+ if (negotiateVideoPipeline && !this.isCameraOn) {
1022
+ if (!isSignalingOnly) {
1023
+ void this.sendCameraState(false).catch(() => { });
1024
+ }
1025
+ void this.api.call
1026
+ .updateCallMediaState(this.partnerId, this.callId, {
1027
+ isOnCam: 0,
1028
+ isOnMic: -1,
1029
+ data: "",
1030
+ })
1031
+ .catch(() => { });
1032
+ }
1033
+ if (!isSignalingOnly) {
1034
+ this.startUdpPingLoop();
1035
+ this.startMediaKeepaliveLoop();
1036
+ }
1037
+ this.setState("active");
1038
+ const params = this.getConnectionParams();
1039
+ this.emit("connection_params", params);
1040
+ this.emit("active", params);
1041
+ return params;
1042
+ }
1043
+ /**
1044
+ * Từ chối cuộc gọi đến (1-1 hoặc Nhóm) từ sự kiện `call_event` (`act: "request"` / `"group_request"`):
1045
+ * - Với 1-1: gọi `/api/voicecall/cancel` (`11305`) và `/api/voicecall/endcall` (`11306`)
1046
+ * - Với Nhóm: gọi `/api/voicecall/group/cancel` (`12437`) và `/api/voicecall/group/endcall` (`12438`)
1047
+ */
1048
+ async rejectIncomingCall(callEvent, options = {}) {
1049
+ const info = parseIncomingCallEvent(callEvent, this.api.getContext().uid);
1050
+ const callerId = info.callerId || info.hostCall;
1051
+ const callId = info.callId;
1052
+ if (!callerId || !callId) {
1053
+ throw new ZaloApiError.ZaloApiError("Invalid incoming call event: missing callerId or callId");
1054
+ }
1055
+ this.callId = callId;
1056
+ this.partnerId = callerId;
1057
+ if (info.isGroupCall) {
1058
+ const hostCall = info.hostCall || info.groupId || callerId;
1059
+ this.groupId = hostCall;
1060
+ await this.api.call
1061
+ .cancelGroupCall(callId, {
1062
+ hostCall,
1063
+ callType: info.callType,
1064
+ partners: info.partners,
1065
+ })
1066
+ .catch(() => { });
1067
+ await this.api.call
1068
+ .endGroupCall(callId, {
1069
+ hostCall,
1070
+ callType: info.callType,
1071
+ partners: info.partners,
1072
+ })
1073
+ .catch(() => { });
1074
+ }
1075
+ else {
1076
+ await this.api.call
1077
+ .cancelVoiceCall(callerId, callId, {
1078
+ callType: info.callType,
1079
+ })
1080
+ .catch(() => { });
1081
+ await this.api.call
1082
+ .endVoiceCall(callerId, callId, {
1083
+ status: options.status ?? 3,
1084
+ })
1085
+ .catch(() => { });
1086
+ }
1087
+ this.setState("ended");
1088
+ this.cleanupResources();
1089
+ this.emit("ended", "rejected");
1090
+ }
1091
+ /**
1092
+ * Chấp nhận lời mời gọi Nhóm đến từ sự kiện `call_event` (`act: "group_request"`):
1093
+ * 1. Gửi `/api/voicecall/group/ringring` (`12439`, signal `506`)
1094
+ * 2. Mở socket UDP và bắt tay `InitCallCallee` (`0x01 0x7e`, `cmd=12`) (bỏ qua nếu `signalingOnly: true`)
1095
+ * 3. Gửi `/api/voicecall/group/answer` (`12436`, signal `503`)
1096
+ */
1097
+ async acceptIncomingGroupCall(callEvent, options = {}) {
1098
+ const info = parseIncomingCallEvent(callEvent, this.api.getContext().uid);
1099
+ const hostCall = info.hostCall || info.groupId || info.callerId;
1100
+ const callId = info.callId;
1101
+ if (!hostCall || !callId) {
1102
+ throw new ZaloApiError.ZaloApiError("Invalid incoming group call event: missing hostCall or callId");
1103
+ }
1104
+ this.resetSessionStateForNewCall(callId);
1105
+ const isSignalingOnly = Boolean(options.signalingOnly ?? this.options.signalingOnly);
1106
+ this.isCaller = false;
1107
+ this.callId = callId;
1108
+ this.groupId = info.groupId || hostCall;
1109
+ this.partnerId = hostCall;
1110
+ this.groupPartners = info.partners;
1111
+ this.fromUid32 = info.fromUid32;
1112
+ this.toUid32 = info.toUid32;
1113
+ this.sessId = info.sessId;
1114
+ this.codec = options.codec ?? info.codec ?? ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
1115
+ this.zrtpKey = info.zrtpKey;
1116
+ this.hasReceivedPartnerMedia = false;
1117
+ this.hasSentAnswerAck = false;
1118
+ this.isScreenSharing = false;
1119
+ this.activeStartedAtMs = 0;
1120
+ this.rtpEndpoint = {
1121
+ address: info.rtpAddress,
1122
+ host: info.rtpHost,
1123
+ port: info.rtpPort,
1124
+ };
1125
+ this.rtcpEndpoint = {
1126
+ address: info.rtcpAddress,
1127
+ host: info.rtcpHost,
1128
+ port: info.rtcpPort,
1129
+ };
1130
+ this.callType =
1131
+ info.callType === sendVoiceCall.CallType.GroupVideo || info.isVideo
1132
+ ? sendVoiceCall.CallType.GroupVideo
1133
+ : sendVoiceCall.CallType.GroupAudio;
1134
+ this.isCameraOn = this.callType === sendVoiceCall.CallType.GroupVideo && !options.offCameraAtStart;
1135
+ this.isMicMuted = false;
1136
+ this.isHold = false;
1137
+ if (this.options.autoAttachListener) {
1138
+ this.attachCallEventListener();
1139
+ }
1140
+ if (!options.skipRingRing) {
1141
+ await this.api.call.ringRingGroupCall(this.callId, {
1142
+ hostCall: this.partnerId,
1143
+ callType: this.callType,
1144
+ partners: this.groupPartners,
1145
+ });
1146
+ }
1147
+ if (!isSignalingOnly) {
1148
+ await this.ensureUdpSocket();
1149
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
1150
+ this.setState("handshaking_udp");
1151
+ await this.performUdpInitHandshake(false);
1152
+ }
1153
+ else {
1154
+ this.routingToken = this.callId >>> 0;
1155
+ }
1156
+ }
1157
+ else {
1158
+ this.routingToken = this.callId >>> 0;
1159
+ }
1160
+ const isVideo = this.callType === sendVoiceCall.CallType.GroupVideo;
1161
+ this.extendData = ZRTPPacketCodec.buildZrtcExtendData({
1162
+ rtpAddress: this.rtpEndpoint.address,
1163
+ rtcpAddress: this.rtcpEndpoint.address,
1164
+ isVideo: isVideo || (options.allowVideoUpgrade ?? true),
1165
+ packetMode: 2,
1166
+ srtpMode: this.srtpSession ? 1 : 0,
1167
+ srtpKey: this.srtpSession?.masterKeyBase64,
1168
+ });
1169
+ if (!options.skipAnswerSignal) {
1170
+ await this.api.call.answerGroupCall(this.callId, {
1171
+ hostCall: this.partnerId,
1172
+ callType: this.callType,
1173
+ partners: this.groupPartners,
1174
+ data: this.extendData,
1175
+ });
1176
+ }
1177
+ if (!isSignalingOnly) {
1178
+ this.startUdpPingLoop();
1179
+ this.startMediaKeepaliveLoop();
1180
+ }
1181
+ this.startGroupPingLoop();
1182
+ this.setState("active");
1183
+ const params = this.getConnectionParams();
1184
+ this.emit("connection_params", params);
1185
+ this.emit("active", params);
1186
+ return params;
1187
+ }
1188
+ /**
1189
+ * Khởi tạo cuộc gọi nhóm (Group Audio `CallType.GroupAudio = 5` hoặc Group Video `CallType.GroupVideo = 6`):
1190
+ * 1. Gọi `/api/voicecall/group/requestcall` (501) xin cấp phát máy chủ Relay UDP
1191
+ * 2. Mở socket UDP và bắt tay `0x01 0x7e` (`cmd=11`) (bỏ qua nếu `signalingOnly: true`)
1192
+ * 3. Gọi `/api/voicecall/group/request` (513) mời danh sách `partners` tham gia
1193
+ * 4. Duy trì nhịp tim UDP + `/api/voicecall/group/ping` (509) và luồng RTP/RTCP
1194
+ */
1195
+ async startGroupCall(groupId, options) {
1196
+ if (!groupId)
1197
+ throw new ZaloApiError.ZaloApiError("Missing groupId");
1198
+ if (!options?.partners || options.partners.length === 0) {
1199
+ throw new ZaloApiError.ZaloApiError("Missing group call partners");
1200
+ }
1201
+ this.resetSessionStateForNewCall();
1202
+ const isSignalingOnly = Boolean(options.signalingOnly ?? this.options.signalingOnly);
1203
+ this.isCaller = true;
1204
+ this.groupId = String(groupId);
1205
+ this.partnerId = String(groupId);
1206
+ this.groupPartners = options.partners.map(String);
1207
+ this.callType = options.callType ?? sendVoiceCall.CallType.GroupAudio;
1208
+ this.isCameraOn =
1209
+ (this.callType === sendVoiceCall.CallType.GroupVideo || this.callType === sendVoiceCall.CallType.Video) &&
1210
+ !options.offCameraAtStart;
1211
+ this.isMicMuted = false;
1212
+ this.isHold = false;
1213
+ this.isScreenSharing = false;
1214
+ this.activeStartedAtMs = 0;
1215
+ this.callId = options.callId ?? Math.floor(Math.random() * 2_000_000_000);
1216
+ this.codec = options.codec ?? ZRTPPacketCodec.DEFAULT_ZALO_AUDIO_CODEC;
1217
+ this.udpHandshakeVerified = false;
1218
+ this.hasReceivedPartnerMedia = false;
1219
+ this.hasSentAnswerAck = false;
1220
+ this.setState("allocating");
1221
+ if (this.options.autoAttachListener) {
1222
+ this.attachCallEventListener();
1223
+ }
1224
+ const isVideo = this.callType === sendVoiceCall.CallType.GroupVideo || this.callType === sendVoiceCall.CallType.Video;
1225
+ const reqRes = await this.api.call.requestGroupCall(this.groupId, {
1226
+ callId: this.callId,
1227
+ typeRequest: this.callType,
1228
+ partners: this.groupPartners,
1229
+ data: { codec: this.codec, isVideo: isVideo ? 1 : 0 },
1230
+ });
1231
+ const serverInfo = (typeof reqRes?.data === "string"
1232
+ ? JSON.parse(reqRes.data)
1233
+ : (reqRes?.data ?? reqRes));
1234
+ this.applyServerInfo(serverInfo);
1235
+ if (!isSignalingOnly) {
1236
+ await this.ensureUdpSocket();
1237
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
1238
+ this.setState("handshaking_udp");
1239
+ await this.performUdpInitHandshake(true);
1240
+ }
1241
+ else {
1242
+ this.routingToken = this.callId >>> 0;
1243
+ }
1244
+ }
1245
+ else {
1246
+ this.routingToken = this.callId >>> 0;
1247
+ }
1248
+ this.extendData = {
1249
+ ...ZRTPPacketCodec.buildZrtcExtendData({
1250
+ rtpAddress: this.rtpEndpoint.address,
1251
+ rtcpAddress: this.rtcpEndpoint.address,
1252
+ isVideo,
1253
+ packetMode: 2,
1254
+ srtpMode: this.srtpSession ? 1 : 0,
1255
+ srtpKey: this.srtpSession?.masterKeyBase64,
1256
+ }),
1257
+ rtcpDiff: 0,
1258
+ rtcp: [{ ip: this.rtcpEndpoint.host, port: this.rtcpEndpoint.port }],
1259
+ rtp: [{ ip: this.rtpEndpoint.host, port: this.rtpEndpoint.port }],
1260
+ status: 0,
1261
+ };
1262
+ await this.api.call.sendRequestGroupCall(this.groupId, {
1263
+ callId: this.callId,
1264
+ callType: this.callType,
1265
+ session: this.sessId,
1266
+ partners: this.groupPartners,
1267
+ data: this.extendData,
1268
+ });
1269
+ if (!isSignalingOnly) {
1270
+ this.startUdpPingLoop();
1271
+ this.startMediaKeepaliveLoop();
1272
+ }
1273
+ this.startGroupPingLoop();
1274
+ this.setState("active");
1275
+ const params = this.getConnectionParams();
1276
+ this.emit("connection_params", params);
1277
+ this.emit("active", params);
1278
+ return params;
1279
+ }
1280
+ /**
1281
+ * Tham gia cuộc gọi nhóm đang diễn ra (`/api/voicecall/group/join`, command `12043`).
1282
+ */
1283
+ async joinActiveGroupCall(callId, options) {
1284
+ if (!callId)
1285
+ throw new ZaloApiError.ZaloApiError("Missing callId");
1286
+ if (!options?.hostCall)
1287
+ throw new ZaloApiError.ZaloApiError("Missing hostCall");
1288
+ this.resetSessionStateForNewCall();
1289
+ const isSignalingOnly = Boolean(options.signalingOnly ?? this.options.signalingOnly);
1290
+ this.isCaller = false;
1291
+ this.callId = callId;
1292
+ this.groupId = String(options.groupId ?? options.hostCall);
1293
+ this.partnerId = String(options.hostCall);
1294
+ this.groupPartners = (options.partners ?? []).map(String);
1295
+ this.callType = options.callType ?? sendVoiceCall.CallType.GroupAudio;
1296
+ this.isCameraOn = this.callType === sendVoiceCall.CallType.GroupVideo || this.callType === sendVoiceCall.CallType.Video;
1297
+ this.isMicMuted = false;
1298
+ this.isHold = false;
1299
+ this.isScreenSharing = false;
1300
+ this.activeStartedAtMs = 0;
1301
+ this.sessId = options.session ?? `${this.partnerId}_${this.callId}_501`;
1302
+ if (this.options.autoAttachListener) {
1303
+ this.attachCallEventListener();
1304
+ }
1305
+ const isVideo = this.callType === sendVoiceCall.CallType.GroupVideo || this.callType === sendVoiceCall.CallType.Video;
1306
+ const joinRes = await this.api.call.joinGroupCall(this.callId, {
1307
+ hostCall: String(options.hostCall),
1308
+ audioState: this.isMicMuted ? 0 : 1,
1309
+ videoState: isVideo ? 1 : 0,
1310
+ data: { codec: this.codec },
1311
+ });
1312
+ const serverInfo = (typeof joinRes?.data === "string"
1313
+ ? JSON.parse(joinRes.data)
1314
+ : (joinRes?.data ?? joinRes));
1315
+ if (options.rtpAddress) {
1316
+ this.rtpEndpoint = parseEndpoint(options.rtpAddress);
1317
+ this.rtcpEndpoint = parseEndpoint(options.rtcpAddress || options.rtpAddress);
1318
+ }
1319
+ else {
1320
+ this.applyServerInfo(serverInfo);
1321
+ }
1322
+ if (!isSignalingOnly) {
1323
+ await this.ensureUdpSocket();
1324
+ if (!options.skipUdpHandshake && this.rtpEndpoint.port > 0) {
1325
+ this.setState("handshaking_udp");
1326
+ await this.performUdpInitHandshake(false);
1327
+ }
1328
+ else {
1329
+ this.routingToken = this.callId >>> 0;
1330
+ }
1331
+ this.startUdpPingLoop();
1332
+ this.startMediaKeepaliveLoop();
1333
+ }
1334
+ else {
1335
+ this.routingToken = this.callId >>> 0;
1336
+ }
1337
+ this.startGroupPingLoop();
1338
+ this.setState("active");
1339
+ const params = this.getConnectionParams();
1340
+ this.emit("connection_params", params);
1341
+ this.emit("active", params);
1342
+ return params;
1343
+ }
1344
+ /**
1345
+ * Mời thêm thành viên vào cuộc gọi nhóm đang diễn ra (`/api/voicecall/group/adduser`, command `12435`).
1346
+ */
1347
+ async addMembersToGroupCall(partners) {
1348
+ if (!this.callId)
1349
+ throw new ZaloApiError.ZaloApiError("No active group call");
1350
+ const cleanPartners = partners.map(String).filter(Boolean);
1351
+ for (const p of cleanPartners) {
1352
+ if (!this.groupPartners.includes(p))
1353
+ this.groupPartners.push(p);
1354
+ }
1355
+ return this.api.call.addUserGroupCall(this.callId, {
1356
+ hostCall: this.groupId || this.partnerId,
1357
+ session: this.sessId,
1358
+ callType: this.callType,
1359
+ partners: cleanPartners,
1360
+ data: this.extendData,
1361
+ });
1362
+ }
1363
+ /**
1364
+ * Bật / tắt Camera trong cuộc gọi Video (đồng bộ cả gói UDP `cmd=32, subCmd=7/8` và HTTP `/api/voicecall/conf` hoặc `/api/voicecall/group/broadcast`).
1365
+ */
1366
+ async setCameraEnabled(enabled) {
1367
+ const wasAudioCall = this.callType === sendVoiceCall.CallType.Audio || this.callType === sendVoiceCall.CallType.GroupAudio;
1368
+ this.isCameraOn = enabled;
1369
+ if (enabled && this.callType === sendVoiceCall.CallType.Audio) {
1370
+ this.callType = sendVoiceCall.CallType.Video;
1371
+ }
1372
+ else if (enabled && this.callType === sendVoiceCall.CallType.GroupAudio) {
1373
+ this.callType = sendVoiceCall.CallType.GroupVideo;
1374
+ }
1375
+ if (enabled) {
1376
+ this.startMediaKeepaliveLoop();
1377
+ }
1378
+ await this.sendCameraState(enabled).catch(() => { });
1379
+ if (this.groupId && this.callId > 0) {
1380
+ await this.api.call
1381
+ .broadcastGroupCallState(this.callId, {
1382
+ hostCall: this.groupId,
1383
+ audioState: this.isMicMuted ? 0 : 1,
1384
+ videoState: enabled ? 1 : 0,
1385
+ })
1386
+ .catch(() => { });
1387
+ }
1388
+ else if (this.partnerId && this.callId > 0) {
1389
+ if (enabled && wasAudioCall) {
1390
+ await this.api.call
1391
+ .requestAudioToVideoCall(this.partnerId, this.callId, { status: 1 })
1392
+ .catch(() => { });
1393
+ }
1394
+ await this.api.call
1395
+ .updateCallMediaState(this.partnerId, this.callId, {
1396
+ isOnCam: enabled ? 1 : 0,
1397
+ isOnMic: -1,
1398
+ data: "",
1399
+ })
1400
+ .catch(() => { });
1401
+ }
1402
+ if (enabled) {
1403
+ void this.sendRtcpPli().catch(() => { });
1404
+ }
1405
+ this.emit("media_state_change", {
1406
+ isLocal: true,
1407
+ isCameraOn: this.isCameraOn,
1408
+ isMicMuted: this.isMicMuted,
1409
+ isHold: this.isHold,
1410
+ isScreenSharing: this.isScreenSharing,
1411
+ callType: this.callType,
1412
+ });
1413
+ }
1414
+ /**
1415
+ * Bật / tắt Microphone trong cuộc gọi (đồng bộ cả gói UDP `cmd=32, subCmd=5/6` và HTTP `/api/voicecall/conf` hoặc `/api/voicecall/group/broadcast`).
1416
+ */
1417
+ async setMicMuted(muted) {
1418
+ this.isMicMuted = muted;
1419
+ await this.sendMicState(!muted).catch(() => { });
1420
+ if (this.groupId && this.callId > 0) {
1421
+ await this.api.call
1422
+ .broadcastGroupCallState(this.callId, {
1423
+ hostCall: this.groupId,
1424
+ audioState: muted ? 0 : 1,
1425
+ videoState: this.isCameraOn ? 1 : 0,
1426
+ })
1427
+ .catch(() => { });
1428
+ }
1429
+ else if (this.partnerId && this.callId > 0) {
1430
+ await this.api.call
1431
+ .updateCallMediaState(this.partnerId, this.callId, {
1432
+ isOnCam: -1,
1433
+ isOnMic: muted ? 0 : 1,
1434
+ data: "",
1435
+ })
1436
+ .catch(() => { });
1437
+ }
1438
+ this.emit("media_state_change", {
1439
+ isLocal: true,
1440
+ isCameraOn: this.isCameraOn,
1441
+ isMicMuted: this.isMicMuted,
1442
+ isHold: this.isHold,
1443
+ isScreenSharing: this.isScreenSharing,
1444
+ callType: this.callType,
1445
+ });
1446
+ }
1447
+ /**
1448
+ * Tạm giữ (`hold`) hoặc tiếp tục (`unhold`) cuộc gọi 1-1 (`/api/voicecall/holdreq` & `/api/voicecall/unholdreq`).
1449
+ */
1450
+ async setHoldState(isHold, isResponse = false) {
1451
+ this.isHold = isHold;
1452
+ if (this.partnerId) {
1453
+ if (isHold) {
1454
+ await this.api.call.holdVoiceCall(this.partnerId, { isResponse }).catch(() => { });
1455
+ }
1456
+ else {
1457
+ await this.api.call.unholdVoiceCall(this.partnerId, { isResponse }).catch(() => { });
1458
+ this.clearIncomingVideoAssemblyBuffers();
1459
+ void this.sendMicState(!this.isMicMuted).catch(() => { });
1460
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
1461
+ void this.sendRtcpPli().catch(() => { });
1462
+ }
1463
+ }
1464
+ this.emit("media_state_change", {
1465
+ isLocal: true,
1466
+ isCameraOn: this.isCameraOn,
1467
+ isMicMuted: this.isMicMuted,
1468
+ isHold: this.isHold,
1469
+ isScreenSharing: this.isScreenSharing,
1470
+ callType: this.callType,
1471
+ });
1472
+ }
1473
+ /**
1474
+ * Bật / tắt chia sẻ màn hình trong cuộc gọi 1-1 (`/api/voicecall/requestsharescreen`, command `12064`)
1475
+ * hoặc cuộc gọi Nhóm (`/api/voicecall/group/sharescreen`, command `12044`).
1476
+ */
1477
+ async setScreenShareEnabled(enabled) {
1478
+ this.isScreenSharing = enabled;
1479
+ if (this.groupId && this.callId > 0) {
1480
+ await this.api.call
1481
+ .shareScreenGroupCall(this.callId, {
1482
+ hostCall: this.groupId,
1483
+ status: enabled ? 1 : 0,
1484
+ audioState: this.isMicMuted ? 0 : 1,
1485
+ videoState: this.isCameraOn || enabled ? 1 : 0,
1486
+ })
1487
+ .catch(() => { });
1488
+ }
1489
+ else if (this.partnerId && this.callId > 0) {
1490
+ await this.api.call
1491
+ .requestShareScreenCall(this.partnerId, this.callId, {
1492
+ status: enabled ? 1 : 0,
1493
+ })
1494
+ .catch(() => { });
1495
+ }
1496
+ this.emit("media_state_change", {
1497
+ isLocal: true,
1498
+ isCameraOn: this.isCameraOn,
1499
+ isMicMuted: this.isMicMuted,
1500
+ isHold: this.isHold,
1501
+ isScreenSharing: this.isScreenSharing,
1502
+ callType: this.callType,
1503
+ });
1504
+ }
1505
+ /**
1506
+ * Chuyển đổi cuộc gọi Thoại (Audio) đang diễn ra lên cuộc gọi Video (`/api/voicecall/requestaudio2video`, command `12056`):
1507
+ * - `status = 1`: Gửi yêu cầu bật Video
1508
+ * - `status = 2`: Đồng ý yêu cầu bật Video từ đối phương
1509
+ * Tự động kích hoạt luồng Video RTP/RTCP, tín hiệu UDP `OnCamera` (`cmd=32, subCmd=7`) & HTTP `/api/voicecall/conf` (`isOnCam=1, isOnMic=-1`) ngay lập tức.
1510
+ */
1511
+ async upgradeAudioToVideoCall(status = 1) {
1512
+ this.callType = this.groupId ? sendVoiceCall.CallType.GroupVideo : sendVoiceCall.CallType.Video;
1513
+ this.isCameraOn = true;
1514
+ this.startMediaKeepaliveLoop();
1515
+ await this.sendCameraState(true).catch(() => { });
1516
+ if (Date.now() - this.lastExternalVideoSentAtMs >= 300) {
1517
+ await this.sendH264AnnexBFrame(H264_KEEPALIVE_ANNEXB_FRAME, {
1518
+ timestampStep: 6000,
1519
+ isKeepalive: true,
1520
+ }).catch(() => { });
1521
+ }
1522
+ if (this.partnerId && this.callId > 0 && !this.groupId) {
1523
+ await this.api.call
1524
+ .requestAudioToVideoCall(this.partnerId, this.callId, { status })
1525
+ .catch(() => { });
1526
+ await this.api.call
1527
+ .updateCallMediaState(this.partnerId, this.callId, {
1528
+ isOnCam: 1,
1529
+ isOnMic: -1,
1530
+ data: "",
1531
+ })
1532
+ .catch(() => { });
1533
+ }
1534
+ else if (this.groupId && this.callId > 0) {
1535
+ await this.api.call
1536
+ .broadcastGroupCallState(this.callId, {
1537
+ hostCall: this.groupId,
1538
+ audioState: this.isMicMuted ? 0 : 1,
1539
+ videoState: 1,
1540
+ })
1541
+ .catch(() => { });
1542
+ }
1543
+ this.emit("media_state_change", {
1544
+ isLocal: true,
1545
+ isCameraOn: this.isCameraOn,
1546
+ isMicMuted: this.isMicMuted,
1547
+ isHold: this.isHold,
1548
+ isScreenSharing: this.isScreenSharing,
1549
+ callType: this.callType,
1550
+ });
1551
+ }
1552
+ /**
1553
+ * Gửi trực tiếp một khung âm thanh Opus (mặc định 20ms @ 16kHz = 320 samples)
1554
+ * qua đường truyền UDP của cuộc gọi.
1555
+ * Tự động đóng gói RTP 12-byte header -> (Tuỳ chọn) Mã hoá SRTP -> Bọc `ZRTPPacket` header.
1556
+ */
1557
+ async sendAudioFrame(opusPayload, options = {}) {
1558
+ if (!options.isKeepalive) {
1559
+ this.lastExternalAudioSentAtMs = Date.now();
1560
+ }
1561
+ const seq = options.sequenceNumber ?? (this.audioSeq = (this.audioSeq + 1) & 0xffff);
1562
+ const samples = options.samples ?? 320;
1563
+ const ts = options.timestamp ?? (this.audioTimestamp = (this.audioTimestamp + samples) >>> 0);
1564
+ const ssrc = options.ssrc ?? this.getLocalAudioSsrc();
1565
+ this.audioPacketCount = (this.audioPacketCount + 1) >>> 0;
1566
+ this.audioOctetCount = (this.audioOctetCount + opusPayload.length) >>> 0;
1567
+ const rtpPacket = ZaloSrtpSession.buildRtpPacket({
1568
+ payloadType: options.payloadType ?? 112,
1569
+ sequenceNumber: seq,
1570
+ timestamp: ts,
1571
+ ssrc,
1572
+ marker: options.marker,
1573
+ payload: opusPayload,
1574
+ });
1575
+ await this.sendAudioRtp(rtpPacket);
1576
+ }
1577
+ /**
1578
+ * Gửi một gói tin chuẩn WebRTC RTP (Audio) qua đường truyền UDP Zalo.
1579
+ */
1580
+ async sendAudioRtp(rtpPacket) {
1581
+ const protectedRtp = this.srtpSession ? this.srtpSession.protectRtp(rtpPacket) : rtpPacket;
1582
+ const token = (this.routingToken || this.callId) >>> 0;
1583
+ const role = this.isCaller ? ZRTPPacketCodec.ZRTCRole.Caller : ZRTPPacketCodec.ZRTCRole.Callee;
1584
+ const framed = this.options.packetMode === 1
1585
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.AudioRtp, token, protectedRtp)
1586
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.AudioRtp, token, protectedRtp, role);
1587
+ await this.sendRawUdp(framed, this.rtpEndpoint.host, this.rtpEndpoint.port);
1588
+ }
1589
+ /**
1590
+ * Gửi trực tiếp một khung hình ảnh H.264 NAL / RTP payload (mặc định 90kHz clock, payloadType=97)
1591
+ * qua đường truyền UDP của cuộc gọi Video.
1592
+ */
1593
+ async sendVideoFrame(h264Payload, options = {}) {
1594
+ if (!options.isKeepalive) {
1595
+ this.lastExternalVideoSentAtMs = Date.now();
1596
+ }
1597
+ const seq = options.sequenceNumber ?? (this.videoSeq = (this.videoSeq + 1) & 0xffff);
1598
+ const step = options.timestampStep ?? 3750; // ~24fps @ 90kHz
1599
+ const ts = options.timestamp ?? (this.videoTimestamp = (this.videoTimestamp + step) >>> 0);
1600
+ const ssrc = options.ssrc ?? this.getLocalVideoSsrc();
1601
+ this.videoPacketCount = (this.videoPacketCount + 1) >>> 0;
1602
+ this.videoOctetCount = (this.videoOctetCount + h264Payload.length) >>> 0;
1603
+ const rtpPacket = ZaloSrtpSession.buildRtpPacket({
1604
+ payloadType: options.payloadType ?? 97,
1605
+ sequenceNumber: seq,
1606
+ timestamp: ts,
1607
+ ssrc,
1608
+ marker: options.marker ?? true,
1609
+ payload: h264Payload,
1610
+ });
1611
+ await this.sendVideoRtp(rtpPacket);
1612
+ }
1613
+ /**
1614
+ * Đóng gói một khung hình H.264 dạng Annex-B (`00 00 00 01 <NAL>`) theo chuẩn RFC 6184:
1615
+ * - Tự động tách các NAL unit, lưu cache SPS (`type=7`) / PPS (`type=8`) và đóng gói STAP-A (`0x78`) trước IDR (`type=5`)
1616
+ * - Không cho phép khung keepalive 16x16 ghi đè `localSpsNal` / `localPpsNal` của camera thực
1617
+ * - Tự động phân mảnh các NAL lớn (> 1150 bytes) thành các gói RTP **FU-A (`type=28`)** cùng `timestamp` 90kHz
1618
+ * - Đặt cờ `marker = true` ở gói RTP cuối cùng của khung hình để bộ giải mã đối phương hiển thị ngay.
1619
+ */
1620
+ async sendH264AnnexBFrame(annexB, options = {}) {
1621
+ const rawNals = splitAnnexBNals(annexB).filter((n) => n.length > 0);
1622
+ if (rawNals.length === 0)
1623
+ return;
1624
+ if (!options.isKeepalive) {
1625
+ this.lastExternalVideoSentAtMs = Date.now();
1626
+ }
1627
+ let inlineSps = null;
1628
+ let inlinePps = null;
1629
+ let hasIdr = false;
1630
+ const vclAndSeiNals = [];
1631
+ for (const nal of rawNals) {
1632
+ const nalType = nal[0] & 0x1f;
1633
+ if (nalType === 7) {
1634
+ inlineSps = node_buffer.Buffer.from(nal);
1635
+ if (!options.isKeepalive || !this.localSpsNal) {
1636
+ this.localSpsNal = inlineSps;
1637
+ }
1638
+ }
1639
+ else if (nalType === 8) {
1640
+ inlinePps = node_buffer.Buffer.from(nal);
1641
+ if (!options.isKeepalive || !this.localPpsNal) {
1642
+ this.localPpsNal = inlinePps;
1643
+ }
1644
+ }
1645
+ else if (nalType === 9) {
1646
+ // Bỏ qua Access Unit Delimiter (AUD, type=9) để tương thích phần cứng VideoToolbox iOS
1647
+ continue;
1648
+ }
1649
+ else {
1650
+ if (nalType === 5)
1651
+ hasIdr = true;
1652
+ vclAndSeiNals.push(nal);
1653
+ }
1654
+ }
1655
+ const spsToUse = inlineSps ?? (hasIdr ? this.localSpsNal : null);
1656
+ const ppsToUse = inlinePps ?? (hasIdr ? this.localPpsNal : null);
1657
+ const step = options.timestampStep ?? 6000; // ~15fps @ 90kHz
1658
+ const frameTs = options.timestamp ?? (this.videoTimestamp = (this.videoTimestamp + step) >>> 0);
1659
+ const maxPayloadSize = 1150;
1660
+ const rtpPayloads = [];
1661
+ // Gộp SPS + PPS vào một gói RFC 6184 STAP-A (type=24, 0x78) duy nhất trước IDR
1662
+ // khớp với RtpPacketizerH264::PacketizeStapA (0x1003db010)
1663
+ if (spsToUse && ppsToUse && spsToUse.length + ppsToUse.length + 5 <= maxPayloadSize) {
1664
+ const stapA = node_buffer.Buffer.allocUnsafe(1 + 2 + spsToUse.length + 2 + ppsToUse.length);
1665
+ stapA[0] = 0x78; // F=0, NRI=3 (0x60), Type=24 (STAP-A)
1666
+ stapA.writeUInt16BE(spsToUse.length, 1);
1667
+ spsToUse.copy(stapA, 3);
1668
+ const ppsOffset = 3 + spsToUse.length;
1669
+ stapA.writeUInt16BE(ppsToUse.length, ppsOffset);
1670
+ ppsToUse.copy(stapA, ppsOffset + 2);
1671
+ rtpPayloads.push(stapA);
1672
+ }
1673
+ else {
1674
+ if (spsToUse)
1675
+ rtpPayloads.push(spsToUse);
1676
+ if (ppsToUse)
1677
+ rtpPayloads.push(ppsToUse);
1678
+ }
1679
+ for (const nal of vclAndSeiNals) {
1680
+ if (nal.length <= maxPayloadSize) {
1681
+ rtpPayloads.push(nal);
1682
+ }
1683
+ else {
1684
+ // RFC 6184 FU-A (type = 28) fragmentation
1685
+ const nalHeader = nal[0];
1686
+ const fnri = nalHeader & 0xe0;
1687
+ const nalType = nalHeader & 0x1f;
1688
+ const fuIndicator = fnri | 28;
1689
+ const body = nal.subarray(1);
1690
+ let offset = 0;
1691
+ while (offset < body.length) {
1692
+ const chunkLen = Math.min(maxPayloadSize, body.length - offset);
1693
+ const isFirst = offset === 0;
1694
+ const isLast = offset + chunkLen >= body.length;
1695
+ const fuHeader = (isFirst ? 0x80 : 0) | (isLast ? 0x40 : 0) | nalType;
1696
+ const frag = node_buffer.Buffer.allocUnsafe(2 + chunkLen);
1697
+ frag[0] = fuIndicator;
1698
+ frag[1] = fuHeader;
1699
+ body.copy(frag, 2, offset, offset + chunkLen);
1700
+ rtpPayloads.push(frag);
1701
+ offset += chunkLen;
1702
+ }
1703
+ }
1704
+ }
1705
+ // Trong RTPSender::SendOutgoingData (0x1003ed76c) & RTPReceiverVideo::ParseRtpPacket (0x1003e8b4c):
1706
+ // Zalo quy định:
1707
+ // - Keyframe H.264 (SPS+PPS+IDR, frameType=3) BẮT BUỘC gửi trên payloadType = 98 (0x62)
1708
+ // - Delta frame H.264 (P-slice, frameType=4) gửi trên payloadType = 97 (0x61)
1709
+ const resolvedPayloadType = options.payloadType ?? (hasIdr ? 98 : 97);
1710
+ for (let i = 0; i < rtpPayloads.length; i++) {
1711
+ const isLastPacket = i === rtpPayloads.length - 1;
1712
+ await this.sendVideoFrame(rtpPayloads[i], {
1713
+ payloadType: resolvedPayloadType,
1714
+ timestamp: frameTs,
1715
+ ssrc: options.ssrc ?? this.getLocalVideoSsrc(),
1716
+ marker: isLastPacket,
1717
+ isKeepalive: options.isKeepalive,
1718
+ });
1719
+ }
1720
+ }
1721
+ /**
1722
+ * Gửi tín hiệu bật/tắt Camera (`cmd = 32, subCmd = 7` `OnCamera` / `subCmd = 8` `OffCamera`)
1723
+ * khớp với `CallController::vidSendSignalOffCamera` (`0x10016fbe4`).
1724
+ * Phát lặp 3 lần (0ms, 60ms, 150ms) để chống mất gói UDP.
1725
+ */
1726
+ async sendCameraState(isCameraOn, reason = 0) {
1727
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
1728
+ return;
1729
+ const fromUid = this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
1730
+ const token = (this.routingToken || this.callId) >>> 0;
1731
+ const payload = node_buffer.Buffer.alloc(4);
1732
+ payload.writeInt32LE(reason | 0, 0);
1733
+ const pkt = ZRTPPacketCodec.buildRequestForwardPacket({
1734
+ seqNum: 0,
1735
+ seqId: 0,
1736
+ fromUid,
1737
+ routingToken: token,
1738
+ subCmd: isCameraOn ? ZRTPPacketCodec.ZRTPForwardSubCommand.OnCamera : ZRTPPacketCodec.ZRTPForwardSubCommand.OffCamera,
1739
+ payload,
1740
+ });
1741
+ await this.sendRawUdp(pkt, this.rtpEndpoint.host, this.rtpEndpoint.port);
1742
+ for (const delayMs of [60, 150]) {
1743
+ const t = setTimeout(() => {
1744
+ if (this.udpSocket && this.rtpEndpoint.port > 0 && this.state !== "ended") {
1745
+ void this.sendRawUdp(pkt, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
1746
+ }
1747
+ }, delayMs);
1748
+ if (typeof t.unref === "function")
1749
+ t.unref();
1750
+ }
1751
+ }
1752
+ /**
1753
+ * Gửi tín hiệu bật/tắt Microphone (`cmd = 32, subCmd = 5` `OnMic` / `subCmd = 6` `OffMic`)
1754
+ * qua máy chủ Relay UDP.
1755
+ */
1756
+ async sendMicState(isMicOn) {
1757
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
1758
+ return;
1759
+ const fromUid = this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
1760
+ const token = (this.routingToken || this.callId) >>> 0;
1761
+ const payload = node_buffer.Buffer.alloc(4);
1762
+ payload.writeInt32LE(0, 0);
1763
+ const pkt = ZRTPPacketCodec.buildRequestForwardPacket({
1764
+ seqNum: 0,
1765
+ seqId: 0,
1766
+ fromUid,
1767
+ routingToken: token,
1768
+ subCmd: isMicOn ? ZRTPPacketCodec.ZRTPForwardSubCommand.OnMic : ZRTPPacketCodec.ZRTPForwardSubCommand.OffMic,
1769
+ payload,
1770
+ });
1771
+ await this.sendRawUdp(pkt, this.rtpEndpoint.host, this.rtpEndpoint.port);
1772
+ for (const delayMs of [60, 150]) {
1773
+ const t = setTimeout(() => {
1774
+ if (this.udpSocket && this.rtpEndpoint.port > 0 && this.state !== "ended") {
1775
+ void this.sendRawUdp(pkt, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
1776
+ }
1777
+ }, delayMs);
1778
+ if (typeof t.unref === "function")
1779
+ t.unref();
1780
+ }
1781
+ }
1782
+ /**
1783
+ * Gửi gói RTCP PLI (Picture Loss Indication — `PT=206, FMT=1`) yêu cầu máy đối phương
1784
+ * gửi ngay khung hình khoá H.264 IDR kèm SPS/PPS.
1785
+ */
1786
+ async sendRtcpPli() {
1787
+ if (!this.remoteVideoSsrc)
1788
+ return;
1789
+ const pli = node_buffer.Buffer.alloc(12);
1790
+ pli.writeUInt8(0x81, 0); // V=2, FMT=1 (PLI)
1791
+ pli.writeUInt8(206, 1); // PT=206 (PSFB)
1792
+ pli.writeUInt16BE(2, 2); // length = 2 words (12 bytes)
1793
+ pli.writeUInt32BE(this.getLocalVideoSsrc(), 4);
1794
+ pli.writeUInt32BE(this.remoteVideoSsrc >>> 0, 8);
1795
+ await this.sendRtcp(pli, true);
1796
+ }
1797
+ /**
1798
+ * Gửi một gói tin chuẩn WebRTC RTP (Video H.264) qua đường truyền UDP Zalo.
1799
+ */
1800
+ async sendVideoRtp(rtpPacket) {
1801
+ const protectedRtp = this.srtpSession ? this.srtpSession.protectRtp(rtpPacket) : rtpPacket;
1802
+ const token = (this.routingToken || this.callId) >>> 0;
1803
+ const role = this.isCaller ? ZRTPPacketCodec.ZRTCRole.Caller : ZRTPPacketCodec.ZRTCRole.Callee;
1804
+ const framed = this.options.packetMode === 1
1805
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(ZRTPPacketCodec.ZRTPLegacyMediaType.VideoRtp, token, protectedRtp)
1806
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(ZRTPPacketCodec.ZRTCMediaSubCommand.VideoRtp, token, protectedRtp, role);
1807
+ await this.sendRawUdp(framed, this.rtpEndpoint.host, this.rtpEndpoint.port);
1808
+ }
1809
+ /**
1810
+ * Gửi gói tin điều khiển chất lượng RTCP (Audio hoặc Video) qua đường truyền UDP Zalo.
1811
+ */
1812
+ async sendRtcp(rtcpPacket, isVideo = false) {
1813
+ const protectedRtcp = this.srtpSession && Number(this.extendData?.srtcp) === 1
1814
+ ? this.srtpSession.protectRtcp(rtcpPacket)
1815
+ : rtcpPacket;
1816
+ const token = (this.routingToken || this.callId) >>> 0;
1817
+ const role = this.isCaller ? ZRTPPacketCodec.ZRTCRole.Caller : ZRTPPacketCodec.ZRTCRole.Callee;
1818
+ const framed = this.options.packetMode === 1
1819
+ ? ZRTPPacketCodec.wrapLegacyMediaPacket(isVideo ? ZRTPPacketCodec.ZRTPLegacyMediaType.VideoRtcp : ZRTPPacketCodec.ZRTPLegacyMediaType.AudioRtcp, token, protectedRtcp)
1820
+ : ZRTPPacketCodec.wrapZrtcMediaPacket(isVideo ? ZRTPPacketCodec.ZRTCMediaSubCommand.VideoRtcp : ZRTPPacketCodec.ZRTCMediaSubCommand.AudioRtcp, token, protectedRtcp, role);
1821
+ const target = this.rtcpEndpoint.port > 0 ? this.rtcpEndpoint : this.rtpEndpoint;
1822
+ await this.sendRawUdp(framed, target.host, target.port);
1823
+ }
1824
+ /**
1825
+ * Kết thúc cuộc gọi hiện tại:
1826
+ * 1. Gửi gói UDP `RequestEndCall` (`0x01 0x7e`, `cmd=3`, 25 bytes) 3 lần tới máy chủ Relay (`_sendRequestZRTPEndCall` @ `0x10016a880`)
1827
+ * 2. Giải phóng toàn bộ tài nguyên UDP socket & bộ định thời
1828
+ * 3. Gửi tín hiệu HTTP `/api/voicecall/cancel` (`11305`) và `/api/voicecall/endcall` (`11306`) hoặc `/api/voicecall/group/end` (`12046`)
1829
+ * cho mọi thao tác ngắt máy từ phía local (`local_end`, `user_hangup`, `new_call`, ...).
1830
+ */
1831
+ async endCall(reason = "local_end") {
1832
+ if (this.state === "ended")
1833
+ return;
1834
+ const prevState = this.state;
1835
+ this.setState("ended");
1836
+ if (this.udpSocket && this.rtpEndpoint.port > 0 && this.callId > 0) {
1837
+ try {
1838
+ const endPkt = ZRTPPacketCodec.buildEndCallRequest({
1839
+ seqNum: 0,
1840
+ callId: this.callId,
1841
+ fromUid: this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid),
1842
+ toUid: this.toUid32 || ZRTPPacketCodec.uidToUint32(this.partnerId),
1843
+ routingToken: (this.routingToken || this.callId) >>> 0,
1844
+ flags: 0,
1845
+ });
1846
+ for (let i = 0; i < 3; i++) {
1847
+ await this.sendRawUdp(endPkt, this.rtpEndpoint.host, this.rtpEndpoint.port);
1848
+ }
1849
+ }
1850
+ catch {
1851
+ // Ignore UDP send error on teardown
1852
+ }
1853
+ }
1854
+ this.cleanupResources();
1855
+ const isRemoteReason = reason === "remote_end" ||
1856
+ reason === "remote_cancel" ||
1857
+ reason === "end" ||
1858
+ reason === "cancel" ||
1859
+ reason === "rejected" ||
1860
+ reason === "busy";
1861
+ if (this.callId > 0 && !isRemoteReason) {
1862
+ try {
1863
+ if (this.groupId ||
1864
+ this.callType === sendVoiceCall.CallType.GroupAudio ||
1865
+ this.callType === sendVoiceCall.CallType.GroupVideo) {
1866
+ if (prevState === "ringing" && !this.hasReceivedPartnerMedia && this.isCaller) {
1867
+ await this.api.call
1868
+ .cancelGroupCall(this.callId, {
1869
+ hostCall: this.groupId || this.partnerId,
1870
+ callType: this.callType,
1871
+ partners: this.groupPartners,
1872
+ })
1873
+ .catch(() => { });
1874
+ }
1875
+ await this.api.call
1876
+ .endGroupCall(this.callId, {
1877
+ hostCall: this.groupId || this.partnerId,
1878
+ callType: this.callType,
1879
+ partners: this.groupPartners,
1880
+ })
1881
+ .catch(() => { });
1882
+ }
1883
+ else if (this.partnerId) {
1884
+ const wasStillRinging = (prevState === "ringing" ||
1885
+ prevState === "handshaking_udp" ||
1886
+ prevState === "allocating") &&
1887
+ !this.hasReceivedPartnerMedia;
1888
+ if (wasStillRinging && this.isCaller) {
1889
+ await this.api.call
1890
+ .cancelVoiceCall(this.partnerId, this.callId, {
1891
+ callType: this.callType,
1892
+ })
1893
+ .catch(() => { });
1894
+ // Dự phòng trường hợp máy đối phương vừa bắt máy nhưng sự kiện WS chưa kịp về:
1895
+ await this.api.call.endVoiceCall(this.partnerId, this.callId).catch(() => { });
1896
+ }
1897
+ else {
1898
+ await this.api.call.endVoiceCall(this.partnerId, this.callId).catch(() => { });
1899
+ }
1900
+ if (this.activeStartedAtMs > 0) {
1901
+ const durationSec = Math.max(1, Math.floor((Date.now() - this.activeStartedAtMs) / 1000));
1902
+ void this.api.call
1903
+ .logEndVoiceCall(this.partnerId, this.callId, {
1904
+ duration: durationSec,
1905
+ status: 0,
1906
+ role: this.isCaller,
1907
+ })
1908
+ .catch(() => { });
1909
+ }
1910
+ }
1911
+ }
1912
+ catch {
1913
+ // Ignore signaling error if already ended
1914
+ }
1915
+ }
1916
+ this.emit("ended", reason);
1917
+ }
1918
+ /**
1919
+ * Xử lý sự kiện `call_event` từ WebSocket (hoặc truyền thủ công).
1920
+ */
1921
+ async handleCallEvent(event) {
1922
+ const rawParams = event.data.params !== undefined
1923
+ ? safeJsonParseRecord(event.data.params)
1924
+ : event.data.data !== undefined
1925
+ ? safeJsonParseRecord(event.data.data)
1926
+ : event.data;
1927
+ const eventCallId = Number(event.data.callId ??
1928
+ event.data.call_id ??
1929
+ event.data.id ??
1930
+ rawParams.callId ??
1931
+ rawParams.call_id ??
1932
+ rawParams.id ??
1933
+ 0);
1934
+ if (this.callId > 0 && eventCallId > 0 && eventCallId !== this.callId) {
1935
+ return;
1936
+ }
1937
+ this.emit("call_event", event);
1938
+ if (event.type === CallEvent.CallEventType.REQUEST ||
1939
+ event.type === CallEvent.CallEventType.GROUP_REQUEST ||
1940
+ event.act === "request" ||
1941
+ event.act === "group_request") {
1942
+ const incomingInfo = parseIncomingCallEvent(event, this.api.getContext().uid);
1943
+ this.emit("incoming_call", incomingInfo);
1944
+ return;
1945
+ }
1946
+ const extendRaw = rawParams.extendData !== undefined
1947
+ ? safeJsonParseRecord(rawParams.extendData)
1948
+ : event.data.extendData !== undefined
1949
+ ? safeJsonParseRecord(event.data.extendData)
1950
+ : {};
1951
+ if (typeof extendRaw.srtpKey === "string" && extendRaw.srtpKey.length >= 40) {
1952
+ this.srtpSession = new ZaloSrtpSession.ZaloSrtpSession(extendRaw.srtpKey, this.options.srtpSuite);
1953
+ }
1954
+ else if (extendRaw.srtpMode === 0 && !this.options.enableSrtp) {
1955
+ this.srtpSession = null;
1956
+ }
1957
+ if (event.type === CallEvent.CallEventType.RINGRING) {
1958
+ if (this.state === "handshaking_udp" || this.state === "allocating") {
1959
+ this.setState("ringing");
1960
+ }
1961
+ }
1962
+ else if (event.type === CallEvent.CallEventType.ANSWER) {
1963
+ this.hasReceivedPartnerMedia = true;
1964
+ if (this.isCaller && this.partnerId && this.callId > 0 && !this.groupId && !this.hasSentAnswerAck) {
1965
+ this.hasSentAnswerAck = true;
1966
+ try {
1967
+ await this.api.call.answerAckVoiceCall(this.partnerId, this.callId);
1968
+ }
1969
+ catch {
1970
+ // Ignore ack failure
1971
+ }
1972
+ }
1973
+ this.setState("active");
1974
+ if (this.callType === sendVoiceCall.CallType.Video ||
1975
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
1976
+ Number(this.extendData?.callType) === 1) {
1977
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
1978
+ if (!this.isCameraOn && this.partnerId && this.callId > 0 && !this.groupId) {
1979
+ void this.api.call
1980
+ .updateCallMediaState(this.partnerId, this.callId, {
1981
+ isOnCam: 0,
1982
+ isOnMic: -1,
1983
+ data: "",
1984
+ })
1985
+ .catch(() => { });
1986
+ }
1987
+ void this.sendRtcpPli().catch(() => { });
1988
+ }
1989
+ const params = this.getConnectionParams();
1990
+ this.emit("connection_params", params);
1991
+ this.emit("active", params);
1992
+ }
1993
+ else if (event.type === CallEvent.CallEventType.CANCEL || event.type === CallEvent.CallEventType.END) {
1994
+ this.setState("ended");
1995
+ this.cleanupResources();
1996
+ this.emit("ended", event.act);
1997
+ }
1998
+ else if (event.act === "onoff_camera") {
1999
+ const camType = Number(rawParams.type ?? 0);
2000
+ const reason = Number(rawParams.reason ?? 0);
2001
+ if (camType === 1 || camType === 2) {
2002
+ this.clearIncomingVideoAssemblyBuffers();
2003
+ }
2004
+ // Khi đối phương gửi onoff_camera (đặc biệt reason=2/3 báo video loss hoặc bật lại camera), phản hồi ngay trạng thái camera và PLI
2005
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
2006
+ void this.sendRtcpPli().catch(() => { });
2007
+ if (reason === 2 || reason === 3) {
2008
+ this.emit("media_state_change", {
2009
+ isLocal: false,
2010
+ rawAct: event.act,
2011
+ reason,
2012
+ });
2013
+ }
2014
+ else {
2015
+ this.emit("media_state_change", {
2016
+ isLocal: false,
2017
+ isCameraOn: camType === 1 || camType === 2,
2018
+ rawAct: event.act,
2019
+ reason,
2020
+ });
2021
+ }
2022
+ }
2023
+ else if (event.act === "audio_to_video" ||
2024
+ event.act === "requestaudio2video" ||
2025
+ Number(event.data.subCmd) === 12056) {
2026
+ const reqStatus = Number(rawParams.status ?? event.data.status ?? 1);
2027
+ this.callType = this.groupId ? sendVoiceCall.CallType.GroupVideo : sendVoiceCall.CallType.Video;
2028
+ this.isCameraOn = true;
2029
+ this.clearIncomingVideoAssemblyBuffers();
2030
+ this.startMediaKeepaliveLoop();
2031
+ if (reqStatus === 1 && this.partnerId && this.callId > 0) {
2032
+ void this.api.call
2033
+ .requestAudioToVideoCall(this.partnerId, this.callId, { status: 2 })
2034
+ .catch(() => { });
2035
+ }
2036
+ void this.sendCameraState(true).catch(() => { });
2037
+ void this.sendRtcpPli().catch(() => { });
2038
+ this.emit("media_state_change", {
2039
+ isLocal: false,
2040
+ callType: this.callType,
2041
+ isCameraOn: true,
2042
+ rawAct: event.act,
2043
+ });
2044
+ }
2045
+ else if (event.act === "holdreq") {
2046
+ if (this.partnerId) {
2047
+ void this.api.call.holdVoiceCall(this.partnerId, { isResponse: true }).catch(() => { });
2048
+ }
2049
+ this.emit("media_state_change", {
2050
+ isLocal: false,
2051
+ isHold: true,
2052
+ rawAct: event.act,
2053
+ });
2054
+ }
2055
+ else if (event.act === "unholdreq") {
2056
+ if (this.partnerId) {
2057
+ void this.api.call
2058
+ .unholdVoiceCall(this.partnerId, { isResponse: true })
2059
+ .catch(() => { });
2060
+ }
2061
+ this.clearIncomingVideoAssemblyBuffers();
2062
+ void this.sendMicState(!this.isMicMuted).catch(() => { });
2063
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
2064
+ void this.sendRtcpPli().catch(() => { });
2065
+ this.emit("media_state_change", {
2066
+ isLocal: false,
2067
+ isHold: false,
2068
+ rawAct: event.act,
2069
+ });
2070
+ }
2071
+ else if (event.act === "requestsharescreen" ||
2072
+ event.act === "sharescreen" ||
2073
+ event.act === "group_sharescreen") {
2074
+ const shareStatus = Number(rawParams.status ?? event.data.status ?? 1);
2075
+ this.emit("media_state_change", {
2076
+ isLocal: false,
2077
+ isScreenSharing: shareStatus === 1,
2078
+ rawAct: event.act,
2079
+ });
2080
+ }
2081
+ else if (event.act === "conf" || event.act === "mute_audio") {
2082
+ this.emit("media_state_change", {
2083
+ isLocal: false,
2084
+ isCameraOn: rawParams.isOnCam !== undefined && Number(rawParams.isOnCam) !== -1
2085
+ ? Boolean(Number(rawParams.isOnCam))
2086
+ : undefined,
2087
+ isMicMuted: rawParams.isOnMic !== undefined && Number(rawParams.isOnMic) !== -1
2088
+ ? !Boolean(Number(rawParams.isOnMic))
2089
+ : undefined,
2090
+ rawAct: event.act,
2091
+ });
2092
+ }
2093
+ }
2094
+ setState(nextState) {
2095
+ if (this.state !== nextState) {
2096
+ if (nextState === "active" && !this.activeStartedAtMs) {
2097
+ this.activeStartedAtMs = Date.now();
2098
+ }
2099
+ if (nextState === "ringing" && this.options.ringingTimeoutMs > 0) {
2100
+ if (this.ringingTimeoutTimer)
2101
+ clearTimeout(this.ringingTimeoutTimer);
2102
+ this.ringingTimeoutTimer = setTimeout(() => {
2103
+ if (this.state === "ringing") {
2104
+ void this.endCall("ringing_timeout");
2105
+ }
2106
+ }, this.options.ringingTimeoutMs);
2107
+ if (typeof this.ringingTimeoutTimer.unref === "function") {
2108
+ this.ringingTimeoutTimer.unref();
2109
+ }
2110
+ }
2111
+ else if (nextState !== "ringing" && this.ringingTimeoutTimer) {
2112
+ clearTimeout(this.ringingTimeoutTimer);
2113
+ this.ringingTimeoutTimer = null;
2114
+ }
2115
+ this.state = nextState;
2116
+ this.emit("state", nextState);
2117
+ }
2118
+ }
2119
+ applyServerInfo(serverInfo) {
2120
+ this.serverInfo = serverInfo;
2121
+ const rtpServer = serverInfo?.rtpServer ?? serverInfo?.rtpIP ?? serverInfo?.servers?.[0]?.rtpaddr ?? "";
2122
+ const rtcpServer = serverInfo?.rtcpServer ??
2123
+ serverInfo?.rtcpIP ??
2124
+ serverInfo?.servers?.[0]?.rtcpaddr ??
2125
+ rtpServer;
2126
+ this.rtpEndpoint = parseEndpoint(rtpServer);
2127
+ this.rtcpEndpoint = parseEndpoint(rtcpServer || rtpServer);
2128
+ this.sessId = serverInfo?.sessId ?? `${this.partnerId}_${this.callId}_401`;
2129
+ this.zrtpKey = serverInfo?.zrtpKey;
2130
+ this.fromUid32 = Number(serverInfo?.fromId) || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
2131
+ this.toUid32 = Number(serverInfo?.toId) || ZRTPPacketCodec.uidToUint32(this.partnerId);
2132
+ if (serverInfo?.codec && serverInfo.codec !== "[]") {
2133
+ this.codec = serverInfo.codec;
2134
+ }
2135
+ }
2136
+ async ensureUdpSocket() {
2137
+ if (this.udpSocket)
2138
+ return this.udpSocket;
2139
+ const sock = dgram.createSocket("udp4");
2140
+ this.udpSocket = sock;
2141
+ sock.on("message", (msg) => {
2142
+ this.handleIncomingUdpPacket(msg);
2143
+ });
2144
+ sock.on("error", (err) => {
2145
+ this.emit("error", err);
2146
+ });
2147
+ await new Promise((resolve, reject) => {
2148
+ const onError = (err) => {
2149
+ sock.removeListener("listening", onListening);
2150
+ reject(err);
2151
+ };
2152
+ const onListening = () => {
2153
+ sock.removeListener("error", onError);
2154
+ this.localUdpPort = sock.address().port;
2155
+ resolve();
2156
+ };
2157
+ sock.once("error", onError);
2158
+ sock.once("listening", onListening);
2159
+ sock.bind(this.options.localUdpPort);
2160
+ });
2161
+ return sock;
2162
+ }
2163
+ async performUdpInitHandshake(isCaller) {
2164
+ const fromUid = this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
2165
+ const toUid = this.toUid32 || ZRTPPacketCodec.uidToUint32(this.partnerId);
2166
+ const initPacket = ZRTPPacketCodec.buildInitCallRequest({
2167
+ isCaller,
2168
+ seqNum: 0,
2169
+ headerCallId: this.callId,
2170
+ callId: this.callId,
2171
+ fromUid,
2172
+ toUid,
2173
+ flags: 1,
2174
+ subFlags: toUid,
2175
+ sessId: this.sessId,
2176
+ });
2177
+ const timeoutMs = this.options.udpHandshakeTimeoutMs;
2178
+ await new Promise((resolve) => {
2179
+ let settled = false;
2180
+ const retryInterval = setInterval(() => {
2181
+ if (!settled && this.udpSocket) {
2182
+ void this.sendRawUdp(initPacket, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2183
+ }
2184
+ }, 350);
2185
+ const timer = setTimeout(() => {
2186
+ if (settled)
2187
+ return;
2188
+ settled = true;
2189
+ clearInterval(retryInterval);
2190
+ this.removeListener("zrtp_control", onControl);
2191
+ if (!this.routingToken) {
2192
+ this.routingToken = this.callId >>> 0;
2193
+ }
2194
+ resolve();
2195
+ }, timeoutMs);
2196
+ const onControl = (pkt) => {
2197
+ if (pkt.isResponse &&
2198
+ (pkt.cmd === ZRTPPacketCodec.ZRTPControlCommand.InitCallCaller ||
2199
+ pkt.cmd === ZRTPPacketCodec.ZRTPControlCommand.InitCallCallee) &&
2200
+ pkt.errorCode === 0 &&
2201
+ pkt.routingToken) {
2202
+ if (settled)
2203
+ return;
2204
+ settled = true;
2205
+ clearTimeout(timer);
2206
+ clearInterval(retryInterval);
2207
+ this.removeListener("zrtp_control", onControl);
2208
+ this.routingToken = pkt.routingToken >>> 0;
2209
+ this.udpHandshakeVerified = true;
2210
+ resolve();
2211
+ }
2212
+ };
2213
+ this.on("zrtp_control", onControl);
2214
+ void this.sendRawUdp(initPacket, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2215
+ });
2216
+ }
2217
+ startUdpPingLoop() {
2218
+ if (this.pingTimer)
2219
+ clearInterval(this.pingTimer);
2220
+ this.startedAtMs = Date.now();
2221
+ const sendPing = () => {
2222
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
2223
+ return;
2224
+ const elapsed = Math.max(1, Date.now() - this.startedAtMs);
2225
+ const fromUid = this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid);
2226
+ const toUid = this.toUid32 || ZRTPPacketCodec.uidToUint32(this.partnerId);
2227
+ const token = (this.routingToken || this.callId) >>> 0;
2228
+ // 1. Gói RelayPing (cmd=2) tới máy chủ Relay kèm sessId
2229
+ if (this.sessId) {
2230
+ const relayPing = ZRTPPacketCodec.buildRelayPingRequest({
2231
+ seqNum: 0,
2232
+ callId: 0,
2233
+ fromUid,
2234
+ routingToken: token,
2235
+ sessId: this.sessId,
2236
+ });
2237
+ void this.sendRawUdp(relayPing, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2238
+ }
2239
+ // 2. Gói Ping (cmd=5) giữ NAT
2240
+ const pkt = ZRTPPacketCodec.buildPingRequest({
2241
+ seqNum: this.controlSeq++,
2242
+ callId: this.callId,
2243
+ fromUid,
2244
+ toUid,
2245
+ elapsedMs: elapsed,
2246
+ });
2247
+ void this.sendRawUdp(pkt, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2248
+ // 3. Gói RequestForward Ping (cmd=32, subCmd=11) qua Relay tới máy đối phương
2249
+ const fwdPing = ZRTPPacketCodec.buildRequestForwardPacket({
2250
+ seqNum: 0,
2251
+ seqId: this.forwardPingSeq,
2252
+ fromUid,
2253
+ routingToken: token,
2254
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.Ping,
2255
+ payload: String(elapsed),
2256
+ });
2257
+ void this.sendRawUdp(fwdPing, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2258
+ // 4. Gói RequestForward ManualControl / VideoControl (cmd=32, subCmd=10, 20-byte payload)
2259
+ // Cập nhật partnerRenderFps (15fps), partnerAudioRecvPktPerSec (50), partnerVideoRecvPktPerSec (30)
2260
+ // khớp với CallController::_sendPacketManualControl (0x100184664) & isPartnerRenderFpsLow (0x1001895dc)
2261
+ const isVideo = this.callType === sendVoiceCall.CallType.Video ||
2262
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
2263
+ Number(this.extendData?.callType) === 1;
2264
+ const manualCtrlPayload = node_buffer.Buffer.alloc(20);
2265
+ manualCtrlPayload.writeInt32LE(isVideo ? 15 : 0, 0); // renderFps >= 3
2266
+ manualCtrlPayload.writeInt32LE(0, 4); // isReadyForHevcDec
2267
+ manualCtrlPayload.writeInt32LE(0, 8); // deviceOrientation
2268
+ manualCtrlPayload.writeInt32LE(50, 12); // audioRecvPktCountPerSec > 6
2269
+ manualCtrlPayload.writeInt32LE(isVideo ? 30 : 0, 16); // videoRecvPktCountPerSec
2270
+ const manualCtrlPkt = ZRTPPacketCodec.buildRequestForwardPacket({
2271
+ seqNum: 0,
2272
+ seqId: this.manualControlSeq++,
2273
+ fromUid,
2274
+ routingToken: token,
2275
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.VideoControl,
2276
+ payload: manualCtrlPayload,
2277
+ });
2278
+ void this.sendRawUdp(manualCtrlPkt, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2279
+ };
2280
+ sendPing();
2281
+ this.pingTimer = setInterval(sendPing, this.options.pingIntervalMs);
2282
+ if (typeof this.pingTimer.unref === "function") {
2283
+ this.pingTimer.unref();
2284
+ }
2285
+ }
2286
+ startGroupPingLoop() {
2287
+ if (this.groupPingTimer) {
2288
+ clearInterval(this.groupPingTimer);
2289
+ this.groupPingTimer = null;
2290
+ }
2291
+ if (!this.groupId || !this.callId)
2292
+ return;
2293
+ this.groupPingTimer = setInterval(() => {
2294
+ if (this.state === "ended" || !this.groupId || !this.callId)
2295
+ return;
2296
+ void this.api.call
2297
+ .pingGroupCall(this.callId, {
2298
+ hostCall: this.groupId,
2299
+ callState: 1,
2300
+ audioState: this.isMicMuted ? 0 : 1,
2301
+ videoState: this.isCameraOn ? 1 : 0,
2302
+ })
2303
+ .catch(() => { });
2304
+ }, 10000);
2305
+ if (typeof this.groupPingTimer.unref === "function") {
2306
+ this.groupPingTimer.unref();
2307
+ }
2308
+ }
2309
+ startMediaKeepaliveLoop() {
2310
+ this.stopMediaKeepaliveLoop();
2311
+ if (!this.options.autoKeepaliveMedia)
2312
+ return;
2313
+ // 1. Luồng Audio RTP 20ms (50 gói/giây) thoả mãn audio_recheck_sock (checkPkgRec >= 14 / 2s)
2314
+ this.audioKeepaliveTimer = setInterval(() => {
2315
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
2316
+ return;
2317
+ if (Date.now() - this.lastExternalAudioSentAtMs >= 18) {
2318
+ void this.sendAudioFrame(OPUS_SILENCE_20MS_FRAME, { isKeepalive: true }).catch(() => { });
2319
+ }
2320
+ }, 20);
2321
+ if (typeof this.audioKeepaliveTimer.unref === "function") {
2322
+ this.audioKeepaliveTimer.unref();
2323
+ }
2324
+ // 2. Nếu là cuộc gọi Video: chỉ gửi H264_KEEPALIVE_ANNEXB_FRAME khi KHÔNG có camera ngoài gửi frame trong >= 1000ms!
2325
+ // Tuyệt đối không chèn khung 16x16 vào giữa luồng camera 480x640 đang chạy (tránh làm hỏng SPS/PPS trên VideoToolbox iOS).
2326
+ const isVideo = this.callType === sendVoiceCall.CallType.Video ||
2327
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
2328
+ Number(this.extendData?.callType) === 1;
2329
+ if (isVideo) {
2330
+ this.videoKeepaliveTimer = setInterval(() => {
2331
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
2332
+ return;
2333
+ if (!this.isCameraOn)
2334
+ return;
2335
+ if (Date.now() - this.lastExternalVideoSentAtMs >= 1000) {
2336
+ void this.sendH264AnnexBFrame(H264_KEEPALIVE_ANNEXB_FRAME, {
2337
+ timestampStep: 6000,
2338
+ isKeepalive: true,
2339
+ }).catch(() => { });
2340
+ }
2341
+ }, 66);
2342
+ if (typeof this.videoKeepaliveTimer.unref === "function") {
2343
+ this.videoKeepaliveTimer.unref();
2344
+ }
2345
+ }
2346
+ // 3. Báo cáo RTCP Sender Report + Receiver Report + SDES mỗi 500ms (thoả mãn durationCheckNoReportSec: 4)
2347
+ const sendPeriodicRtcp = () => {
2348
+ if (!this.udpSocket || this.rtpEndpoint.port <= 0 || this.state === "ended")
2349
+ return;
2350
+ const audioRtcp = this.buildCompoundRtcpReport(false);
2351
+ void this.sendRtcp(audioRtcp, false).catch(() => { });
2352
+ const activeVideo = this.callType === sendVoiceCall.CallType.Video ||
2353
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
2354
+ Number(this.extendData?.callType) === 1;
2355
+ if (activeVideo) {
2356
+ const videoRtcp = this.buildCompoundRtcpReport(true);
2357
+ void this.sendRtcp(videoRtcp, true).catch(() => { });
2358
+ if (this.remoteVideoSsrc !== 0 && !this.remoteSpsNal) {
2359
+ void this.sendRtcpPli().catch(() => { });
2360
+ }
2361
+ }
2362
+ };
2363
+ sendPeriodicRtcp();
2364
+ this.rtcpTimer = setInterval(sendPeriodicRtcp, 500);
2365
+ if (typeof this.rtcpTimer.unref === "function") {
2366
+ this.rtcpTimer.unref();
2367
+ }
2368
+ }
2369
+ stopMediaKeepaliveLoop() {
2370
+ if (this.audioKeepaliveTimer) {
2371
+ clearInterval(this.audioKeepaliveTimer);
2372
+ this.audioKeepaliveTimer = null;
2373
+ }
2374
+ if (this.videoKeepaliveTimer) {
2375
+ clearInterval(this.videoKeepaliveTimer);
2376
+ this.videoKeepaliveTimer = null;
2377
+ }
2378
+ if (this.rtcpTimer) {
2379
+ clearInterval(this.rtcpTimer);
2380
+ this.rtcpTimer = null;
2381
+ }
2382
+ }
2383
+ buildCompoundRtcpReport(isVideo) {
2384
+ const nowMs = Date.now();
2385
+ const ntpSec = (Math.floor(nowMs / 1000) + 2208988800) >>> 0;
2386
+ const ntpFrac = Math.floor(((nowMs % 1000) / 1000) * 0x100000000) >>> 0;
2387
+ const localSsrc = isVideo ? this.getLocalVideoSsrc() : this.getLocalAudioSsrc();
2388
+ const rtpTs = isVideo ? this.videoTimestamp : this.audioTimestamp;
2389
+ const pktCount = isVideo ? this.videoPacketCount : this.audioPacketCount;
2390
+ const octCount = isVideo ? this.videoOctetCount : this.audioOctetCount;
2391
+ const remoteSsrc = isVideo ? this.remoteVideoSsrc : this.remoteAudioSsrc;
2392
+ const remoteMaxSeq = isVideo ? this.remoteVideoMaxSeq : this.remoteAudioMaxSeq;
2393
+ const remoteCycles = isVideo ? this.remoteVideoSeqCycles : this.remoteAudioSeqCycles;
2394
+ const lastSrMiddleNtp = isVideo
2395
+ ? this.lastRemoteVideoSrMiddleNtp
2396
+ : this.lastRemoteAudioSrMiddleNtp;
2397
+ const lastSrAtMs = isVideo
2398
+ ? this.lastRemoteVideoSrReceivedAtMs
2399
+ : this.lastRemoteAudioSrReceivedAtMs;
2400
+ const hasReportBlock = remoteSsrc !== 0;
2401
+ const srLenBytes = hasReportBlock ? 52 : 28;
2402
+ const sdesLenBytes = 16;
2403
+ const rembLenBytes = isVideo && hasReportBlock ? 24 : 0;
2404
+ const buf = node_buffer.Buffer.alloc(srLenBytes + sdesLenBytes + rembLenBytes);
2405
+ // RTCP Sender Report (PT = 200)
2406
+ buf.writeUInt8(hasReportBlock ? 0x81 : 0x80, 0);
2407
+ buf.writeUInt8(200, 1);
2408
+ buf.writeUInt16BE(srLenBytes / 4 - 1, 2);
2409
+ buf.writeUInt32BE(localSsrc >>> 0, 4);
2410
+ buf.writeUInt32BE(ntpSec, 8);
2411
+ buf.writeUInt32BE(ntpFrac, 12);
2412
+ buf.writeUInt32BE(rtpTs >>> 0, 16);
2413
+ buf.writeUInt32BE(pktCount >>> 0, 20);
2414
+ buf.writeUInt32BE(octCount >>> 0, 24);
2415
+ if (hasReportBlock) {
2416
+ const extMaxSeq = (((remoteCycles & 0xffff) << 16) | (remoteMaxSeq & 0xffff)) >>> 0;
2417
+ const nowMiddleNtp = (((ntpSec & 0xffff) << 16) | ((ntpFrac >>> 16) & 0xffff)) >>> 0;
2418
+ const lsr = lastSrMiddleNtp !== 0
2419
+ ? lastSrMiddleNtp >>> 0
2420
+ : ((nowMiddleNtp - 1638) >>> 0); // Fallback 25ms ago
2421
+ const dlsr = lastSrAtMs > 0 && lastSrMiddleNtp !== 0
2422
+ ? (Math.max(65, Math.floor(((nowMs - lastSrAtMs) / 1000) * 65536)) >>> 0)
2423
+ : 1638; // 25ms in 1/65536s units so RTT is ~25ms (1 <= rtt < 9999)
2424
+ buf.writeUInt32BE(remoteSsrc >>> 0, 28);
2425
+ buf.writeUInt32BE(0, 32); // fractionLost = 0, cumulativeLost = 0
2426
+ buf.writeUInt32BE(extMaxSeq, 36);
2427
+ buf.writeUInt32BE(10, 40); // interarrival jitter
2428
+ buf.writeUInt32BE(lsr, 44);
2429
+ buf.writeUInt32BE(dlsr, 48);
2430
+ }
2431
+ // RTCP SDES (PT = 202)
2432
+ const sdesOffset = srLenBytes;
2433
+ buf.writeUInt8(0x81, sdesOffset);
2434
+ buf.writeUInt8(202, sdesOffset + 1);
2435
+ buf.writeUInt16BE(3, sdesOffset + 2);
2436
+ buf.writeUInt32BE(localSsrc >>> 0, sdesOffset + 4);
2437
+ buf.writeUInt8(1, sdesOffset + 8); // CNAME
2438
+ buf.writeUInt8(5, sdesOffset + 9);
2439
+ buf.write("zcajs", sdesOffset + 10, "ascii");
2440
+ buf.writeUInt8(0, sdesOffset + 15);
2441
+ // RTCP REMB (PT = 206, FMT = 15, 'R' 'E' 'M' 'B', ~1.46 Mbps) cho Video BWE
2442
+ if (rembLenBytes > 0) {
2443
+ const rembOffset = srLenBytes + sdesLenBytes;
2444
+ buf.writeUInt8(0x8f, rembOffset); // V=2, P=0, FMT=15
2445
+ buf.writeUInt8(206, rembOffset + 1); // PT=206 (PSFB)
2446
+ buf.writeUInt16BE(5, rembOffset + 2); // 5 words (24 bytes)
2447
+ buf.writeUInt32BE(localSsrc >>> 0, rembOffset + 4);
2448
+ buf.writeUInt32BE(0, rembOffset + 8);
2449
+ buf.write("REMB", rembOffset + 12, "ascii");
2450
+ buf.writeUInt8(1, rembOffset + 16); // numSsrcs = 1
2451
+ // brExp = 3, brMantissa = 183105 (0x2cb41) => 183105 << 3 = 1,464,840 bps
2452
+ buf.writeUInt8(0x0e, rembOffset + 17);
2453
+ buf.writeUInt8(0xcb, rembOffset + 18);
2454
+ buf.writeUInt8(0x41, rembOffset + 19);
2455
+ buf.writeUInt32BE(remoteSsrc >>> 0, rembOffset + 20);
2456
+ }
2457
+ return buf;
2458
+ }
2459
+ markCallActiveFromPartnerTraffic() {
2460
+ this.hasReceivedPartnerMedia = true;
2461
+ if (this.state === "ringing" || this.state === "handshaking_udp") {
2462
+ this.setState("active");
2463
+ if (this.isCaller &&
2464
+ this.partnerId &&
2465
+ this.callId > 0 &&
2466
+ !this.groupId &&
2467
+ !this.hasSentAnswerAck) {
2468
+ this.hasSentAnswerAck = true;
2469
+ void this.api.call.answerAckVoiceCall(this.partnerId, this.callId).catch(() => { });
2470
+ }
2471
+ if (this.callType === sendVoiceCall.CallType.Video ||
2472
+ this.callType === sendVoiceCall.CallType.GroupVideo ||
2473
+ Number(this.extendData?.callType) === 1) {
2474
+ void this.sendCameraState(this.isCameraOn).catch(() => { });
2475
+ if (!this.isCameraOn && this.partnerId && this.callId > 0 && !this.groupId) {
2476
+ void this.api.call
2477
+ .updateCallMediaState(this.partnerId, this.callId, {
2478
+ isOnCam: 0,
2479
+ isOnMic: -1,
2480
+ data: "",
2481
+ })
2482
+ .catch(() => { });
2483
+ }
2484
+ void this.sendRtcpPli().catch(() => { });
2485
+ }
2486
+ const params = this.getConnectionParams();
2487
+ this.emit("connection_params", params);
2488
+ this.emit("active", params);
2489
+ }
2490
+ }
2491
+ handleIncomingUdpPacket(msg) {
2492
+ const parsed = ZRTPPacketCodec.parseZRTPPacket(msg);
2493
+ this.emit("zrtp_packet", parsed);
2494
+ if (parsed.family === "control") {
2495
+ if (parsed.isResponse &&
2496
+ (parsed.cmd === ZRTPPacketCodec.ZRTPControlCommand.InitCallCaller ||
2497
+ parsed.cmd === ZRTPPacketCodec.ZRTPControlCommand.InitCallCallee) &&
2498
+ parsed.routingToken) {
2499
+ this.routingToken = parsed.routingToken >>> 0;
2500
+ }
2501
+ else if (parsed.cmd === ZRTPPacketCodec.ZRTPControlCommand.EndCall &&
2502
+ (this.state === "ringing" || this.state === "active")) {
2503
+ this.emit("zrtp_control", parsed);
2504
+ void this.endCall("remote_end");
2505
+ return;
2506
+ }
2507
+ else if (!parsed.isResponse &&
2508
+ parsed.cmd === ZRTPPacketCodec.ZRTPControlCommand.Ping &&
2509
+ this.udpSocket &&
2510
+ this.rtpEndpoint.port > 0) {
2511
+ const resp = ZRTPPacketCodec.buildPingResponse({
2512
+ seqNum: parsed.seqNum,
2513
+ callId: parsed.headerCallId || this.callId,
2514
+ fromUid: parsed.toUid,
2515
+ toUid: parsed.fromUid,
2516
+ elapsedMs: parsed.pingString ?? "0",
2517
+ });
2518
+ void this.sendRawUdp(resp, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2519
+ }
2520
+ else if (!parsed.isResponse &&
2521
+ parsed.cmd === ZRTPPacketCodec.ZRTPControlCommand.RequestForward &&
2522
+ this.udpSocket &&
2523
+ this.rtpEndpoint.port > 0) {
2524
+ this.markCallActiveFromPartnerTraffic();
2525
+ if (parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.Ping) {
2526
+ const pongPkt = ZRTPPacketCodec.buildRequestForwardPacket({
2527
+ seqNum: 0,
2528
+ seqId: parsed.headerCallId,
2529
+ fromUid: this.fromUid32 || ZRTPPacketCodec.uidToUint32(this.api.getContext().uid),
2530
+ routingToken: (this.routingToken || this.callId) >>> 0,
2531
+ subCmd: ZRTPPacketCodec.ZRTPForwardSubCommand.Pong,
2532
+ payload: parsed.forwardPayload ?? node_buffer.Buffer.alloc(0),
2533
+ });
2534
+ void this.sendRawUdp(pongPkt, this.rtpEndpoint.host, this.rtpEndpoint.port).catch(() => { });
2535
+ }
2536
+ else if (parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.Pong) {
2537
+ if (parsed.headerCallId === this.forwardPingSeq) {
2538
+ this.forwardPingSeq = (this.forwardPingSeq + 1) >>> 0;
2539
+ }
2540
+ }
2541
+ else if (parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OnCamera ||
2542
+ parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OffCamera ||
2543
+ parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OffCameraCallKit ||
2544
+ parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.PartnerSuspendVideo) {
2545
+ const isCamOn = parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OnCamera;
2546
+ this.clearIncomingVideoAssemblyBuffers();
2547
+ if (isCamOn) {
2548
+ if (this.callType === sendVoiceCall.CallType.Audio) {
2549
+ this.callType = sendVoiceCall.CallType.Video;
2550
+ this.startMediaKeepaliveLoop();
2551
+ }
2552
+ else if (this.callType === sendVoiceCall.CallType.GroupAudio) {
2553
+ this.callType = sendVoiceCall.CallType.GroupVideo;
2554
+ this.startMediaKeepaliveLoop();
2555
+ }
2556
+ void this.sendRtcpPli().catch(() => { });
2557
+ const t = setTimeout(() => {
2558
+ if (this.state === "active") {
2559
+ void this.sendRtcpPli().catch(() => { });
2560
+ }
2561
+ }, 250);
2562
+ if (typeof t.unref === "function")
2563
+ t.unref();
2564
+ }
2565
+ this.emit("media_state_change", {
2566
+ isLocal: false,
2567
+ isCameraOn: isCamOn,
2568
+ callType: this.callType,
2569
+ rawAct: isCamOn
2570
+ ? "udp_on_camera"
2571
+ : parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OffCameraCallKit
2572
+ ? "udp_off_camera_callkit"
2573
+ : parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.PartnerSuspendVideo
2574
+ ? "udp_partner_suspend_video"
2575
+ : "udp_off_camera",
2576
+ });
2577
+ }
2578
+ else if (parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OnMic ||
2579
+ parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OffMic) {
2580
+ this.emit("media_state_change", {
2581
+ isLocal: false,
2582
+ isMicMuted: parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OffMic,
2583
+ rawAct: parsed.flags === ZRTPPacketCodec.ZRTPForwardSubCommand.OnMic
2584
+ ? "udp_on_mic"
2585
+ : "udp_off_mic",
2586
+ });
2587
+ }
2588
+ }
2589
+ this.emit("zrtp_control", parsed);
2590
+ return;
2591
+ }
2592
+ if (parsed.family === "legacy_media" || parsed.family === "zrtc_media") {
2593
+ this.markCallActiveFromPartnerTraffic();
2594
+ try {
2595
+ if (parsed.isRtcp) {
2596
+ let rawRtcp = parsed.rtpPayload;
2597
+ if (this.srtpSession && Number(this.extendData?.srtcp) === 1) {
2598
+ try {
2599
+ rawRtcp = this.srtpSession.unprotectRtcp(parsed.rtpPayload);
2600
+ }
2601
+ catch {
2602
+ rawRtcp = parsed.rtpPayload;
2603
+ }
2604
+ }
2605
+ // Quét toàn bộ các sub-packet trong gói Compound RTCP để tìm Sender Report (PT=200)
2606
+ let rtcpOffset = 0;
2607
+ while (rtcpOffset + 8 <= rawRtcp.length) {
2608
+ const pt = rawRtcp.readUInt8(rtcpOffset + 1);
2609
+ const lenWords = rawRtcp.readUInt16BE(rtcpOffset + 2);
2610
+ const pktBytes = (lenWords + 1) * 4;
2611
+ if (pt === 200 && rtcpOffset + 28 <= rawRtcp.length) {
2612
+ const srSsrc = rawRtcp.readUInt32BE(rtcpOffset + 4);
2613
+ const middleNtp = rawRtcp.readUInt32BE(rtcpOffset + 10);
2614
+ if (parsed.isVideo) {
2615
+ if (!this.remoteVideoSsrc)
2616
+ this.remoteVideoSsrc = srSsrc;
2617
+ this.lastRemoteVideoSrMiddleNtp = middleNtp;
2618
+ this.lastRemoteVideoSrReceivedAtMs = Date.now();
2619
+ }
2620
+ else {
2621
+ if (!this.remoteAudioSsrc)
2622
+ this.remoteAudioSsrc = srSsrc;
2623
+ this.lastRemoteAudioSrMiddleNtp = middleNtp;
2624
+ this.lastRemoteAudioSrReceivedAtMs = Date.now();
2625
+ }
2626
+ }
2627
+ if (pktBytes <= 0)
2628
+ break;
2629
+ rtcpOffset += pktBytes;
2630
+ }
2631
+ this.emit("rtcp_packet", {
2632
+ isVideo: parsed.isVideo,
2633
+ rtcpPacket: rawRtcp,
2634
+ zrtpPacket: parsed,
2635
+ });
2636
+ }
2637
+ else {
2638
+ let rawRtp = parsed.rtpPayload;
2639
+ if (this.srtpSession) {
2640
+ try {
2641
+ rawRtp = this.srtpSession.unprotectRtp(parsed.rtpPayload);
2642
+ }
2643
+ catch {
2644
+ rawRtp = parsed.rtpPayload;
2645
+ }
2646
+ }
2647
+ const parsedRtp = ZaloSrtpSession.parseRtpPacket(rawRtp);
2648
+ if (parsed.isVideo) {
2649
+ if (isZaloVideoPayloadType(parsedRtp.payloadType)) {
2650
+ this.remoteVideoSsrc = parsedRtp.ssrc;
2651
+ if (parsedRtp.sequenceNumber < this.remoteVideoMaxSeq && this.remoteVideoMaxSeq - parsedRtp.sequenceNumber > 30000) {
2652
+ this.remoteVideoSeqCycles++;
2653
+ }
2654
+ this.remoteVideoMaxSeq = parsedRtp.sequenceNumber;
2655
+ }
2656
+ }
2657
+ else {
2658
+ this.remoteAudioSsrc = parsedRtp.ssrc;
2659
+ if (parsedRtp.sequenceNumber < this.remoteAudioMaxSeq && this.remoteAudioMaxSeq - parsedRtp.sequenceNumber > 30000) {
2660
+ this.remoteAudioSeqCycles++;
2661
+ }
2662
+ this.remoteAudioMaxSeq = parsedRtp.sequenceNumber;
2663
+ }
2664
+ this.emit("rtp_packet", {
2665
+ isVideo: parsed.isVideo,
2666
+ isFec: parsed.isFec,
2667
+ rtpPacket: rawRtp,
2668
+ parsedRtp,
2669
+ zrtpPacket: parsed,
2670
+ });
2671
+ if (!parsed.isVideo && !parsed.isFec) {
2672
+ this.emit("audio_frame", {
2673
+ payload: parsedRtp.payload,
2674
+ sequenceNumber: parsedRtp.sequenceNumber,
2675
+ timestamp: parsedRtp.timestamp,
2676
+ ssrc: parsedRtp.ssrc,
2677
+ payloadType: parsedRtp.payloadType,
2678
+ marker: parsedRtp.marker,
2679
+ });
2680
+ }
2681
+ else if (parsed.isVideo &&
2682
+ !parsed.isFec &&
2683
+ isZaloVideoPayloadType(parsedRtp.payloadType)) {
2684
+ this.emit("video_frame", {
2685
+ payload: parsedRtp.payload,
2686
+ sequenceNumber: parsedRtp.sequenceNumber,
2687
+ timestamp: parsedRtp.timestamp,
2688
+ ssrc: parsedRtp.ssrc,
2689
+ payloadType: parsedRtp.payloadType,
2690
+ marker: parsedRtp.marker,
2691
+ });
2692
+ this.handleIncomingVideoRtpPayload(parsedRtp);
2693
+ }
2694
+ }
2695
+ }
2696
+ catch (err) {
2697
+ if (this.listenerCount("error") > 0) {
2698
+ this.emit("error", err);
2699
+ }
2700
+ }
2701
+ }
2702
+ }
2703
+ handleIncomingVideoRtpPayload(parsedRtp) {
2704
+ const payload = parsedRtp.payload;
2705
+ // Bỏ qua ngay các gói RTP padding rỗng (P=1) để không làm flush nhầm frame đang ghép
2706
+ if (!payload || payload.length < 1)
2707
+ return;
2708
+ if (this.currentAccessUnitTs !== -1 && parsedRtp.timestamp !== this.currentAccessUnitTs) {
2709
+ this.flushIncomingAccessUnit(this.currentAccessUnitTs, parsedRtp.ssrc);
2710
+ }
2711
+ this.currentAccessUnitTs = parsedRtp.timestamp;
2712
+ if (isZaloKeyframePayloadType(parsedRtp.payloadType)) {
2713
+ this.currentAccessUnitHasIdr = true;
2714
+ }
2715
+ // Trường hợp 1: Payload đã bắt đầu trực tiếp bằng Annex-B start code (00 00 00 01 hoặc 00 00 01)
2716
+ if (startsWithAnnexBStartCode(payload)) {
2717
+ for (const nal of splitAnnexBNals(payload)) {
2718
+ this.recordIncomingNal(nal);
2719
+ }
2720
+ if (parsedRtp.marker) {
2721
+ this.flushIncomingAccessUnit(parsedRtp.timestamp, parsedRtp.ssrc);
2722
+ }
2723
+ return;
2724
+ }
2725
+ const nalHeader = payload[0];
2726
+ const nalType = nalHeader & 0x1f;
2727
+ if (nalType === 0) {
2728
+ // Khi g_nalHeaderSize == 0 trên iOS, NextAggregatePacket (0x1003db3a4) ghi trực tiếp
2729
+ // [uint16BE len1][00 00 00 01 SPS][uint16BE len2][00 00 00 01 PPS] tại offset 0
2730
+ const agg = tryParseAggregatedNals(payload, 0);
2731
+ if (agg) {
2732
+ for (const chunk of agg) {
2733
+ for (const nal of splitAnnexBNals(chunk)) {
2734
+ this.recordIncomingNal(nal);
2735
+ }
2736
+ }
2737
+ }
2738
+ else {
2739
+ for (const nal of splitAnnexBNals(payload)) {
2740
+ this.recordIncomingNal(nal);
2741
+ }
2742
+ }
2743
+ }
2744
+ else if (nalType >= 1 && nalType <= 23) {
2745
+ for (const nal of splitAnnexBNals(payload)) {
2746
+ this.recordIncomingNal(nal);
2747
+ }
2748
+ }
2749
+ else if (nalType === 24) {
2750
+ // RFC 6184 STAP-A (Single-Time Aggregation Packet)
2751
+ const agg = tryParseAggregatedNals(payload, 1);
2752
+ if (agg) {
2753
+ for (const chunk of agg) {
2754
+ for (const nal of splitAnnexBNals(chunk)) {
2755
+ this.recordIncomingNal(nal);
2756
+ }
2757
+ }
2758
+ }
2759
+ else {
2760
+ let offset = 1;
2761
+ while (offset + 2 <= payload.length) {
2762
+ const nalSize = payload.readUInt16BE(offset);
2763
+ offset += 2;
2764
+ if (nalSize > 0 && offset + nalSize <= payload.length) {
2765
+ for (const nal of splitAnnexBNals(payload.subarray(offset, offset + nalSize))) {
2766
+ this.recordIncomingNal(nal);
2767
+ }
2768
+ }
2769
+ offset += nalSize;
2770
+ }
2771
+ }
2772
+ }
2773
+ else if ((nalType === 28 || nalType === 29) && payload.length >= 2) {
2774
+ // RFC 6184 FU-A (28) / FU-B (29) + Zalo ZRTP FU-A (origNalType = 0 với Annex-B bên trong)
2775
+ const fuIndicator = payload[0];
2776
+ const fuHeader = payload[1];
2777
+ const isStart = (fuHeader & 0x80) !== 0;
2778
+ const isEnd = (fuHeader & 0x40) !== 0;
2779
+ const origNalType = fuHeader & 0x1f;
2780
+ const reconstructedNalHeader = (fuIndicator & 0xe0) | origNalType;
2781
+ const headerSize = nalType === 29 ? 4 : 2;
2782
+ if (payload.length > headerSize) {
2783
+ const body = payload.subarray(headerSize);
2784
+ if (isStart) {
2785
+ // Nếu origNalType === 0 hoặc body đã bắt đầu bằng Annex-B start code (00 00 00 01 / 00 00 01),
2786
+ // TUYỆT ĐỐI KHÔNG chèn thêm byte 0x00 vào đầu để không biến 00 00 00 01 thành 5 byte!
2787
+ if (origNalType === 0 || startsWithAnnexBStartCode(body)) {
2788
+ this.fuaFragments = [body];
2789
+ }
2790
+ else {
2791
+ this.fuaFragments = [node_buffer.Buffer.from([reconstructedNalHeader]), body];
2792
+ }
2793
+ }
2794
+ else if (this.fuaFragments.length > 0) {
2795
+ this.fuaFragments.push(body);
2796
+ }
2797
+ if (isEnd && this.fuaFragments.length > 0) {
2798
+ const fullNal = node_buffer.Buffer.concat(this.fuaFragments);
2799
+ this.fuaFragments = [];
2800
+ for (const nal of splitAnnexBNals(fullNal)) {
2801
+ this.recordIncomingNal(nal);
2802
+ }
2803
+ }
2804
+ }
2805
+ }
2806
+ if (parsedRtp.marker) {
2807
+ this.flushIncomingAccessUnit(parsedRtp.timestamp, parsedRtp.ssrc);
2808
+ }
2809
+ }
2810
+ recordIncomingNal(rawNal) {
2811
+ if (!rawNal || rawNal.length === 0)
2812
+ return;
2813
+ const nal = stripLeadingAnnexBStartCode(rawNal);
2814
+ if (!nal || nal.length === 0)
2815
+ return;
2816
+ const nalType = nal[0] & 0x1f;
2817
+ // Bỏ qua NAL rỗng (type=0) hoặc Access Unit Delimiter (AUD, type=9)
2818
+ if (nalType === 0 || nalType === 9)
2819
+ return;
2820
+ if (nalType === 7) {
2821
+ this.remoteSpsNal = node_buffer.Buffer.from(nal);
2822
+ }
2823
+ else if (nalType === 8) {
2824
+ this.remotePpsNal = node_buffer.Buffer.from(nal);
2825
+ }
2826
+ else if (nalType === 5) {
2827
+ this.currentAccessUnitHasIdr = true;
2828
+ }
2829
+ this.currentAccessUnitNals.push(node_buffer.Buffer.from(nal));
2830
+ }
2831
+ flushIncomingAccessUnit(timestamp, ssrc) {
2832
+ if (this.currentAccessUnitNals.length === 0)
2833
+ return;
2834
+ const startCode = node_buffer.Buffer.from([0x00, 0x00, 0x00, 0x01]);
2835
+ const chunks = [];
2836
+ const nalTypes = this.currentAccessUnitNals.map((n) => n[0] & 0x1f);
2837
+ const hasInlineSps = nalTypes.includes(7);
2838
+ const hasInlinePps = nalTypes.includes(8);
2839
+ const hasVclSlice = nalTypes.some((t) => t === 1 || t === 5);
2840
+ // Nếu chỉ mới nhận SPS/PPS mà chưa có slice hình ảnh (IDR/P-slice), giữ lại cho gói tiếp theo
2841
+ if (!hasVclSlice) {
2842
+ return;
2843
+ }
2844
+ if (this.currentAccessUnitHasIdr) {
2845
+ if (!hasInlineSps && this.remoteSpsNal) {
2846
+ chunks.push(startCode, this.remoteSpsNal);
2847
+ }
2848
+ if (!hasInlinePps && this.remotePpsNal) {
2849
+ chunks.push(startCode, this.remotePpsNal);
2850
+ }
2851
+ }
2852
+ for (const nal of this.currentAccessUnitNals) {
2853
+ chunks.push(startCode, nal);
2854
+ }
2855
+ const annexB = node_buffer.Buffer.concat(chunks);
2856
+ const isKeyframe = this.currentAccessUnitHasIdr;
2857
+ this.currentAccessUnitNals = [];
2858
+ this.currentAccessUnitHasIdr = false;
2859
+ this.currentAccessUnitTs = -1;
2860
+ this.emit("video_annexb_frame", {
2861
+ annexB,
2862
+ isKeyframe,
2863
+ timestamp,
2864
+ ssrc,
2865
+ sps: this.remoteSpsNal ?? undefined,
2866
+ pps: this.remotePpsNal ?? undefined,
2867
+ nalTypes,
2868
+ });
2869
+ }
2870
+ attachCallEventListener() {
2871
+ if (this.boundCallEventListener)
2872
+ return;
2873
+ this.boundCallEventListener = (event) => {
2874
+ void this.handleCallEvent(event);
2875
+ };
2876
+ try {
2877
+ this.api.listener.on("call_event", this.boundCallEventListener);
2878
+ }
2879
+ catch {
2880
+ // Listener might not be initialized if cookie/userAgent is missing in unit test mocks
2881
+ }
2882
+ }
2883
+ detachCallEventListener() {
2884
+ if (!this.boundCallEventListener)
2885
+ return;
2886
+ try {
2887
+ this.api.listener.removeListener("call_event", this.boundCallEventListener);
2888
+ }
2889
+ catch {
2890
+ // Ignore
2891
+ }
2892
+ this.boundCallEventListener = undefined;
2893
+ }
2894
+ cleanupResources() {
2895
+ if (this.ringingTimeoutTimer) {
2896
+ clearTimeout(this.ringingTimeoutTimer);
2897
+ this.ringingTimeoutTimer = null;
2898
+ }
2899
+ if (this.pingTimer) {
2900
+ clearInterval(this.pingTimer);
2901
+ this.pingTimer = null;
2902
+ }
2903
+ if (this.groupPingTimer) {
2904
+ clearInterval(this.groupPingTimer);
2905
+ this.groupPingTimer = null;
2906
+ }
2907
+ this.stopMediaKeepaliveLoop();
2908
+ this.detachCallEventListener();
2909
+ if (this.udpSocket) {
2910
+ try {
2911
+ this.udpSocket.removeAllListeners();
2912
+ this.udpSocket.close();
2913
+ }
2914
+ catch {
2915
+ // Ignore
2916
+ }
2917
+ this.udpSocket = null;
2918
+ }
2919
+ }
2920
+ sendRawUdp(buf, host, port) {
2921
+ return new Promise((resolve, reject) => {
2922
+ if (!this.udpSocket) {
2923
+ reject(new ZaloApiError.ZaloApiError("UDP socket is not open"));
2924
+ return;
2925
+ }
2926
+ this.udpSocket.send(buf, port, host, (err) => {
2927
+ if (err)
2928
+ reject(err);
2929
+ else
2930
+ resolve();
2931
+ });
2932
+ });
2933
+ }
2934
+ }
2935
+
2936
+ exports.ZaloRtcSession = ZaloRtcSession;
2937
+ exports.parseIncomingCallEvent = parseIncomingCallEvent;