diff --git a/doc/tasks.md b/doc/tasks.md index ada31216..68b01840 100644 --- a/doc/tasks.md +++ b/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: H.265 с fallback H.264, смена камеры/разрешения/кодека/битрейта во время звонка. Общая шкала захвата, приёмное видео следует аудиоряду; адаптация по ACK 500мс/50мс и задолженности. diff --git a/src/call/call_video_port.c b/src/call/call_video_port.c index 66c6c37c..2ca77439 100644 --- a/src/call/call_video_port.c +++ b/src/call/call_video_port.c @@ -1,5 +1,6 @@ #include "call_video_port.h" #include "call_video_transport.h" +#include "call_priv.h" #include "../chat/chat_setting.h" #include "../utun_instance.h" #include "../../lib/u_async.h" @@ -105,16 +106,29 @@ static void process(void *arg) { if (opening) port.bound = id; pthread_mutex_unlock(&lock); 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) { 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) 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); - 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 (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); diff --git a/tests/test_call.c b/tests/test_call.c index 2a76f1de..421e8c8b 100644 --- a/tests/test_call.c +++ b/tests/test_call.c @@ -6,8 +6,9 @@ // Сценарий: // 1. PM BGP: живой/закрывающийся/исчезнувший путь, выбор общей группы и повторная проверка. // 2. happy path из PM: A call_start → B INCOMING → B accept → обе ACTIVE (ACCEPTED) -// → A hangup → обе ENDED. -// 3. decline из PM: A call_start → B INCOMING → B decline → обе ENDED(DECLINE). +// → B hangup с ожидающими видеокомандами → обе ENDED, поздние команды отменены. +// 3. Новый звонок: поздний close старого порта не закрывает новый, detach отменяет post. +// 4. decline из PM: A call_start → B INCOMING → B decline → обе ENDED(DECLINE). #include "chat_core.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_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_HANGUP, /* A: call_hangup */ + P_VIDEO_PORT_CANCELLED, P_VIDEO_PORT_REOPENED, P_WAIT_ENDED1, /* обе ENDED */ P_CALL2_START, /* A: call_start (2-й звонок) */ P_WAIT_INCOMING2, /* B: INCOMING */ + P_VIDEO_NEW_CALL, P_VIDEO_NEW_PORT, P_DECLINE, /* B: call_decline */ P_WAIT_ENDED2, /* обе ENDED + A DECLINED */ P_DONE, @@ -84,6 +86,8 @@ struct call_ctx { int phase_ticks; void* timer; uint64_t call_id; /* текущий call_id */ + uint64_t previous_call_id; + int video_restart_tested; }; /* записи событий CHAT_EVT_CALL_* на узел */ @@ -688,18 +692,44 @@ static void call_tick(void* arg) { 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; 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_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; } - t->phase = P_HANGUP; break; + t->previous_call_id = t->call_id; + call_hangup(B, t->call_id); + t->phase = P_VIDEO_PORT_CANCELLED; 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: - 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; case P_CALL2_START: { memset(ea, 0, sizeof(*ea)); @@ -708,20 +738,44 @@ static void call_tick(void* arg) { if (!test_pm_route(A, nb, target)) { 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); t->phase = P_WAIT_INCOMING2; break; } 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; + 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: call_decline(B, t->call_id); t->phase = P_WAIT_ENDED2; break; 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; case P_DONE: t->result = 1;