Browse Source

Cancel stale video port commands and expose hangup regression

master
evgeny 4 days ago
parent
commit
59fb020711
  1. 9
      doc/tasks.md
  2. 22
      src/call/call_video_port.c
  3. 84
      tests/test_call.c

9
doc/tasks.md

@ -4,6 +4,15 @@
## Текущие задачи ## Текущие задачи
[ ] **Проверить HANGUP B → A после восстановления транспорта в test_call** — 15.10.2026:
Регрессия завершения видеопорта выявила: после искусственного DOWN/UP в P_SEND_MEDIA
B завершает звонок локально, но A получает ENDED по NO_TRAFFIC через 20с вместо REMOTE_HANGUP.
Проверка причин завершения теперь явная; простой факт двух ENDED не означает доставку HANGUP.
Отмена ожидающих TX/configure и позднего open в call_video_port проверена; до строгой проверки
также прошли новый звонок, защита от старого close и detach с ожидающим post.
Причина потери сигналинга пока не установлена: проверить router epochs/control после DOWN/UP
и корректность тестового сценария. Сборки core/desktop/APK проходят; обновление не установлено.
[ ] **Видеозвонки Android и ChatGUI** — 14.10.2026: [ ] **Видеозвонки Android и ChatGUI** — 14.10.2026:
H.265 с fallback H.264, смена камеры/разрешения/кодека/битрейта во время звонка. H.265 с fallback H.264, смена камеры/разрешения/кодека/битрейта во время звонка.
Общая шкала захвата, приёмное видео следует аудиоряду; адаптация по ACK 500мс/50мс и задолженности. Общая шкала захвата, приёмное видео следует аудиоряду; адаптация по ACK 500мс/50мс и задолженности.

22
src/call/call_video_port.c

@ -1,5 +1,6 @@
#include "call_video_port.h" #include "call_video_port.h"
#include "call_video_transport.h" #include "call_video_transport.h"
#include "call_priv.h"
#include "../chat/chat_setting.h" #include "../chat/chat_setting.h"
#include "../utun_instance.h" #include "../utun_instance.h"
#include "../../lib/u_async.h" #include "../../lib/u_async.h"
@ -105,16 +106,29 @@ static void process(void *arg) {
if (opening) port.bound = id; if (opening) port.bound = id;
pthread_mutex_unlock(&lock); pthread_mutex_unlock(&lock);
int result = 0, sent = 1; int result = 0, sent = 1;
if (inst) { /* Команды и завершение сессии исполняются одним uasync-потоком: после этой
* проверки сессия не может завершиться посреди исполнения снимка. */
struct call_session *session = inst && id ? call_session_find(inst->call, id) : NULL;
int active = session && session->state == CALL_ST_ACTIVE;
if (inst && opening && old && old != id) {
struct call_session *previous = call_session_find(inst->call, old);
if (previous && previous->state == CALL_ST_ACTIVE) {
struct call_video_caps empty = {0}; call_video_configure(inst, old, &empty, NULL, NULL);
}
}
if (inst && id && !active) {
DEBUG_DEBUG(DEBUG_CATEGORY_CALL, "video port cancelled stale commands call=%llu state=%d tx_bytes=%u",
(unsigned long long)id, session ? session->state : -1, tx.size);
// Проверка id внутри close сохраняет порт следующего звонка, открытый worker во время обработки.
call_video_port_close(id);
}
if (inst && active) {
if (range) { if (range) {
char value[32]; snprintf(value, sizeof(value), "%u:%u", minimum / 1000, maximum / 1000); char value[32]; snprintf(value, sizeof(value), "%u:%u", minimum / 1000, maximum / 1000);
if (chat_setting_set(inst, "call_video_bitrate_range_kbps", value) < 0) if (chat_setting_set(inst, "call_video_bitrate_range_kbps", value) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video port bitrate range rejected value=%s", value); DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video port bitrate range rejected value=%s", value);
} }
call_video_rate_parse_range(chat_setting_get(inst, "call_video_bitrate_range_kbps"), &minimum, &maximum); call_video_rate_parse_range(chat_setting_get(inst, "call_video_bitrate_range_kbps"), &minimum, &maximum);
if (opening && old && old != id) {
struct call_video_caps empty = {0}; call_video_configure(inst, old, &empty, NULL, NULL);
}
if (opening && id) result = call_video_configure(inst, id, &caps, &ops, (void *)(uintptr_t)id); if (opening && id) result = call_video_configure(inst, id, &caps, &ops, (void *)(uintptr_t)id);
if (id && configure) result = call_video_configure_rate(inst, id, &format, bps, format.maximum_bps); if (id && configure) result = call_video_configure_rate(inst, id, &format, bps, format.maximum_bps);
if (id && completed) call_video_rate_applied(inst, id, generation, request, applied_bps); if (id && completed) call_video_rate_applied(inst, id, generation, request, applied_bps);

84
tests/test_call.c

@ -6,8 +6,9 @@
// Сценарий: // Сценарий:
// 1. PM BGP: живой/закрывающийся/исчезнувший путь, выбор общей группы и повторная проверка. // 1. PM BGP: живой/закрывающийся/исчезнувший путь, выбор общей группы и повторная проверка.
// 2. happy path из PM: A call_start → B INCOMING → B accept → обе ACTIVE (ACCEPTED) // 2. happy path из PM: A call_start → B INCOMING → B accept → обе ACTIVE (ACCEPTED)
// → A hangup → обе ENDED. // → B hangup с ожидающими видеокомандами → обе ENDED, поздние команды отменены.
// 3. decline из PM: A call_start → B INCOMING → B decline → обе ENDED(DECLINE). // 3. Новый звонок: поздний close старого порта не закрывает новый, detach отменяет post.
// 4. decline из PM: A call_start → B INCOMING → B decline → обе ENDED(DECLINE).
#include "chat_core.h" #include "chat_core.h"
#include "chat_sync.h" #include "chat_sync.h"
@ -66,10 +67,11 @@ enum call_phase {
P_VIDEO_CONFIG, P_VIDEO_SEND, P_VIDEO_HOLD, P_VIDEO_BUSY, P_VIDEO_RESUME, P_VIDEO_RANGE_WAIT, P_VIDEO_SWITCH, P_VIDEO_SWITCH_WAIT, P_VIDEO_CONFIG, P_VIDEO_SEND, P_VIDEO_HOLD, P_VIDEO_BUSY, P_VIDEO_RESUME, P_VIDEO_RANGE_WAIT, P_VIDEO_SWITCH, P_VIDEO_SWITCH_WAIT,
P_VIDEO_SHRINK_WAIT, P_VIDEO_PORT_OPEN, P_VIDEO_PORT_RECEIVE, P_VIDEO_PORT_SEND, P_VIDEO_PORT_ACK, P_VIDEO_SHRINK_WAIT, P_VIDEO_PORT_OPEN, P_VIDEO_PORT_RECEIVE, P_VIDEO_PORT_SEND, P_VIDEO_PORT_ACK,
P_VIDEO_DECODE_OK, P_VIDEO_DECODE_REJECT, P_VIDEO_DECODE_OK, P_VIDEO_DECODE_REJECT,
P_HANGUP, /* A: call_hangup */ P_VIDEO_PORT_CANCELLED, P_VIDEO_PORT_REOPENED,
P_WAIT_ENDED1, /* обе ENDED */ P_WAIT_ENDED1, /* обе ENDED */
P_CALL2_START, /* A: call_start (2-й звонок) */ P_CALL2_START, /* A: call_start (2-й звонок) */
P_WAIT_INCOMING2, /* B: INCOMING */ P_WAIT_INCOMING2, /* B: INCOMING */
P_VIDEO_NEW_CALL, P_VIDEO_NEW_PORT,
P_DECLINE, /* B: call_decline */ P_DECLINE, /* B: call_decline */
P_WAIT_ENDED2, /* обе ENDED + A DECLINED */ P_WAIT_ENDED2, /* обе ENDED + A DECLINED */
P_DONE, P_DONE,
@ -84,6 +86,8 @@ struct call_ctx {
int phase_ticks; int phase_ticks;
void* timer; void* timer;
uint64_t call_id; /* текущий call_id */ uint64_t call_id; /* текущий call_id */
uint64_t previous_call_id;
int video_restart_tested;
}; };
/* записи событий CHAT_EVT_CALL_* на узел */ /* записи событий CHAT_EVT_CALL_* на узел */
@ -688,18 +692,44 @@ static void call_tick(void* arg) {
if (++t->phase_ticks < 10) break; // Регулятор не должен отменять запрет decoder на следующем tick. if (++t->phase_ticks < 10) break; // Регулятор не должен отменять запрет decoder на следующем tick.
struct call_video_frame f = video_test_frame(t->call_id, 1); f.frame_id = 2; f.pts_us += 33333; struct call_video_frame f = video_test_frame(t->call_id, 1); f.frame_id = 2; f.pts_us += 33333;
if (call_video_send_frame(B, &f) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video ignored decoder rejection"); t->result = 2; break; } if (call_video_send_frame(B, &f) != 0) { DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video ignored decoder rejection"); t->result = 2; break; }
// Все команды попадут в uasync после синхронного ENDED; TX ещё принадлежит mailbox.
if (call_video_port_format(t->call_id, &f.format, 500000)
|| call_video_port_applied(t->call_id, 1, 1, 500000)
|| call_video_port_decoded(t->call_id, 1, 1) || call_video_port_send(&f) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video teardown commands were not queued"); t->result = 2; break;
}
call_video_port_request_key(t->call_id); call_video_port_request_key(t->call_id);
call_video_port_close(t->call_id); t->previous_call_id = t->call_id;
call_video_port_detach(B); // Отменяет уже поставленный post до destroy ядра. call_hangup(B, t->call_id);
if (call_video_port_status(t->call_id, &status) != -1) { t->result = 2; break; } t->phase = P_VIDEO_PORT_CANCELLED; break;
t->phase = P_HANGUP; break; }
case P_VIDEO_PORT_CANCELLED: {
struct call_video_port_status status;
struct call_video_frame f = video_test_frame(t->call_id, 1);
if (call_video_port_status(t->call_id, &status) != -1 || call_video_port_send(&f) != -1 || g_port_frame != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "ended video port retained commands or accepted a late frame"); t->result = 2; break;
}
call_video_port_close(t->call_id); // Повторное закрытие после отмены команд.
if (call_video_port_open(t->call_id, &g_video_peer[0])) { t->result = 2; break; }
t->phase = P_VIDEO_PORT_REOPENED; break;
}
case P_VIDEO_PORT_REOPENED: {
struct call_video_port_status status;
if (call_video_port_status(t->call_id, &status) != -1) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "late open resurrected an ended video session"); t->result = 2; break;
}
DEBUG_INFO(DEBUG_CATEGORY_CALL, "video teardown regression: queued TX/configuration and late open cancelled");
t->phase = P_WAIT_ENDED1; break;
} }
case P_HANGUP:
call_hangup(A, t->call_id);
t->phase = P_WAIT_ENDED1;
break;
case P_WAIT_ENDED1: case P_WAIT_ENDED1:
if (ea->ended && eb->ended && ea->ended_call_id == t->call_id) t->phase = P_CALL2_START; if (ea->ended && eb->ended && ea->ended_call_id == t->call_id) {
if (ea->ended_reason != CALL_REASON_REMOTE_HANGUP || eb->ended_reason != CALL_REASON_LOCAL_HANGUP) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "video teardown: HANGUP delivery failed caller_reason=%u callee_reason=%u",
ea->ended_reason, eb->ended_reason);
t->result = 2; break;
}
t->phase = P_CALL2_START;
}
break; break;
case P_CALL2_START: { case P_CALL2_START: {
memset(ea, 0, sizeof(*ea)); memset(ea, 0, sizeof(*ea));
@ -708,20 +738,44 @@ static void call_tick(void* arg) {
if (!test_pm_route(A, nb, target)) { if (!test_pm_route(A, nb, target)) {
fprintf(stderr, "PM route availability/recheck failed\n"); t->result = 2; uasync_stop(t->ua); return; fprintf(stderr, "PM route availability/recheck failed\n"); t->result = 2; uasync_stop(t->ua); return;
} }
t->call_id = ((uint64_t)2 << 40) | (uint64_t)(rand() & 0xFFFFF); ++t->call_id;
call_start(A, target, nb, t->call_id); call_start(A, target, nb, t->call_id);
t->phase = P_WAIT_INCOMING2; t->phase = P_WAIT_INCOMING2;
break; break;
} }
case P_WAIT_INCOMING2: case P_WAIT_INCOMING2:
if (eb->incoming && eb->incoming_call_id == t->call_id) t->phase = P_DECLINE; if (eb->incoming && eb->incoming_call_id == t->call_id) {
if (t->video_restart_tested) t->phase = P_DECLINE;
else { call_accept(B, t->call_id); t->phase = P_VIDEO_NEW_CALL; }
}
break; break;
case P_VIDEO_NEW_CALL: {
if (!ea->accepted || !eb->accepted) break;
if (call_video_configure(A, t->call_id, &g_video_peer[1], &video_ops, (void *)0)
|| call_video_port_open(t->call_id, &g_video_peer[0])) { t->result = 2; break; }
call_video_port_close(t->previous_call_id);
t->phase = P_VIDEO_NEW_PORT; break;
}
case P_VIDEO_NEW_PORT: {
struct call_video_port_status status;
if (call_video_port_status(t->call_id, &status) || status.error) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "late close affected the new video port"); t->result = 2; break;
}
if (!status.have_peer) break;
call_video_port_request_key(t->call_id);
call_video_port_close(t->call_id);
call_video_port_detach(B); // Отменяет уже поставленный post до destroy ядра.
if (call_video_port_status(t->call_id, &status) != -1) { t->result = 2; break; }
DEBUG_INFO(DEBUG_CATEGORY_CALL, "video teardown regression: new call survives stale close; detach cancels post");
t->video_restart_tested = 1;
call_hangup(A, t->call_id); t->phase = P_WAIT_ENDED2; break;
}
case P_DECLINE: case P_DECLINE:
call_decline(B, t->call_id); call_decline(B, t->call_id);
t->phase = P_WAIT_ENDED2; t->phase = P_WAIT_ENDED2;
break; break;
case P_WAIT_ENDED2: case P_WAIT_ENDED2:
if (ea->ended && eb->ended && ea->declined) t->phase = P_DONE; if (ea->ended && eb->ended) t->phase = ea->declined ? P_DONE : P_CALL2_START;
break; break;
case P_DONE: case P_DONE:
t->result = 1; t->result = 1;

Loading…
Cancel
Save