diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..2efe544e --- /dev/null +++ b/.envrc @@ -0,0 +1,2 @@ +eval "$(devenv direnvrc)" +use devenv diff --git a/PLAN-calls.md b/PLAN-calls.md new file mode 100644 index 00000000..d42fb949 --- /dev/null +++ b/PLAN-calls.md @@ -0,0 +1,81 @@ + # Stabilize Existing VoIP Call Flow and Add Non-Fatal Diagnostics + + ## Summary + + Tighten the existing Matrix call flow in the current VoIP stack, focusing on real correctness issues already present in src/voip/CallManager.cpp and src/voip/WebRTCSession.cpp, and + add structured logging around non-fatal failures so future call bugs are diagnosable without crashing or silently retrying forever. + + This plan targets: + + - Core invite/answer/candidate/select-answer stability + - Inbound renegotiation support for m.call.negotiate + - Logging of non-critical exceptions and ignored/invalid call events + - No user-visible feature expansion beyond making current calls more reliable + + ## Implementation Changes + + - Fix the CallSelectAnswer rejection lookup bug in CallManager::handleEvent(const RoomEvent&) by changing the std::find(begin, begin, ...) calls to search through + end(). This is a concrete logic bug that currently prevents expected branch behavior. + - Add strict call-id and state guards across inbound handlers in CallManager and log every early-return branch that currently drops events silently. Include at least: + - mismatched call_id + - wrong local webrtc::State + - duplicate answers/select-answer/reject + - self-echo cases + - candidates received before a call is initialized + - Implement WebRTCSession::acceptNegotiation(const std::string &) instead of the current unconditional false. Use the same parsing/validation style as acceptOffer: + - require an active session, not DISCONNECTED + - parse SDP as a remote offer + - validate required audio media and optional VP8 video media + - update remote video direction flags + - call set-remote-description + - create and send an answer through the existing answerCreated path + - keep wire format unchanged; do not add local renegotiation initiation in this pass + - Extend CallManager::handleEvent(const RoomEvent&) so renegotiation only applies to the active call, logs dropped mismatches, and drains queued ICE candidates only + after renegotiation is accepted. + - Harden acceptICECandidates handling: + - log when candidates are ignored because the session is not ready + - keep buffering only in manager-side inbound invite/renegotiation waiting states + - avoid silent no-ops when candidates arrive for the wrong call or after teardown + - Improve failure handling around setup paths: + - log TURN retrieval failures in retrieveTurnServer() with the request error and retry interval + - log why createOffer, acceptOffer, acceptAnswer, and renegotiation fail, including current state and call id where available + - log pipeline creation failures with the missing device/element context instead of only generic “Problem setting up call” + - Add non-critical exception logging around call orchestration paths that touch cache/state but should not kill the process: + - syncEvent dispatch + - inbound call event handlers that query room info/members + - outbound invite setup that reads room/member data + - TURN response parsing/conversion + Catch std::exception and log with nhlog at warn or error depending on whether the call can continue; rethrow nothing for these paths. Add a final catch (...) only where needed + to prevent silent termination, with a clear “unknown exception” log line. + - Keep user notifications mostly unchanged. Prefer logs for diagnostics and only surface UI notifications for existing fatal outcomes such as setup failure, ICE failure, reject, + timeout, or answered elsewhere. + + ## Public Interfaces / Behavior + + - No QML property or signal changes. + - No Matrix event schema changes. + - Existing CallAnswer, CallCandidates, CallNegotiate, and CallSelectAnswer messages remain the transport surface. + - Behavioral change: inbound m.call.negotiate will now be handled instead of always failing. + - Behavioral change: previously silent dropped call events and recoverable exceptions will now emit logs with call id, party id, and local state. + + ## Test Plan + + - Add focused unit-style coverage where practical for pure call-manager logic, or otherwise add regression tests around isolated helper logic if the repo lacks VoIP integration + tests. + - Verify manually or with targeted harness coverage: + - outbound call reaches OFFERSENT, receives answer, transitions to CONNECTED + - inbound call invite buffers ICE before accept, then drains after accept + - duplicate CallAnswer and CallSelectAnswer are ignored with logs, not state corruption + - CallSelectAnswer rejection-party matching now works after the std::find fix + - incoming CallNegotiate on the active call produces a local answer instead of immediate failure + - CallNegotiate for a different call_id is ignored with a diagnostic log + - TURN retrieval failure logs retry details without crashing + - exceptions thrown from room/member lookup in call paths are logged and do not abort the process + - Run a build check after the changes. If the environment still lacks cmake, document that verification gap and at minimum run the strongest available static inspection. + + ## Assumptions + + - Use existing nhlog logging categories rather than raw printf; “prints” here means persistent diagnostic logging. + - Keep the existing user-facing notification policy unless a failure is already fatal to the active call. + - Renegotiation scope is inbound-only for now because the current codebase already receives m.call.negotiate but does not expose a local renegotiation initiator. + - No attempt will be made in this pass to redesign the entire call state machine or add automatic media-session recovery after teardown. diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 00000000..4cdbba71 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,65 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1773179136, + "narHash": "sha256-aOxRGdpITEvasogdZXGqlSrr8pJQ+QGiRxpzmT3i/UQ=", + "owner": "cachix", + "repo": "devenv", + "rev": "169c048513ca064c02a4233ace0ecbdd935d4f80", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "nixpkgs": { + "inputs": { + "nixpkgs-src": "nixpkgs-src" + }, + "locked": { + "lastModified": 1772749504, + "narHash": "sha256-eqtQIz0alxkQPym+Zh/33gdDjkkch9o6eHnMPnXFXN0=", + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "08543693199362c1fddb8f52126030d0d374ba2e", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-src": { + "flake": false, + "locked": { + "lastModified": 1772173633, + "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} \ No newline at end of file diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 00000000..dfb4d0f9 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,129 @@ +{ pkgs, lib, config, inputs, ... }: + +let + qtPackages = with pkgs.qt6Packages; [ + qtbase + qtdeclarative + qtimageformats + qtkeychain + qtmultimedia + qtsvg + qttools + qtwayland + qt-jdenticon + ]; + + gstreamerPackages = [ + pkgs.gst_all_1.gstreamer + pkgs.gst_all_1.gst-plugins-base + (pkgs.gst_all_1.gst-plugins-good.override { qt6Support = true; }) + pkgs.gst_all_1.gst-plugins-bad + ]; + + cmakePackages = [ + pkgs.cmark + pkgs.coeurl + pkgs.curl + pkgs.kdsingleapplication + pkgs.libevent + pkgs.libsecret + pkgs.lmdb + pkgs.lmdbxx + pkgs.mtxclient + pkgs.nlohmann_json + pkgs.olm + pkgs.re2 + pkgs.spdlog + ] ++ qtPackages ++ gstreamerPackages ++ [ pkgs.libnice ]; + + runtimePackages = [ + pkgs.pipewire + ]; + + qtPluginPath = lib.makeSearchPath "lib/qt-6/plugins" qtPackages; + qmlImportPath = lib.makeSearchPath "lib/qt-6/qml" qtPackages; + gstreamerPluginPath = lib.makeSearchPath "lib/gstreamer-1.0" gstreamerPackages; + runtimeLibraryPath = lib.makeLibraryPath runtimePackages; + cmakePrefixPath = + lib.concatStringsSep ":" ( + (map (pkg: "${lib.getDev pkg}/lib/cmake") cmakePackages) + ++ (map (pkg: "${lib.getDev pkg}") cmakePackages) + ); +in +{ + packages = [ + pkgs.asciidoc + pkgs.cmake + pkgs.direnv + pkgs.gcc + pkgs.git + pkgs.gnumake + pkgs.gdb + pkgs.ninja + pkgs.pkg-config + pkgs.pipewire + pkgs.qt6Packages.wrapQtAppsHook + ] ++ cmakePackages; + + env = { + CMAKE_GENERATOR = "Ninja"; + CMAKE_PREFIX_PATH = cmakePrefixPath; + GST_PLUGIN_SYSTEM_PATH_1_0 = gstreamerPluginPath; + LD_LIBRARY_PATH = runtimeLibraryPath; + NHEKO_BUILD_DIR = "build"; + QT_PLUGIN_PATH = qtPluginPath; + QML2_IMPORT_PATH = qmlImportPath; + }; + + scripts."configure-nheko".exec = '' + cmake -S . -B "$NHEKO_BUILD_DIR" \ + -G "$CMAKE_GENERATOR" \ + -DCMAKE_BUILD_TYPE="''${CMAKE_BUILD_TYPE:-RelWithDebInfo}" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + ''; + + scripts."build-nheko".exec = '' + if [ ! -f "$NHEKO_BUILD_DIR/CMakeCache.txt" ]; then + configure-nheko + fi + + cmake --build "$NHEKO_BUILD_DIR" --parallel "''${NIX_BUILD_CORES:-$(nproc)}" + ''; + + scripts."test-nheko".exec = '' + build-nheko + ctest --test-dir "$NHEKO_BUILD_DIR" --output-on-failure + + binary="$(find "$NHEKO_BUILD_DIR" -maxdepth 4 -type f -name nheko -executable | head -n 1)" + if [ -z "$binary" ]; then + echo "Unable to locate the built nheko binary under $NHEKO_BUILD_DIR" >&2 + exit 1 + fi + + QT_QPA_PLATFORM=offscreen "$binary" --help >/dev/null + ''; + + scripts."run-nheko".exec = '' + build-nheko + + binary="$(find "$NHEKO_BUILD_DIR" -maxdepth 4 -type f -name nheko -executable | head -n 1)" + if [ -z "$binary" ]; then + echo "Unable to locate the built nheko binary under $NHEKO_BUILD_DIR" >&2 + exit 1 + fi + + exec "$binary" "$@" + ''; + + enterShell = '' + echo "nheko devenv ready" + echo " configure-nheko Configure the CMake build tree" + echo " build-nheko Build the nheko binary" + echo " test-nheko Run ctest and a headless --help smoke test" + echo " run-nheko Launch the built client" + ''; + + enterTest = '' + test-nheko + ''; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 00000000..965f695f --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + +nixpkgs: + permittedInsecurePackages: + - "olm-3.2.16" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend