quickblox 2.23.1-beta.3 → 2.23.1-beta.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.
@@ -0,0 +1,353 @@
1
+ # SDK fix plan — unhandled TypeError in `_getStats` (`s.remote[mediaType].bitrate`)
2
+
3
+ **Клон:** `C:\___QuickBlox\05-quickblox-javascript-sdk-internal`
4
+ **База:** ветка `develop`, версия **`2.23.0-beta.3`** (= версия, опубликованная на проекте
5
+ Q-Consultation; именно она крашится в логе QC-1682).
6
+ **Целевая ветка фикса:** отдельная от `develop` (имя создаёт пользователь; Claude
7
+ объявляет «приступаю к ветке X» → пользователь создаёт → «начинай»).
8
+ **Файл:** `src/modules/webrtc/qbRTCPeerConnection.js`
9
+ **Происхождение:** диагностика логов QC-1682 (P2P web-провайдер, видео отсутствует).
10
+ Полный разбор лога — `03-q-consultation-premium/__ai-work-log/bug-batch-2026-05-28-brightside/logs/QC-1682-log-analysis-2026-05-30.md`.
11
+
12
+ > Примечание: ранняя версия этого плана была сохранена в клоне 01
13
+ > (`__getStats-mediaType-guard-fix-plan.md`, ветка `rc/...-QC-1454`, beta.2). Она
14
+ > устарела — выполняем в клоне 05. Тот файл можно удалить отдельно.
15
+
16
+ ---
17
+
18
+ ## 1. Симптом (из лога QC-1682)
19
+
20
+ В консоли web-провайдера 7 раз подряд:
21
+ ```
22
+ [Error] Unhandled Promise Rejection: TypeError: undefined is not an object
23
+ (evaluating '(e=s.remote[r.mediaType]).bitrate=c(r,a,!1)')
24
+ (anonymous function) (vendors...js:2:2190970)
25
+ forEach
26
+ forEachWrapper
27
+ forEach
28
+ ```
29
+
30
+ ## 2. Точное место (верифицировано в клоне 05)
31
+
32
+ `src/modules/webrtc/qbRTCPeerConnection.js`, функция `_getStats` (объявлена строкой 521),
33
+ внутри `peer.getStats(null).then(... results.forEach(function (result) {...}))`.
34
+ Строки **547-564** в клоне 05 **идентичны** клону 01 (сверено 2026-05-30).
35
+
36
+ ```js
37
+ // строки 522-533 — заранее заведённые ключи:
38
+ var statistic = {
39
+ 'local': { 'audio': {}, 'video': {}, 'candidate': {} },
40
+ 'remote': { 'audio': {}, 'video': {}, 'candidate': {} }
41
+ };
42
+ ...
43
+ // строки 547-555 — падающая ветка (remote / inbound-rtp):
44
+ if (result.bytesReceived && result.type === 'inbound-rtp') {
45
+ item = statistic.remote[result.mediaType]; // ← (1) item может стать undefined
46
+ item.bitrate = _getBitratePerSecond(result, lastResults, false); // ← (2) КРАШ: (undefined).bitrate
47
+ item.bytesReceived = result.bytesReceived;
48
+ item.packetsReceived = result.packetsReceived;
49
+ item.timestamp = result.timestamp;
50
+ if (result.mediaType === 'video' && result.framerateMean) {
51
+ item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
52
+ }
53
+ }
54
+ // строки 556-564 — симметричная local / outbound-rtp ветка с тем же дефектом:
55
+ else if (result.bytesSent && result.type === 'outbound-rtp') {
56
+ item = statistic.local[result.mediaType]; // ← тот же риск
57
+ item.bitrate = _getBitratePerSecond(result, lastResults, true);
58
+ ...
59
+ }
60
+ ```
61
+
62
+ Сопоставление с минифик-стеком (1:1): `s`=`statistic`, `r`=`result`,
63
+ `c`=`_getBitratePerSecond`, `a`=`lastResults`, `!1`=`false`, внешний `forEach` = `results.forEach`.
64
+
65
+ ## 3. Корневая причина (верифицировано)
66
+
67
+ `statistic.remote` содержит **только** ключи `audio` / `video` / `candidate` (строки 528-532).
68
+ В ветке `inbound-rtp` код индексирует `statistic.remote[result.mediaType]` **без проверки**,
69
+ что `result.mediaType` — один из этих ключей. Если у inbound-rtp report
70
+ `result.mediaType` равен `undefined` или нестандартному значению — `statistic.remote[undefined]`
71
+ === `undefined`, и следующая строка делает `(undefined).bitrate = ...` → `TypeError`.
72
+
73
+ **Почему mediaType бывает undefined:** поле `RTCStats.mediaType` устаревшее и заменено на
74
+ `kind` в современном webrtc-stats. Safari/iOS WebKit может не заполнять `mediaType` в
75
+ inbound-rtp статистике → индекс по `undefined`. В логе QC-1682 ровно этот сценарий
76
+ (P2P web↔iOS-mobile).
77
+
78
+ Это **НЕ корень отсутствия видео** в QC-1682 (тот корень — отсутствие H.264 в SDP,
79
+ backend-задача). Это **самостоятельный дефект getStats SDK**: краш при отсутствующем/
80
+ нестандартном `mediaType`. Он будет всплывать в любом звонке, где это поле не заполнено,
81
+ и при этом **прерывает весь `forEach` и роняет промис** (`getStats(null).then(...)` без
82
+ catch на этом уровне → unhandled rejection) — то есть теряется ВЕСЬ stats-репорт за тик,
83
+ не только проблемный трек. Это затрагивает и потребителей `_onCallStatsReport` (на нём
84
+ у Q-Consultation построены QC-1645/QC-1475 — оценка «живости» видео).
85
+
86
+ **Стабильность кода:** строка `item = statistic.remote[result.mediaType]` присутствует
87
+ минимум с релиза 2.12.5 и не менялась. Guard никогда не добавлялся.
88
+
89
+ **Допущение про beta.3 — СНЯТО.** Клон 05 = ровно `2.23.0-beta.3` (версия с бага), код
90
+ строк 547-564 подтверждён идентичным. В клоне 01 (beta.2) это было непроверенным риском —
91
+ здесь его нет.
92
+
93
+ ## 4. План правки (РЕШЕНО 2026-05-30: guard + `kind`-fallback, вариант 1)
94
+
95
+ **Scope:** guard (убрать краш) + `mediaType || kind` fallback (заполнить Safari-stats).
96
+ Оба изменения — в одном `[QC-1682]`-PR.
97
+
98
+ > **СОСТОЯНИЕ КОДА на 2026-05-31 (сверено с `qbRTCPeerConnection.js`):** другой агент уже
99
+ > применил БОЛЬШУЮ часть: (1) логика вынесена из inline-`forEach` в отдельную функцию
100
+ > `_applyStatReport(statistic, result, lastResults)` (строка 551) — это и есть T2-рефактор,
101
+ > делающий код тестируемым; (2) **guard `if (item) {...} else { traceWarning }` уже стоит**
102
+ > в обеих ветках (inbound строки 557-567, outbound 571-581). **ОСТАЛОСЬ ТОЛЬКО `kind`-fallback.**
103
+ > Блоки кода ниже — это ЦЕЛЕВОЕ состояние; фактическая правка = добавить fallback к уже
104
+ > существующему `_applyStatReport`, а не писать guard заново.
105
+
106
+ ### Ключевая деталь fallback — нормализовать тип ОДИН раз в локальную переменную
107
+
108
+ `result.mediaType` используется в обеих ветках **дважды**: для индексации
109
+ (`statistic.remote[result.mediaType]`) И для проверки framerate
110
+ (`result.mediaType === 'video'`, строки 114/137 исходника). Если применить fallback только
111
+ к индексации, то на Safari (где есть только `result.kind`) индекс заработает, а проверка
112
+ `result.mediaType === 'video'` останется ложной → `framesPerSecond` не запишется.
113
+ Поэтому fallback вычисляем **один раз** в `var mediaType` и используем его в обоих местах:
114
+
115
+ ```js
116
+ var mediaType = result.mediaType || result.kind; // ← в начале forEach-колбэка, рядом с `var item;`
117
+ ```
118
+
119
+ (`var item;` уже объявлен в `_applyStatReport` строкой 552 — добавляем `mediaType` туда же.)
120
+
121
+ ### Фактическая правка в `_applyStatReport` (4 точечных замены)
122
+
123
+ В существующей функции `_applyStatReport` (строка 551):
124
+
125
+ 1. **Строка 552** — добавить вычисление `mediaType` сразу после `var item;`:
126
+ ```js
127
+ var item;
128
+ var mediaType = result.mediaType || result.kind;
129
+ ```
130
+ 2. **Строка 555** (inbound индексация): `statistic.remote[result.mediaType]`
131
+ → `statistic.remote[mediaType]`
132
+ 3. **Строка 562** (inbound framerate): `if (result.mediaType === 'video' ...`
133
+ → `if (mediaType === 'video' ...`
134
+ 4. **Строка 569** (outbound индексация): `statistic.local[result.mediaType]`
135
+ → `statistic.local[mediaType]`
136
+ 5. **Строка 576** (outbound framerate): `if (result.mediaType === 'video' ...`
137
+ → `if (mediaType === 'video' ...`
138
+ 6. (косметика, опц.) traceWarning-строки 566/580: текст `unknown mediaType:` +
139
+ `result.mediaType` → `unknown mediaType/kind:` + `mediaType` (чтобы лог показывал
140
+ уже нормализованное значение).
141
+
142
+ **Целевое состояние inbound-ветки (557-567):**
143
+ ```js
144
+ if (result.bytesReceived && result.type === 'inbound-rtp') {
145
+ item = statistic.remote[mediaType];
146
+
147
+ if (item) {
148
+ item.bitrate = _getBitratePerSecond(result, lastResults, false);
149
+ item.bytesReceived = result.bytesReceived;
150
+ item.packetsReceived = result.packetsReceived;
151
+ item.timestamp = result.timestamp;
152
+ if (mediaType === 'video' && result.framerateMean) {
153
+ item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
154
+ }
155
+ } else {
156
+ Helpers.traceWarning('_getStats: skipping inbound-rtp report with unknown mediaType/kind: ' + mediaType);
157
+ }
158
+ }
159
+ ```
160
+ Outbound-ветка (568-581) — симметрично (`statistic.local[mediaType]`,
161
+ `mediaType === 'video'`, `isLocal=true`).
162
+
163
+ **Эффект fallback:** на Safari/iOS (`result.mediaType === undefined`, но
164
+ `result.kind === 'video'/'audio'`) `mediaType` теперь резолвится в корректный ключ →
165
+ `statistic.remote.video` / `.audio` заполняются → QC-1645/QC-1475 получают stats.
166
+ **guard остаётся** как страховка: если у report'а нет ни `mediaType`, ни `kind` (или
167
+ значение нестандартное, напр. `'data'`/`'application'`) — `item` будет `undefined`, и
168
+ тело пропускается с diagnostic-warn вместо краша.
169
+
170
+ **Почему `Helpers.traceWarning`, а не свой `if (config.debug) console.warn`:**
171
+ - Файл уже импортирует `Helpers = require('./qbWebRTCHelpers')` (строка 2 в require-блоке),
172
+ `traceWarning` используется в SDK широко.
173
+ - `traceWarning` сам гейтит на `config.debug` (qbWebRTCHelpers.js:26-29) — в production
174
+ под `config.debug=false` лог молчит, никакого шума.
175
+ - В debug-сессиях (диагностика следующих QC-инцидентов) команда сразу увидит причину
176
+ пропуска report без поиска guard'а в коде.
177
+
178
+ ### Что НЕ трогаем
179
+
180
+ - Ветки `local-candidate` / `remote-candidate` / `track` (565-593) — там `item`
181
+ присваивается фиксированным ключам (`statistic.local.candidate`, `statistic.remote.video`),
182
+ всегда существующим. Краш невозможен — guard не нужен. **(Утверждение
183
+ предварительное; перед началом реализации перечитать строки 565-593 и явно
184
+ подтвердить в commit-сообщении.)**
185
+ - `_getBitratePerSecond` / `_getFramesPerSecond` — не меняем.
186
+ - `package.json` версию — НЕ бамаем без отдельного решения (правило DO NOT).
187
+ - Грязные в `develop` файлы (`STATE.md`, `DECISIONS.md`, `__ai-work-log/assessments/`) —
188
+ не трогаем, они не относятся к фиксу (AI-tooling). Их не коммитим в ветку фикса.
189
+
190
+ ### Что НЕ делаем в этом PR (зафиксировано как follow-up)
191
+
192
+ - **`result.mediaType || result.kind` fallback** — отдельный follow-up SDK-тикет.
193
+ Контекст: поле `mediaType` deprecated в текущем W3C WebRTC stats spec, заменено на
194
+ `kind` (значения те же — `'audio'`/`'video'`). Старые Chrome/FF отдавали `mediaType`,
195
+ современные браузеры отдают оба ради backward-compat, **Safari/WebKit в части report'ов
196
+ отдаёт только `kind`** — это и есть истинная причина `mediaType === undefined` в логе
197
+ QC-1682. Текущий guard устраняет краш и unhandled rejection (P0), но Safari-stats
198
+ по-прежнему будут пустыми (P1 — функциональная потеря для QC-1645/QC-1475 «оценка
199
+ живости видео», которые читают `_onCallStatsReport`).
200
+ - Почему не делаем сейчас: fallback **меняет наблюдаемое поведение**
201
+ (`statistic.remote.video` для Safari начнёт заполняться там, где раньше был
202
+ `undefined`), а guard — нет (для валидного `mediaType` идентичный путь). Это разный
203
+ риск, разный scope, разный test plan. Не смешиваем.
204
+ - Что делаем: **РЕШЕНИЕ (2026-05-30): отдельного тикета НЕТ — всё в рамках QC-1682.**
205
+ Если fallback тоже делаем (см. §8 п.1), он идёт тем же `[QC-1682]`-коммитом/в том же
206
+ PR, что и guard. Если откладываем — фиксируем как follow-up внутри QC-1682, без
207
+ нового номера.
208
+
209
+ ## 5. Тестирование (РЕШЕНО: авто-тест на `_applyStatReport`)
210
+
211
+ **СОСТОЯНИЕ на 2026-05-31 (сверено):** инфраструктура теста уже готова другим агентом —
212
+ тестировать решено через выделенную функцию `_applyStatReport`, БЕЗ новых dev-зависимостей
213
+ (rewire не нужен):
214
+ - `qbRTCPeerConnection._applyStatReport = _applyStatReport;` (строка **832**) — функция
215
+ экспонирована как статик-свойство модуля. Чистая, синхронная, без сети/браузера.
216
+ - Создан спек `spec/QB-WebRTCStatsSpec.js` (113 строк) — `require` модуля напрямую под
217
+ Node, `apply = RTCPeerConnection._applyStatReport`, `freshStatistic()` хелпер.
218
+ - Спек **зарегистрирован во всех 3 jasmine-конфигах** (`jasmine.json`, `jasmine.pr.json`,
219
+ `jasmine.full.json`) → подхватывается `npm test` и `npm run test:pr`.
220
+
221
+ **Уже покрыто (6 кейсов, guard-часть):** inbound video, inbound audio, `mediaType=undefined`
222
+ (не бросает, remote не мутируется), `mediaType='application'` (skip), outbound video
223
+ (симметрия), `remote-candidate` (рефактор не сломал candidate-ветку).
224
+
225
+ ### ОСТАЛОСЬ ДОБАВИТЬ — кейсы на `kind`-fallback (сердцевина варианта 1)
226
+
227
+ Текущий тест НЕ проверяет fallback (его в коде ещё нет). После добавления fallback (§4)
228
+ дописать в `QB-WebRTCStatsSpec.js`:
229
+
230
+ 1. **Safari inbound video — `kind` без `mediaType`:**
231
+ ```js
232
+ it('fills statistic.remote.video when only kind is present (Safari)', function() {
233
+ var statistic = freshStatistic();
234
+ apply(statistic, {
235
+ type: 'inbound-rtp', kind: 'video', mediaType: undefined,
236
+ bytesReceived: 1000, packetsReceived: 10, timestamp: 12345
237
+ }, null);
238
+ expect(statistic.remote.video.bytesReceived).toEqual(1000);
239
+ });
240
+ ```
241
+ 2. **Safari inbound audio — `kind` без `mediaType`** (аналогично, `kind:'audio'`,
242
+ проверить `statistic.remote.audio.bytesReceived`).
243
+ 3. **Safari outbound video — `kind` без `mediaType`** (`type:'outbound-rtp'`, `kind:'video'`,
244
+ `bytesSent`, проверить `statistic.local.video.bytesSent`).
245
+ 4. **Приоритет `mediaType` над `kind`** (на случай конфликта): `mediaType:'video'`,
246
+ `kind:'audio'` → пишется в `remote.video` (т.к. `mediaType || kind` → `mediaType`).
247
+ Защищает от регрессии, если кто-то поменяет порядок на `kind || mediaType`.
248
+ 5. **Ни `mediaType`, ни `kind`** → skip, не бросает, remote не мутируется
249
+ (этот кейс частично есть как `mediaType=undefined`, но без `kind` — дополнить, что и
250
+ `kind` отсутствует, чтобы fallback тоже резолвился в `undefined`).
251
+ 6. **(желательно) framerate на Safari:** inbound `kind:'video'`, `framerateMean: 30`,
252
+ `lastResults` с предыдущим timestamp → `statistic.remote.video.framesPerSecond`
253
+ заполнен. Это прямая проверка, что нормализация `mediaType` в переменную (см. §4)
254
+ починила и framerate-ветку, а не только индексацию.
255
+
256
+ ### Замечание по browser-ветке спека (не блокер для PR)
257
+
258
+ Строка 9 спека: `window.QB.webrtc.RTCPeerConnection` — путь для браузер-прогона. Под Node
259
+ (`isNodeEnv=true`, как в `test:pr`) не используется. Перед браузер-прогоном стоит
260
+ подтвердить, что `QB.webrtc.RTCPeerConnection` — реальный публичный путь; для PR-проверки
261
+ (Node) не критично.
262
+
263
+ ## 6. Сборка / проверка (клон 05)
264
+
265
+ `node_modules` на месте — `npm i` не требуется. Скрипты (из package.json):
266
+ 1. `git diff` — функция `_applyStatReport` в `qbRTCPeerConnection.js` (+ статик-экспорт стр.
267
+ 832) + спек `spec/QB-WebRTCStatsSpec.js`. Финальная правка добавит лишь `kind`-fallback
268
+ к функции и fallback-кейсы в спек.
269
+ 2. `npm run lint` (jshint) — должно быть чисто.
270
+ 3. `npm run test:pr` (jasmine PR-набор, включает `QB-WebRTCSpec.js` + `QB-WebRTCStatsSpec.js`)
271
+ — все кейсы зелёные (6 guard + новые fallback).
272
+ 4. (опц.) `npm run test:node-regression` (`spec/phase-a-upgrade-check.js`).
273
+ 5. `npm run build` НЕ обязателен для PR кода (dist собирается отдельно), но если нужен
274
+ собранный артефакт для проверки на проекте — `npm run build` (lint+gulp+minify).
275
+
276
+ ## 7. Definition of Done
277
+
278
+ - [x] Логика вынесена в `_applyStatReport` (T2-рефактор) — уже сделано другим агентом.
279
+ - [x] Guard + `traceWarning` в обеих ветках — уже сделано другим агентом.
280
+ - [ ] **`kind`-fallback добавлен** (`var mediaType = result.mediaType || result.kind` +
281
+ 4 замены `result.mediaType` → `mediaType`, см. §4) — ОСТАВШАЯСЯ работа.
282
+ - [ ] `git diff` чист (только функция `_applyStatReport`, файл
283
+ `src/modules/webrtc/qbRTCPeerConnection.js`).
284
+ - [ ] Подтверждено перечитыванием candidate/`track`-веток (после 581), что они используют
285
+ фиксированные ключи (`statistic.*.candidate`, `statistic.remote.video` по
286
+ `remoteSource`) и fallback/guard им не нужен.
287
+ - [ ] `npm run lint` зелёный.
288
+ - [ ] `npm run test:pr` зелёный (регрессия).
289
+ - [ ] **Авто-тест на `_applyStatReport`** (теперь реалистичен — функция выделена; см. §5):
290
+ кейсы `mediaType:'video'` / `kind:'video'` без mediaType (Safari) / ни того ни
291
+ другого (skip+warn). Решить T1 (без теста) vs тест на `_applyStatReport` — §8 п.2.
292
+ - [ ] Префикс коммита — `[QC-1682]` (РЕШЕНИЕ 2026-05-30: отдельного тикета нет, всё в
293
+ рамках QC-1682). Связь отражена в коммите/PR: это побочный SDK-дефект из диагностики
294
+ QC-1682, НЕ чинит само отсутствие видео (корень — H.264/backend). Шаблон сообщения:
295
+ «Surfaced while diagnosing QC-1682. Not the root cause of QC-1682 video absence —
296
+ that is backend H.264 negotiation.»
297
+ - [ ] Коммит/пуш — только после явного approval пользователя; Claude готовит текст
298
+ коммита (1-2 строки), пользователь коммитит.
299
+
300
+ ## 8. Решения и открытые вопросы
301
+
302
+ **РЕШЕНО (2026-05-30):**
303
+ - **Доставка — вариант A:** фикс в SDK (клон 05) → бамп версии + build + `npm publish`
304
+ → перевод Q-Consultation на новую версию. Полный релизный цикл. Детали — §9.
305
+ - **Без отдельного Jira-тикета:** всё в рамках **QC-1682**. Префикс коммита `[QC-1682]`.
306
+
307
+ - **Scope — РЕШЕНО (вариант 1): guard + `kind`-fallback.** Закрывает и краш, и пустую
308
+ Safari-статистику (QC-1645/QC-1475). Оба в одном `[QC-1682]`-PR. Guard уже в коде;
309
+ остался fallback (§4).
310
+
311
+ - **Авто-тест — РЕШЕНО: ДА.** Через `_applyStatReport` (статик `qbRTCPeerConnection._applyStatReport`,
312
+ строка 832), спек `spec/QB-WebRTCStatsSpec.js` уже создан и зарегистрирован в jasmine-конфигах.
313
+ Guard-кейсы (6) уже написаны; остаётся дописать `kind`-fallback-кейсы (см. §5, 6 шт.).
314
+
315
+ **Открытых вопросов перед кодом не осталось.** Оставшаяся работа агента:
316
+ (1) `kind`-fallback в `_applyStatReport` (§4), (2) fallback-тест-кейсы (§5), (3) lint +
317
+ test:pr, (4) подготовить текст коммита `[QC-1682]` для пользователя.
318
+
319
+ ## 9. Доставка фикса до прода — вариант A (РЕШЕНО 2026-05-30)
320
+
321
+ Полный релизный цикл: SDK → npm → Q-Consultation. Всё под `[QC-1682]`.
322
+
323
+ ### Этап 1 — фикс в SDK (клон 05, отдельная ветка от `develop`)
324
+ Реализация по §4 (другой агент), проверка по §6 (`lint` + `test:pr`). Коммит — пользователь,
325
+ после approval, текст готовит Claude (1-2 строки, префикс `[QC-1682]`).
326
+
327
+ ### Этап 2 — версия + публикация в npm ⚠️ финансово/инфраструктурно значимо
328
+ - **Бамп версии** в `package.json` SDK: `2.23.0-beta.3` → `2.23.0-beta.4` (или по
329
+ конвенции релизов QuickBlox — уточнить у владельца релиза SDK). Это и есть «отдельное
330
+ решение», которое снимает запрет §4 «версию не бамаем».
331
+ - **Сборка `dist/`:** `npm run build` (lint + gulp + minify) — npm-пакет содержит
332
+ собранный код, не `src/`.
333
+ - **`npm publish`** под нужным dist-tag (вероятно `beta`). Выполняет **ПОЛЬЗОВАТЕЛЬ**
334
+ (или CI/релиз-процесс). Claude НЕ имеет и не должен иметь доступа к npm-аккаунту
335
+ QuickBlox (security-baseline §5 «НИКОГДА npm publish»). Claude только готовит шаги.
336
+ - Проверить, что новая версия реально появилась в npm registry перед этапом 3.
337
+
338
+ ### Этап 3 — перевод Q-Consultation на новую версию
339
+ Клон проекта: `C:\___QuickBlox\0X-q-consultation-premium` (тот, где будем переключать —
340
+ уточнить какой; текущая работа шла в `03-`).
341
+ - `packages/quickblox/package.json`: `"quickblox": "2.23.0-beta.3"` → новая версия.
342
+ (Проверено 2026-05-30: зависимость объявлена именно там; `yarn.lock` резолвит из npm.)
343
+ - `yarn install` — обновит `yarn.lock` (пользователь разрешил install объявленного без
344
+ отдельного спроса; но смена версии пакета — это уже изменение зависимости, спросить).
345
+ - Пересборка зависимых пакетов: `@qc/quickblox`, `@qc/conference` (зависят от `quickblox`).
346
+ - Проверка: tsc (provider/client/api) + vitest + Playwright + dev-test реального звонка
347
+ (web↔Safari/iOS, чтобы убедиться: краш `bitrate` ушёл; если делали `kind`-fallback —
348
+ что Safari-stats заполняются).
349
+
350
+ ### Что подтвердить ПЕРЕД этапом 3
351
+ - Версию (`beta.4`?) и dist-tag — согласовать с владельцем релиза SDK.
352
+ - Не сломает ли смена версии SDK другие клоны Q-Consultation (01/02/04) — они на той же
353
+ зависимости; менять только целевой клон или все — отдельное решение.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "quickblox",
3
3
  "description": "QuickBlox JavaScript SDK",
4
- "version": "2.23.1-beta.3",
4
+ "version": "2.23.1-beta.5",
5
5
  "homepage": "https://quickblox.com/developers/Javascript",
6
6
  "main": "src/qbMain.js",
7
7
  "types": "quickblox.d.ts",
@@ -83,6 +83,9 @@
83
83
  "lint": "jshint src --reporter=node_modules/jshint-stylish",
84
84
  "test:node-regression": "node spec/phase-a-upgrade-check.js",
85
85
  "test:regression": "npm run test:node-regression && npm test",
86
+ "test:pr": "jasmine --config=spec/support/jasmine.pr.json",
87
+ "test:full": "jasmine --config=spec/support/jasmine.full.json",
88
+ "test:ci": "npm run lint && npm run test:node-regression && npm run test:pr",
86
89
  "build": "cross-env NODE_ENV=production npm run lint && gulp build && gulp minify",
87
90
  "buildNotMinified": "npm run lint && gulp build",
88
91
  "generateBuildVersion": "gulp generate-build_version",
package/quickblox.js CHANGED
@@ -36930,92 +36930,110 @@ function _getStats(peer, lastResults, successCallback, errorCallback) {
36930
36930
 
36931
36931
  peer.getStats(null).then(function (results) {
36932
36932
  results.forEach(function (result) {
36933
- var item;
36934
-
36935
- if (result.bytesReceived && result.type === 'inbound-rtp') {
36936
- item = statistic.remote[result.mediaType];
36937
- item.bitrate = _getBitratePerSecond(result, lastResults, false);
36938
- item.bytesReceived = result.bytesReceived;
36939
- item.packetsReceived = result.packetsReceived;
36940
- item.timestamp = result.timestamp;
36941
- if (result.mediaType === 'video' && result.framerateMean) {
36942
- item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
36943
- }
36944
- } else if (result.bytesSent && result.type === 'outbound-rtp') {
36945
- item = statistic.local[result.mediaType];
36946
- item.bitrate = _getBitratePerSecond(result, lastResults, true);
36947
- item.bytesSent = result.bytesSent;
36948
- item.packetsSent = result.packetsSent;
36949
- item.timestamp = result.timestamp;
36950
- if (result.mediaType === 'video' && result.framerateMean) {
36951
- item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
36952
- }
36953
- } else if (result.type === 'local-candidate') {
36954
- item = statistic.local.candidate;
36955
- if (result.candidateType === 'host' && result.mozLocalTransport === 'udp' && result.transport === 'udp') {
36956
- item.protocol = result.transport;
36957
- item.ip = result.ipAddress;
36958
- item.port = result.portNumber;
36959
- } else if (!Helpers.getVersionFirefox()) {
36960
- item.protocol = result.protocol;
36961
- item.ip = result.ip;
36962
- item.port = result.port;
36963
- }
36964
- } else if (result.type === 'remote-candidate') {
36965
- item = statistic.remote.candidate;
36966
- item.protocol = result.protocol || result.transport;
36967
- item.ip = result.ip || result.ipAddress;
36968
- item.port = result.port || result.portNumber;
36969
- } else if (result.type === 'track' && result.kind === 'video' && !Helpers.getVersionFirefox()) {
36970
- if (result.remoteSource) {
36971
- item = statistic.remote.video;
36972
- item.frameHeight = result.frameHeight;
36973
- item.frameWidth = result.frameWidth;
36974
- item.framesPerSecond = _getFramesPerSecond(result, lastResults, false);
36975
- } else {
36976
- item = statistic.local.video;
36977
- item.frameHeight = result.frameHeight;
36978
- item.frameWidth = result.frameWidth;
36979
- item.framesPerSecond = _getFramesPerSecond(result, lastResults, true);
36980
- }
36981
- }
36933
+ _applyStatReport(statistic, result, lastResults);
36982
36934
  });
36983
36935
  successCallback(statistic, results);
36984
36936
  }, errorCallback);
36937
+ }
36938
+
36939
+ function _applyStatReport(statistic, result, lastResults) {
36940
+ var item;
36941
+ // mediaType is deprecated in the W3C webrtc-stats spec and replaced by kind.
36942
+ // Safari/WebKit omits mediaType on some reports and provides only kind, so we
36943
+ // normalize once and use it for both the statistic lookup and the 'video' check.
36944
+ var mediaType = result.mediaType || result.kind;
36945
+
36946
+ if (result.bytesReceived && result.type === 'inbound-rtp') {
36947
+ item = statistic.remote[mediaType];
36985
36948
 
36986
- function _getBitratePerSecond(result, lastResults, isLocal) {
36987
- var lastResult = lastResults && lastResults.get(result.id),
36988
- seconds = lastResult ? ((result.timestamp - lastResult.timestamp) / 1000) : 5,
36989
- kilo = 1024,
36990
- bit = 8,
36991
- bitrate;
36992
-
36993
- if (!lastResult) {
36994
- bitrate = 0;
36995
- } else if (isLocal) {
36996
- bitrate = bit * (result.bytesSent - lastResult.bytesSent) / (kilo * seconds);
36949
+ if (item) {
36950
+ item.bitrate = _getBitratePerSecond(result, lastResults, false);
36951
+ item.bytesReceived = result.bytesReceived;
36952
+ item.packetsReceived = result.packetsReceived;
36953
+ item.timestamp = result.timestamp;
36954
+ if (mediaType === 'video' && result.framerateMean) {
36955
+ item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
36956
+ }
36957
+ } else {
36958
+ Helpers.traceWarning('_getStats: skipping inbound-rtp report with unknown mediaType/kind: ' + mediaType);
36959
+ }
36960
+ } else if (result.bytesSent && result.type === 'outbound-rtp') {
36961
+ item = statistic.local[mediaType];
36962
+
36963
+ if (item) {
36964
+ item.bitrate = _getBitratePerSecond(result, lastResults, true);
36965
+ item.bytesSent = result.bytesSent;
36966
+ item.packetsSent = result.packetsSent;
36967
+ item.timestamp = result.timestamp;
36968
+ if (mediaType === 'video' && result.framerateMean) {
36969
+ item.framesPerSecond = Math.round(result.framerateMean * 10) / 10;
36970
+ }
36997
36971
  } else {
36998
- bitrate = bit * (result.bytesReceived - lastResult.bytesReceived) / (kilo * seconds);
36972
+ Helpers.traceWarning('_getStats: skipping outbound-rtp report with unknown mediaType/kind: ' + mediaType);
36973
+ }
36974
+ } else if (result.type === 'local-candidate') {
36975
+ item = statistic.local.candidate;
36976
+ if (result.candidateType === 'host' && result.mozLocalTransport === 'udp' && result.transport === 'udp') {
36977
+ item.protocol = result.transport;
36978
+ item.ip = result.ipAddress;
36979
+ item.port = result.portNumber;
36980
+ } else if (!Helpers.getVersionFirefox()) {
36981
+ item.protocol = result.protocol;
36982
+ item.ip = result.ip;
36983
+ item.port = result.port;
36984
+ }
36985
+ } else if (result.type === 'remote-candidate') {
36986
+ item = statistic.remote.candidate;
36987
+ item.protocol = result.protocol || result.transport;
36988
+ item.ip = result.ip || result.ipAddress;
36989
+ item.port = result.port || result.portNumber;
36990
+ } else if (result.type === 'track' && result.kind === 'video' && !Helpers.getVersionFirefox()) {
36991
+ if (result.remoteSource) {
36992
+ item = statistic.remote.video;
36993
+ item.frameHeight = result.frameHeight;
36994
+ item.frameWidth = result.frameWidth;
36995
+ item.framesPerSecond = _getFramesPerSecond(result, lastResults, false);
36996
+ } else {
36997
+ item = statistic.local.video;
36998
+ item.frameHeight = result.frameHeight;
36999
+ item.frameWidth = result.frameWidth;
37000
+ item.framesPerSecond = _getFramesPerSecond(result, lastResults, true);
36999
37001
  }
37002
+ }
37003
+ }
37000
37004
 
37001
- return Math.round(bitrate);
37005
+ function _getBitratePerSecond(result, lastResults, isLocal) {
37006
+ var lastResult = lastResults && lastResults.get(result.id),
37007
+ seconds = lastResult ? ((result.timestamp - lastResult.timestamp) / 1000) : 5,
37008
+ kilo = 1024,
37009
+ bit = 8,
37010
+ bitrate;
37011
+
37012
+ if (!lastResult) {
37013
+ bitrate = 0;
37014
+ } else if (isLocal) {
37015
+ bitrate = bit * (result.bytesSent - lastResult.bytesSent) / (kilo * seconds);
37016
+ } else {
37017
+ bitrate = bit * (result.bytesReceived - lastResult.bytesReceived) / (kilo * seconds);
37002
37018
  }
37003
37019
 
37004
- function _getFramesPerSecond(result, lastResults, isLocal) {
37005
- var lastResult = lastResults && lastResults.get(result.id),
37006
- seconds = lastResult ? ((result.timestamp - lastResult.timestamp) / 1000) : 5,
37007
- framesPerSecond;
37020
+ return Math.round(bitrate);
37021
+ }
37008
37022
 
37009
- if (!lastResult) {
37010
- framesPerSecond = 0;
37011
- } else if (isLocal) {
37012
- framesPerSecond = (result.framesSent - lastResult.framesSent) / seconds;
37013
- } else {
37014
- framesPerSecond = (result.framesReceived - lastResult.framesReceived) / seconds;
37015
- }
37023
+ function _getFramesPerSecond(result, lastResults, isLocal) {
37024
+ var lastResult = lastResults && lastResults.get(result.id),
37025
+ seconds = lastResult ? ((result.timestamp - lastResult.timestamp) / 1000) : 5,
37026
+ framesPerSecond;
37016
37027
 
37017
- return Math.round(framesPerSecond * 10) / 10;
37028
+ if (!lastResult) {
37029
+ framesPerSecond = 0;
37030
+ } else if (isLocal) {
37031
+ framesPerSecond = (result.framesSent - lastResult.framesSent) / seconds;
37032
+ } else {
37033
+ framesPerSecond = (result.framesReceived - lastResult.framesReceived) / seconds;
37018
37034
  }
37035
+
37036
+ return Math.round(framesPerSecond * 10) / 10;
37019
37037
  }
37020
37038
 
37021
37039
  // Find the line in sdpLines[startLine...endLine - 1] that starts with |prefix|
@@ -37202,6 +37220,9 @@ function setMediaBitrate(sdp, media, bitrate) {
37202
37220
  return newLines.join('\n');
37203
37221
  }
37204
37222
 
37223
+ // PRIVATE - exposed for unit tests only, not part of the public SDK contract.
37224
+ qbRTCPeerConnection._applyStatReport = _applyStatReport;
37225
+
37205
37226
  module.exports = qbRTCPeerConnection;
37206
37227
 
37207
37228
  },{"../../qbConfig":151,"./qbWebRTCHelpers":145}],144:[function(require,module,exports){
@@ -39702,7 +39723,7 @@ module.exports = StreamManagement;
39702
39723
  */
39703
39724
 
39704
39725
  var config = {
39705
- version: '2.23.1-beta.3',
39726
+ version: '2.23.1-beta.5',
39706
39727
  buildNumber: '1179',
39707
39728
  creds: {
39708
39729
  'appId': 0,