From 79503740d0cb18ce94a767b3e7d7a835aa7ca75d Mon Sep 17 00:00:00 2001 From: "Ilya (Marshal)" Date: Wed, 30 Sep 2026 22:33:55 +0200 Subject: [PATCH] Settle set_parameters when libwebrtc drops the callback --- .../cpp/src/interfaces/rtc_rtp_sender.cpp | 50 ++++++++++++++++--- tests/test_rtp_sender_receiver.py | 11 ++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/python-webrtc/cpp/src/interfaces/rtc_rtp_sender.cpp b/python-webrtc/cpp/src/interfaces/rtc_rtp_sender.cpp index 709a8f0..d9bc00b 100644 --- a/python-webrtc/cpp/src/interfaces/rtc_rtp_sender.cpp +++ b/python-webrtc/cpp/src/interfaces/rtc_rtp_sender.cpp @@ -8,6 +8,10 @@ #include "rtc_rtp_sender.h" #include "../utils/python_callback.h" +#include +#include +#include + #include #include @@ -17,6 +21,43 @@ namespace python_webrtc { + namespace { + + // libwebrtc drops the callback of SetParametersAsync without calling it when the sender has no media channel + // (like after a rollback of its offer) or loses it meanwhile: rejects like it does once the channel is gone + class SetParametersCompletion final { + public: + SetParametersCompletion(std::function onSuccess, std::function onFailure) + : _onSuccess(std::move(onSuccess)), _onFailure(std::move(onFailure)) {} + SetParametersCompletion(const SetParametersCompletion &) = delete; + SetParametersCompletion(SetParametersCompletion &&) = delete; + SetParametersCompletion &operator=(const SetParametersCompletion &) = delete; + SetParametersCompletion &operator=(SetParametersCompletion &&) = delete; + + ~SetParametersCompletion() { + if (_onFailure) { + _onFailure(RTCCallbackException(webrtc::RTCErrorType::INVALID_STATE, + "The sender was detached before the parameters were set")); + } + } + + void operator()(webrtc::RTCError error) { + auto onSuccess = std::exchange(_onSuccess, nullptr); + auto onFailure = std::exchange(_onFailure, nullptr); + if (error.ok()) { + onSuccess(); + } else { + onFailure(RTCCallbackException(std::move(error))); + } + } + + private: + std::function _onSuccess; + std::function _onFailure; + }; + + } // namespace + RTCRtpSender::RTCRtpSender(std::shared_ptr factory, webrtc::scoped_refptr sender) : _factory(std::move(factory)), _sender(std::move(sender)) {} @@ -184,13 +225,8 @@ namespace python_webrtc { current.transaction_id = fresh.transaction_id; // read-only: getParameters() shows the negotiated codecs this side can send, libwebrtc takes its own list current.codecs = fresh.codecs; - _sender->SetParametersAsync(current, [onSuccess, onFailure](webrtc::RTCError error) { - if (error.ok()) { - onSuccess(); - } else { - onFailure(RTCCallbackException(std::move(error))); - } - }); + _sender->SetParametersAsync(current, [completion = std::make_unique(onSuccess, onFailure)]( + webrtc::RTCError error) { (*completion)(std::move(error)); }); } bool RTCRtpSender::ReplaceTrack(std::optional> track) { diff --git a/tests/test_rtp_sender_receiver.py b/tests/test_rtp_sender_receiver.py index 6fd1b1f..c7d9a78 100644 --- a/tests/test_rtp_sender_receiver.py +++ b/tests/test_rtp_sender_receiver.py @@ -7,6 +7,7 @@ """Senders and receivers: capabilities, parameters, codecs, tracks, DTMF and synchronization sources.""" +import asyncio import time import pytest @@ -187,6 +188,16 @@ async def test_set_parameters_key_frames(caller, callee): await sender.set_parameters(sender.get_parameters(), key_frames=[True]) +@pytest.mark.asyncio +async def test_set_parameters_after_rollback(pc): + """A sender rolled back out of its offer has no media channel: setting parameters rejects, not hangs""" + sender = pc.add_transceiver(webrtc.MediaType.video).sender + await pc.set_local_description() + await pc.set_local_description({'type': 'rollback'}) + with pytest.raises(webrtc.InvalidStateError): + await asyncio.wait_for(sender.set_parameters(sender.get_parameters()), 5) + + @pytest.mark.asyncio async def test_simulcast_receiver_parameters(caller, callee): """The receiver of simulcast has the negotiated codecs and header extensions"""