PyAgoraRTC 0.2.1__tar.gz → 0.2.2__tar.gz
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.
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.github/workflows/release.yml +3 -1
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/PKG-INFO +1 -1
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/decisions.md +19 -1
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/protocol.md +5 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/session/session.py +18 -6
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyproject.toml +1 -1
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_session_join.py +22 -1
- pyagorartc-0.2.2/tests/unit/session/test_session_join_race.py +95 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/uv.lock +1 -1
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.claude/agents/code-reviewer.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.claude/agents/test-reviewer.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.github/workflows/ci.yml +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.gitignore +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.pre-commit-config.yaml +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/.python-version +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/CLAUDE.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/CONSTITUTION.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/CONTRIBUTING.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/LICENSE +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/README.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/analysis/divergence.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/analysis/legacy/README.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/analysis/legacy/agora_api.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/analysis/legacy/agora_sdp.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/analysis/legacy/agora_websockets.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/architecture.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/backlog.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/code_style.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/migration.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/open_questions.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/docs/testing.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/ap/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/ap/client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/ap/password.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/ap/response.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/const.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/exceptions.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/models.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/py.typed +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/rtm/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/rtm/client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/sdp/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/sdp/answer.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/sdp/candidates.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/sdp/offer.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/session/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/session/messages.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/session/recovery.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/pyagorartc/session/transport.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/_helpers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/conftest.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/__main__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/_common.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/ap.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/gateway.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/rtm.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/scheduler.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/server.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fakegateway/state.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/README.md +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_all_failed.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_gateway_failed.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_irregular.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_one_failed.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_request_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/choose_server_response.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/ap/update_ticket_response.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/error.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_failed.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_failed_code_only.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_ok.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_ok_no_ortc.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_ok_rtx.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_v3_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/join_v3_inputs.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/leave_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/non_object_message.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_h265_pt0.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_minimal.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_no_ssrc.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_notification_quit.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_notification_warn.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_p2p_lost.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_p2p_lost_top_level.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_p2p_lost_top_level_only.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_p2p_ok.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_rtp_capability_change.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_rtp_capability_change_irregular.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_token_privilege_did_expire.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_token_privilege_will_expire.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_user_offline.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_user_offline_no_uid.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_user_online.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/ping_back.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/ping_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/renew_token_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/set_client_role_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/subscribe_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/unknown_event.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/unsubscribe_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_delivered.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_failed.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_failed_sent.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_mixed_case.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_offline.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/ack_sent.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/rtm/peer_message_request_expected.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/chrome_answer_shipped.sdp +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/chrome_offer_ortc_shipped.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/chrome_recvonly_offer.sdp +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/gateway_join_ortc.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/gateway_ortc_vp8.json +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/go2rtc_offer.sdp +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/sdp/whep_trickle_fragment.sdpfrag +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/_helpers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/conftest.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_ap_client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_ap.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_control.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_gateway_events.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_gateway_join.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_rtm.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_rtm_client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_session_endings.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_session_secrets.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_session_streams.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_session_timers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_transport.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/meta/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/meta/_helpers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/meta/test_conventions.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/meta/test_guards.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/regression/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/_fakes.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/ap/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/ap/test_client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/ap/test_password.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/ap/test_response.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/conftest.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/rtm/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/rtm/test_client.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/sdp/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/sdp/_helpers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/sdp/test_answer.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/sdp/test_candidates.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/sdp/test_offer.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/__init__.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/_helpers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_messages.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_recovery.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_session.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_session_recovery.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_session_streams.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_session_teardown.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_session_timers.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/session/test_transport.py +0 -0
- {pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/unit/test_models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: PyAgoraRTC
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Async Python client for Agora RTC signalling: edge discovery, SDP/ORTC negotiation and the WebSocket gateway session
|
|
5
5
|
Project-URL: Homepage, https://github.com/mikey0000/PyAgoraRTC
|
|
6
6
|
Project-URL: Documentation, https://github.com/mikey0000/PyAgoraRTC/tree/main/docs
|
|
@@ -191,7 +191,7 @@ socket and tasks and fires `on_closed(JOIN_FAILED)` before raising (a socket
|
|
|
191
191
|
that closes after the join result but before `join()` acts on it is a failed
|
|
192
192
|
join, `GatewayConnectError`, not a silently dead joined session); the
|
|
193
193
|
gateway's quit, a socket close, the deadline and (with D22) `p2p_lost` fire
|
|
194
|
-
their reasons. `close()` is idempotent and safe after a failed join. A
|
|
194
|
+
their reasons, including when they land while `join()` is in flight (D29). `close()` is idempotent and safe after a failed join. A
|
|
195
195
|
candidate added after `join()` is ignored with a DEBUG line (Q4).
|
|
196
196
|
|
|
197
197
|
## D24. `parse_offer` restores what `sdp_transform` coerces (amends D3)
|
|
@@ -250,3 +250,21 @@ defaults to `True`, preserving the Mammotion rule; PetKit passes `False`,
|
|
|
250
250
|
which counts the announced uid as present so `on_user_offline` still tears
|
|
251
251
|
the subscription down. A held stream logs its uid at DEBUG. Q18 decides the
|
|
252
252
|
eventual default.
|
|
253
|
+
|
|
254
|
+
## D29. A quit or `p2p_lost` during `join()` fails the join with its own reason (amends D23)
|
|
255
|
+
|
|
256
|
+
The gateway sends the older session's 2003 quit when a second join on the
|
|
257
|
+
same uid lands, which can be right behind the older session's own join
|
|
258
|
+
result: the reader dispatches both before `join()` resumes. The handlers
|
|
259
|
+
used to act only on a joined session, so the quit was dropped and `join()`
|
|
260
|
+
answered for a session the gateway had already quit (a frozen feed in HA).
|
|
261
|
+
A quit, or `p2p_lost` with `end_on_p2p_lost`, that arrives before `join()`
|
|
262
|
+
has marked the session joined is recorded; `join()` then raises
|
|
263
|
+
`GatewayConnectError` and `on_closed` fires once with `GATEWAY_QUIT` /
|
|
264
|
+
`P2P_LOST`, not `JOIN_FAILED`: the join succeeded at the gateway and the
|
|
265
|
+
reason is what the host acts on. It is checked ahead of the socket-close
|
|
266
|
+
race, since the gateway closes the socket after the quit. One that arrives
|
|
267
|
+
before the result fails the join at once instead of waiting out the
|
|
268
|
+
timeout. `GatewayConnectError`, not `JoinRejectedError`: the join result was
|
|
269
|
+
a success, and `JoinRejectedError` carries a join-result code the gateway
|
|
270
|
+
never sent; this is the same shape as the socket-close race.
|
|
@@ -659,6 +659,11 @@ Negative case from the same test:
|
|
|
659
659
|
|
|
660
660
|
- **Mammotion uid sharing.** Every Mammotion stream token shares one viewer uid. A second camera joining the same
|
|
661
661
|
uid while the first is established gets the first one quit with code 2003.
|
|
662
|
+
- **Timing against the join result.** The fake gateway, and the HA log that prompted D29, deliver the older
|
|
663
|
+
session's quit right after its own join result, often in the same read. A client must treat a quit that arrives
|
|
664
|
+
before it has acted on its join result as ending that join, not drop it for "not joined yet". The timing is from
|
|
665
|
+
a real HA DEBUG log (quit at 11:52:58.467, answer built 8 ms later, no ending reported); the frames themselves
|
|
666
|
+
were not logged.
|
|
662
667
|
- **How the SDK handles it** (`handleNotification`, SDK:31150-31198): it maps `code` through the table below.
|
|
663
668
|
- Code 28 with `detail` is a recover notification.
|
|
664
669
|
- Code 30 is `K_VOS_FALLBACK`, whose `detail` is `"FALLBACKCN"` or `"fallback_hls"`.
|
|
@@ -124,6 +124,7 @@ class AgoraSession:
|
|
|
124
124
|
self._reader_stopped = False
|
|
125
125
|
self._join_started = False
|
|
126
126
|
self._joined = False
|
|
127
|
+
self._pending_end: CloseReason | None = None
|
|
127
128
|
self._own_uid = creds.uid
|
|
128
129
|
self._token = creds.token
|
|
129
130
|
self._rtx = False
|
|
@@ -193,8 +194,9 @@ class AgoraSession:
|
|
|
193
194
|
JoinTimeoutError: No join result within ``options.join_timeout_s``.
|
|
194
195
|
SdpError: The offer or the gateway ORTC could not be translated.
|
|
195
196
|
|
|
196
|
-
On any failure the socket is closed, owned tasks are cancelled and ``on_closed
|
|
197
|
-
|
|
197
|
+
On any failure the socket is closed, owned tasks are cancelled and ``on_closed`` fires before the
|
|
198
|
+
exception propagates: ``JOIN_FAILED``, or ``GATEWAY_QUIT`` / ``P2P_LOST`` when the gateway ended the
|
|
199
|
+
session before ``join`` finished, which raises ``GatewayConnectError`` (D9, D23, D29).
|
|
198
200
|
|
|
199
201
|
"""
|
|
200
202
|
if self._join_started or self._close_reason is not None:
|
|
@@ -203,7 +205,7 @@ class AgoraSession:
|
|
|
203
205
|
try:
|
|
204
206
|
return await self._join(offer_sdp, session_id)
|
|
205
207
|
except BaseException:
|
|
206
|
-
await self._end(CloseReason.JOIN_FAILED)
|
|
208
|
+
await self._end(self._pending_end or CloseReason.JOIN_FAILED)
|
|
207
209
|
raise
|
|
208
210
|
|
|
209
211
|
async def renew_token(self, token: str | None = None) -> None:
|
|
@@ -274,6 +276,9 @@ class AgoraSession:
|
|
|
274
276
|
self._join_waiter = None
|
|
275
277
|
timer.cancel()
|
|
276
278
|
join = parse_join_result(frame)
|
|
279
|
+
if self._pending_end is not None:
|
|
280
|
+
# Checked first: the gateway usually closes the socket right after the quit it announced.
|
|
281
|
+
raise GatewayConnectError(f"gateway ended the session during join ({self._pending_end.value})")
|
|
277
282
|
if self._reader_stopped:
|
|
278
283
|
# The result arrived, but the message loop stopped before this task resumed to act on it.
|
|
279
284
|
raise GatewayConnectError("gateway socket closed before the join completed")
|
|
@@ -486,8 +491,7 @@ class AgoraSession:
|
|
|
486
491
|
notification.code,
|
|
487
492
|
notification.detail,
|
|
488
493
|
)
|
|
489
|
-
|
|
490
|
-
await self._end(CloseReason.GATEWAY_QUIT)
|
|
494
|
+
await self._end_by_gateway(CloseReason.GATEWAY_QUIT)
|
|
491
495
|
|
|
492
496
|
async def _on_p2p_lost(self, frame: GatewayFrame) -> None:
|
|
493
497
|
lost = parse_p2p_lost(frame)
|
|
@@ -496,8 +500,16 @@ class AgoraSession:
|
|
|
496
500
|
_LOGGER.debug("Gateway reported p2p_lost (code %s, %s); ignored", lost.code, lost.error)
|
|
497
501
|
return
|
|
498
502
|
_LOGGER.warning("Gateway reported p2p_lost (code %s, %s); ending the session", lost.code, lost.error)
|
|
503
|
+
await self._end_by_gateway(CloseReason.P2P_LOST)
|
|
504
|
+
|
|
505
|
+
async def _end_by_gateway(self, reason: CloseReason) -> None:
|
|
506
|
+
"""End a joined session; while ``join`` is in flight, leave the ending for ``join`` to apply (D29)."""
|
|
499
507
|
if self._joined:
|
|
500
|
-
await self._end(
|
|
508
|
+
await self._end(reason)
|
|
509
|
+
return
|
|
510
|
+
self._pending_end = self._pending_end or reason
|
|
511
|
+
if (waiter := self._join_waiter) is not None and not waiter.done():
|
|
512
|
+
waiter.set_exception(GatewayConnectError(f"gateway ended the session during join ({reason.value})"))
|
|
501
513
|
|
|
502
514
|
async def _on_p2p_ok(self, frame: GatewayFrame) -> None:
|
|
503
515
|
ok = parse_p2p_ok(frame.message)
|
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
10
|
from pyagorartc.const import EDGE_DOMAIN_SUFFIX
|
|
11
|
-
from pyagorartc.exceptions import JoinRejectedError, JoinTimeoutError
|
|
11
|
+
from pyagorartc.exceptions import GatewayConnectError, JoinRejectedError, JoinTimeoutError
|
|
12
12
|
from pyagorartc.models import CloseReason, SessionOptions
|
|
13
13
|
from pyagorartc.sdp import extract_inline_candidates
|
|
14
14
|
from tests._helpers import RTC_TOKEN
|
|
@@ -165,3 +165,24 @@ class TestJoinFailure:
|
|
|
165
165
|
with pytest.raises(JoinTimeoutError):
|
|
166
166
|
await asyncio.wait_for(join, SESSION_TIMEOUT_S)
|
|
167
167
|
assert r.closed.calls == [CloseReason.JOIN_FAILED]
|
|
168
|
+
|
|
169
|
+
@pytest.mark.regression
|
|
170
|
+
async def test_a_repeat_join_landing_with_the_first_result_fails_the_first_join_as_a_gateway_quit(
|
|
171
|
+
self, fake_agora: FakeAgora, new_session: Callable[..., SessionRig]
|
|
172
|
+
) -> None:
|
|
173
|
+
"""The 2003 quit reaching the first session with its join result was dropped; its ``join`` then answered."""
|
|
174
|
+
fake_agora.control(join_delay_s=5.0)
|
|
175
|
+
first, second = new_session(), new_session()
|
|
176
|
+
first_join = asyncio.ensure_future(first.session.join(CHROME_OFFER, SESSION_ID))
|
|
177
|
+
second_join = asyncio.ensure_future(second.session.join(CHROME_OFFER, SESSION_ID))
|
|
178
|
+
# Each session's join timer and the fake's two held join results.
|
|
179
|
+
await first.sleepers(4)
|
|
180
|
+
|
|
181
|
+
# Both held results fall due together: the first socket gets its result, then the second join's 2003.
|
|
182
|
+
await first.advance(5.0)
|
|
183
|
+
|
|
184
|
+
with pytest.raises(GatewayConnectError):
|
|
185
|
+
await asyncio.wait_for(first_join, SESSION_TIMEOUT_S)
|
|
186
|
+
await asyncio.wait_for(second_join, SESSION_TIMEOUT_S)
|
|
187
|
+
assert first.closed.calls == [CloseReason.GATEWAY_QUIT]
|
|
188
|
+
assert (second.closed.calls, second.session.is_joined) == ([], True)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""An ending the gateway announces between the join result and ``join`` resuming (D23, D29)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from pyagorartc.exceptions import GatewayConnectError
|
|
11
|
+
from pyagorartc.models import CloseReason, SessionOptions
|
|
12
|
+
from tests._helpers import load_json_fixture
|
|
13
|
+
from tests.unit.session._helpers import OFFER, SESSION_ID, TIMEOUT, all_done, rig
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from tests.unit.session._helpers import Rig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def join_then(r: Rig, fixture: str) -> asyncio.Task[str]:
|
|
20
|
+
"""Start ``join`` and feed the result and ``fixture`` together, so the reader dispatches both before it resumes."""
|
|
21
|
+
task = asyncio.ensure_future(r.session.join(OFFER, SESSION_ID))
|
|
22
|
+
await r.sent(1)
|
|
23
|
+
r.conn.feed({**load_json_fixture("gateway/join_ok.json"), "_id": r.conn.sent[0]["_id"]})
|
|
24
|
+
r.conn.feed(load_json_fixture(fixture))
|
|
25
|
+
return task
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestEndingDuringJoin:
|
|
29
|
+
@pytest.mark.regression
|
|
30
|
+
async def test_a_quit_right_after_the_result_fails_the_join_as_a_gateway_quit(self) -> None:
|
|
31
|
+
"""A 2003 quit dispatched before ``join`` resumed was dropped, so ``join`` answered for a quit session."""
|
|
32
|
+
r = rig()
|
|
33
|
+
task = await join_then(r, "gateway/on_notification_quit.json")
|
|
34
|
+
|
|
35
|
+
with pytest.raises(GatewayConnectError, match="quit"):
|
|
36
|
+
await asyncio.wait_for(task, TIMEOUT)
|
|
37
|
+
assert r.closed.calls == [CloseReason.GATEWAY_QUIT]
|
|
38
|
+
assert not r.session.is_joined
|
|
39
|
+
assert all_done(r.spawned)
|
|
40
|
+
assert r.conn.sent_of_type("leave") == []
|
|
41
|
+
|
|
42
|
+
async def test_a_quit_then_a_socket_close_during_join_reports_the_quit(self) -> None:
|
|
43
|
+
"""The gateway closes the socket after quitting; the quit, not the close, is why the join failed."""
|
|
44
|
+
r = rig()
|
|
45
|
+
task = await join_then(r, "gateway/on_notification_quit.json")
|
|
46
|
+
await r.conn.close()
|
|
47
|
+
|
|
48
|
+
with pytest.raises(GatewayConnectError, match="quit"):
|
|
49
|
+
await asyncio.wait_for(task, TIMEOUT)
|
|
50
|
+
assert r.closed.calls == [CloseReason.GATEWAY_QUIT]
|
|
51
|
+
|
|
52
|
+
async def test_a_quit_during_join_is_reported_once_after_close(self) -> None:
|
|
53
|
+
r = rig()
|
|
54
|
+
task = await join_then(r, "gateway/on_notification_quit.json")
|
|
55
|
+
with pytest.raises(GatewayConnectError):
|
|
56
|
+
await asyncio.wait_for(task, TIMEOUT)
|
|
57
|
+
|
|
58
|
+
await r.session.close()
|
|
59
|
+
|
|
60
|
+
assert r.closed.calls == [CloseReason.GATEWAY_QUIT]
|
|
61
|
+
|
|
62
|
+
@pytest.mark.regression
|
|
63
|
+
async def test_p2p_lost_right_after_the_result_fails_the_join_when_asked(self) -> None:
|
|
64
|
+
"""With ``end_on_p2p_lost``, a p2p_lost dispatched before ``join`` resumed was dropped and the join succeeded."""
|
|
65
|
+
r = rig(SessionOptions(end_on_p2p_lost=True))
|
|
66
|
+
task = await join_then(r, "gateway/on_p2p_lost.json")
|
|
67
|
+
|
|
68
|
+
with pytest.raises(GatewayConnectError):
|
|
69
|
+
await asyncio.wait_for(task, TIMEOUT)
|
|
70
|
+
assert r.closed.calls == [CloseReason.P2P_LOST]
|
|
71
|
+
assert not r.session.is_joined
|
|
72
|
+
assert all_done(r.spawned)
|
|
73
|
+
assert r.conn.sent_of_type("leave") == []
|
|
74
|
+
|
|
75
|
+
async def test_p2p_lost_right_after_the_result_is_ignored_by_default(self) -> None:
|
|
76
|
+
r = rig()
|
|
77
|
+
task = await join_then(r, "gateway/on_p2p_lost.json")
|
|
78
|
+
|
|
79
|
+
answer = await asyncio.wait_for(task, TIMEOUT)
|
|
80
|
+
|
|
81
|
+
assert "a=setup:" in answer
|
|
82
|
+
assert (r.closed.calls, r.session.is_joined) == ([], True)
|
|
83
|
+
|
|
84
|
+
async def test_a_quit_before_the_result_fails_the_join_without_waiting_for_the_timeout(self) -> None:
|
|
85
|
+
"""The quit fails the pending join at once rather than leaving it to ``join_timeout_s``."""
|
|
86
|
+
r = rig()
|
|
87
|
+
task = asyncio.ensure_future(r.session.join(OFFER, SESSION_ID))
|
|
88
|
+
await r.sent(1)
|
|
89
|
+
|
|
90
|
+
r.conn.feed(load_json_fixture("gateway/on_notification_quit.json"))
|
|
91
|
+
|
|
92
|
+
with pytest.raises(GatewayConnectError, match="quit"):
|
|
93
|
+
await asyncio.wait_for(task, TIMEOUT)
|
|
94
|
+
assert r.closed.calls == [CloseReason.GATEWAY_QUIT]
|
|
95
|
+
assert all_done(r.spawned)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_h265_pt0.json
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_minimal.json
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_add_video_stream_no_ssrc.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_p2p_lost_top_level_only.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_rtp_capability_change_irregular.json
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_token_privilege_did_expire.json
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/fixtures/gateway/on_token_privilege_will_expire.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_control.py
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_gateway_events.py
RENAMED
|
File without changes
|
{pyagorartc-0.2.1 → pyagorartc-0.2.2}/tests/integration/test_fakegateway_contract_gateway_join.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|