python-aidot-cameras 1.0.0rc7__tar.gz → 1.0.0rc8__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.
Files changed (287) hide show
  1. {python_aidot_cameras-1.0.0rc7/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc8}/PKG-INFO +2 -1
  2. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/README.md +1 -0
  3. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sdes_open.py +284 -102
  4. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/webrtc_open.py +45 -0
  5. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/pyproject.toml +1 -1
  6. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
  7. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/SOURCES.txt +2 -1
  8. python_aidot_cameras-1.0.0rc8/tests/test_nominates_as_soon_as_the_answer_lands.py +281 -0
  9. python_aidot_cameras-1.0.0rc8/tests/test_sdes_echo_wait_is_not_dead_time.py +96 -0
  10. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_turn_create_permission.py +17 -11
  11. python_aidot_cameras-1.0.0rc7/tests/test_sdes_echo_wait_timeout.py +0 -20
  12. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/LICENSE +0 -0
  13. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/MANIFEST.in +0 -0
  14. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/__main__.py +0 -0
  16. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_upstream.py +0 -0
  17. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  19. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  20. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  21. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  22. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  23. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  24. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  25. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  26. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  27. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  28. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  29. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  30. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  31. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  32. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  33. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  34. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  35. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  36. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  37. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  38. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  39. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  40. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  41. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  42. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  43. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  44. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  45. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  46. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  47. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  48. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  49. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  50. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  51. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/ble_gateway.py +0 -0
  52. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/__init__.py +0 -0
  53. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/client.py +0 -0
  54. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/constants.py +0 -0
  55. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/controls.py +0 -0
  56. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/go2rtc.py +0 -0
  57. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/hwaccel.py +0 -0
  58. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/lan_control.py +0 -0
  59. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/models.py +0 -0
  60. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/playback.py +0 -0
  61. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/protocol.py +0 -0
  62. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_events.py +0 -0
  63. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_listing.py +0 -0
  64. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sd_playback.py +0 -0
  65. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/sdes.py +0 -0
  66. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/tutk.py +0 -0
  67. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/camera/webrtc.py +0 -0
  68. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/client.py +0 -0
  69. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/cloud_auth.py +0 -0
  70. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/const.py +0 -0
  71. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/credentials.py +0 -0
  72. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/crypto.py +0 -0
  73. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/device_client.py +0 -0
  74. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/discover.py +0 -0
  75. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/exceptions.py +0 -0
  76. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/g711.py +0 -0
  77. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/lan_retry.py +0 -0
  78. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/aidot_cameras/py.typed +0 -0
  79. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  80. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  81. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/requires.txt +0 -0
  82. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  83. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/setup.cfg +0 -0
  84. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_aioice_compat.py +0 -0
  85. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_alarm_event.py +0 -0
  86. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_pt_kinds.py +0 -0
  87. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_sdp_video_pt.py +0 -0
  88. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_answer_section_selection.py +0 -0
  89. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_audio_mux_offline.py +0 -0
  90. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  91. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_request.py +0 -0
  92. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_response_parse.py +0 -0
  93. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_avio_response_router.py +0 -0
  94. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_backoff.py +0 -0
  95. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_detection.py +0 -0
  96. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_evidence_corroboration.py +0 -0
  97. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_skip_turn.py +0 -0
  98. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  99. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_battery_wake_gate_app_parity.py +0 -0
  100. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_bitrate_campaign_arms.py +0 -0
  101. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ble_gateway.py +0 -0
  102. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_bridge_state_sentinels.py +0 -0
  103. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_busy_backoff.py +0 -0
  104. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_camera_properties.py +0 -0
  105. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_canary_log_names_the_camera.py +0 -0
  106. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_cloud_count.py +0 -0
  107. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_cloud_plan.py +0 -0
  108. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_constants_imports_resolve.py +0 -0
  109. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_control_verdicts.py +0 -0
  110. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_deferred_hardening.py +0 -0
  111. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_cmd_seq_match.py +0 -0
  112. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_login_guard.py +0 -0
  113. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_device_user_info_cache.py +0 -0
  114. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_direct_ts_keyframe_signal.py +0 -0
  115. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_direct_ts_serve.py +0 -0
  116. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dormant_camera_cleanup.py +0 -0
  117. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_drain_is_cancellable.py +0 -0
  118. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_mux_terminal_write.py +0 -0
  119. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_not_ready_burst.py +0 -0
  120. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_pinning.py +0 -0
  121. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_resolution_ack.py +0 -0
  122. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_decode_skip.py +0 -0
  123. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_ice_wait.py +0 -0
  124. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_serve_open_timeout.py +0 -0
  125. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_skip_signaling_wait.py +0 -0
  126. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_slow_probe.py +0 -0
  127. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_stdout_producer.py +0 -0
  128. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_dtls_video_presence.py +0 -0
  129. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_egress_guard.py +0 -0
  130. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_env_seams.py +0 -0
  131. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  132. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  133. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_futile_keepalive.py +0 -0
  134. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  135. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_go2rtc.py +0 -0
  136. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_go2rtc_cli.py +0 -0
  137. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_h264_decode_rate_limit.py +0 -0
  138. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_highport_nomination.py +0 -0
  139. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_hwaccel_probe.py +0 -0
  140. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ice_config_cache.py +0 -0
  141. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ice_peer_reflexive.py +0 -0
  142. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_idle_release_both_loops.py +0 -0
  143. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_keepalive_renew.py +0 -0
  144. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_key_restart_sdp.py +0 -0
  145. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_keyframe_prompter.py +0 -0
  146. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_lan_control.py +0 -0
  147. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  148. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_late_repeats_are_dropped.py +0 -0
  149. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_light_active_color_mode.py +0 -0
  150. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_session_carries_talk.py +0 -0
  151. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_stream_param.py +0 -0
  152. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_validate_policy.py +0 -0
  153. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_live_video_canary.py +0 -0
  154. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_liveplay_echo_timeout.py +0 -0
  155. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_local_only_controls.py +0 -0
  156. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_logging_caps.py +0 -0
  157. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_login_connect_deadline.py +0 -0
  158. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_login_reconnect_backoff.py +0 -0
  159. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_counted_only_when_plaintext.py +0 -0
  160. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_path_is_reported.py +0 -0
  161. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_socket_rcvbuf.py +0 -0
  162. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_media_wait_floor.py +0 -0
  163. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_motion_poll.py +0 -0
  164. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_client_id_is_exact.py +0 -0
  165. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_credential_refetch.py +0 -0
  166. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_credential_self_heal.py +0 -0
  167. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_password_not_persisted.py +0 -0
  168. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_session_reconnect.py +0 -0
  169. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  170. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_nack.py +0 -0
  171. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  172. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_narrow_pc_ice.py +0 -0
  173. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_key_material_in_logs.py +0 -0
  174. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_self_referential_go2rtc.py +0 -0
  175. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_no_undefined_names.py +0 -0
  176. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_off_subnet_candidate.py +0 -0
  177. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offer_bandwidth_line.py +0 -0
  178. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  179. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_offline_keepalive_pause.py +0 -0
  180. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_on_frame_is_an_av_frame.py +0 -0
  181. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_open_fail_logger.py +0 -0
  182. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_open_gate_delay.py +0 -0
  183. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_peer_id_client_type.py +0 -0
  184. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_peerid_reuse.py +0 -0
  185. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_persistent_mqtt.py +0 -0
  186. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_persistent_mqtt_robustness.py +0 -0
  187. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_playback_tls.py +0 -0
  188. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_post_merge_hardening.py +0 -0
  189. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  190. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_public_signature_matches_impl.py +0 -0
  191. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_publish_target_readiness.py +0 -0
  192. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_quality_campaign_in_session.py +0 -0
  193. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_reboot_action.py +0 -0
  194. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_recent_recordings.py +0 -0
  195. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_remb.py +0 -0
  196. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  197. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_resent_frames_are_dropped.py +0 -0
  198. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_resolution_persists.py +0 -0
  199. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_restart_cycle.py +0 -0
  200. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_retry_policy.py +0 -0
  201. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_scripts_importable.py +0 -0
  202. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_init_ack_tsn.py +0 -0
  203. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_reassembly.py +0 -0
  204. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sctp_sack.py +0 -0
  205. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_card_presence.py +0 -0
  206. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_event_decode.py +0 -0
  207. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_event_probe_payloads.py +0 -0
  208. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_listing.py +0 -0
  209. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sd_playback.py +0 -0
  210. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_adaptive.py +0 -0
  211. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_avio_dispatch.py +0 -0
  212. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_bridge_break_gate.py +0 -0
  213. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_connection_mode.py +0 -0
  214. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_fast_liveplay.py +0 -0
  215. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  216. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_first_media_stall_report.py +0 -0
  217. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_ice_teardown.py +0 -0
  218. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_idle_release.py +0 -0
  219. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_nack_bridge.py +0 -0
  220. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_offer_video_order.py +0 -0
  221. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_offer_video_pin.py +0 -0
  222. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_open_cleanup.py +0 -0
  223. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_sctp_abort.py +0 -0
  224. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_audio.py +0 -0
  225. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_cmd.py +0 -0
  226. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_serve_stderr_drain.py +0 -0
  227. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_session_cap.py +0 -0
  228. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_sprop.py +0 -0
  229. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_talk.py +0 -0
  230. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_teardown_exit_log.py +0 -0
  231. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_terminal_ack.py +0 -0
  232. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_terminal_ack_latency.py +0 -0
  233. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_tmmbr_bridge.py +0 -0
  234. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdes_watchdog.py +0 -0
  235. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdp_narrowing_properties.py +0 -0
  236. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sdp_payload_type_narrowing.py +0 -0
  237. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_security_hardening.py +0 -0
  238. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  239. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serializable_login_info.py +0 -0
  240. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_input_timeout.py +0 -0
  241. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_loss_tolerance.py +0 -0
  242. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_pt_respects_the_pin.py +0 -0
  243. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_relay.py +0 -0
  244. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  245. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_session_stats.py +0 -0
  246. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_shared_house_seam.py +0 -0
  247. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  248. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_snapshot_cancellation.py +0 -0
  249. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_speak.py +0 -0
  250. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_capture_gate.py +0 -0
  251. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_staleness.py +0 -0
  252. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_unstable_public.py +0 -0
  253. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  254. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_srtp_rx_rekey.py +0 -0
  255. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_srtp_tx_key_note.py +0 -0
  256. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stable_terminal_id.py +0 -0
  257. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_nudge_policy.py +0 -0
  258. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  259. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  260. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  261. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  262. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  263. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_cap.py +0 -0
  264. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_drain_ownership.py +0 -0
  265. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_idle.py +0 -0
  266. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_slot_cap.py +0 -0
  267. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_stream_teardown.py +0 -0
  268. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_talk.py +0 -0
  269. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  270. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tap_to_mux_end_to_end.py +0 -0
  271. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_terminal_ack.py +0 -0
  272. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tmmbr.py +0 -0
  273. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_token_file_robustness.py +0 -0
  274. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_token_refresh.py +0 -0
  275. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  276. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_upstream_compat.py +0 -0
  277. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_validator_shares_one_token.py +0 -0
  278. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_profile_log.py +0 -0
  279. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_pts_dts.py +0 -0
  280. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_video_ts_unwrap.py +0 -0
  281. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_check_throttle.py +0 -0
  282. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_presence.py +0 -0
  283. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_viewer_present_push_mode.py +0 -0
  284. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wait_or_event.py +0 -0
  285. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wake_gate_camera_evidence.py +0 -0
  286. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_wake_readiness_retry.py +0 -0
  287. {python_aidot_cameras-1.0.0rc7 → python_aidot_cameras-1.0.0rc8}/tests/test_webrtc_answer_race.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0rc7
3
+ Version: 1.0.0rc8
4
4
  Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
5
5
  Author-email: cbrightly <chris.brightly@gmail.com>
6
6
  License-Expression: MIT
@@ -572,6 +572,7 @@ audio, idle release, the sprop cache path) are documented in
572
572
  | `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
573
573
  | `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
574
574
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
575
+ | `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway. Shortened rather than removed, so a broker that does echo still short-circuits it and gets its `webrtcResp`. Measured on an A001064: time to first media 4195 ms -> 2489 ms. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. **Caveat:** the 17/17 evidence is one fleet, and an echo landing between 0.25 s and 2.0 s would now miss the wait - that flag also selects whether the `webrtcResp` is built, whether the ICE window runs 20 s or 2.5 s, and whether the quickConn reconnect retry is armed. A late echo is logged, so set this to `2.0` if you see one. | `0.25` |
575
576
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
576
577
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
577
578
  | `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
@@ -532,6 +532,7 @@ audio, idle release, the sprop cache path) are documented in
532
532
  | `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
533
533
  | `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
534
534
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
535
+ | `AIDOT_SDES_ECHO_WAIT_S` | How long to wait for the broker to echo our own `webrtcReq` back before carrying on, in seconds, for the role-reversal models that build a `webrtcResp` from that echo (A001513-class cameras never took this wait and are unaffected). Like `AIDOT_SDES_LIVEPLAY_ECHO_S` it was pure latency: across 18 h of one deployment, of 61 SDES opens the 17 that took the wait timed out 17 times out of 17 at a mean of 2.086 s, the `webrtcResp` it exists to build was never sent once, and all 17 streamed anyway. Shortened rather than removed, so a broker that does echo still short-circuits it and gets its `webrtcResp`. Measured on an A001064: time to first media 4195 ms -> 2489 ms. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. **Caveat:** the 17/17 evidence is one fleet, and an echo landing between 0.25 s and 2.0 s would now miss the wait - that flag also selects whether the `webrtcResp` is built, whether the ICE window runs 20 s or 2.5 s, and whether the quickConn reconnect retry is armed. A late echo is logged, so set this to `2.0` if you see one. | `0.25` |
535
536
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
536
537
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
537
538
  | `AIDOT_SDES_OFFER_BANDWIDTH_KBPS` | Add a `b=AS:<kbps>` receive-bandwidth ceiling (RFC 4566) to the video section of the SDES offer. **Measured to do nothing** on an A001064 over ten sessions; kept, off, alongside `AIDOT_REMB_TARGET_BPS` for the same reason. Run as a strict ABAB campaign it appeared to give a decisive 2.1x reduction - that was a codec split, not the knob. | unset (no `b=` line) |
@@ -12,6 +12,7 @@ import json
12
12
  import logging
13
13
  import os
14
14
  import random
15
+ import re
15
16
  import time
16
17
  from typing import Callable, Optional # noqa: F401 - method annotations
17
18
 
@@ -1110,6 +1111,133 @@ _PRE_LAUNCH_ANSWER_WAIT_S = 8.0
1110
1111
  _AUDIO_PT_GRACE_S = 1.0
1111
1112
 
1112
1113
 
1114
+ # --------------------------------------------------------------------------- #
1115
+ # Reading the camera's answer, and leaving the STUN window once it is readable
1116
+ # --------------------------------------------------------------------------- #
1117
+ #
1118
+ # The ICE responder window below runs synchronously and blocks the event loop,
1119
+ # so it was written on the assumption stated above the answer harvest: that it
1120
+ # "closes ~2.4 s before the camera answers". Cutting the livePlayReq echo wait
1121
+ # from 5.0 s to 0.25 s inverted that. Measured on a cold A001064 open:
1122
+ #
1123
+ # +0.45 s webrtcReq
1124
+ # +0.72 s the camera's webrtcResp answer <- credentials in hand
1125
+ # +5.05 s window closes on 1.5 s of ICE silence
1126
+ # +5.06 s USE-CANDIDATE
1127
+ # +6.66 s first SRTP
1128
+ #
1129
+ # 4.3 s of a 6.7 s connect spent holding an answer nothing was looking at.
1130
+ # Leaving early costs nothing: the bridge thread takes over responding to the
1131
+ # camera's binding requests the moment the window closes, and it is the bridge -
1132
+ # not the window - that learns peer-reflexive candidates and re-nominates every
1133
+ # 2.5 s, so the relay-only and off-subnet recovery paths are untouched.
1134
+ # Two of the five parses this replaced ended at a bare ``typ`` and captured no
1135
+ # type token, and one accepted a line with no ``a=`` prefix (trickled candidates
1136
+ # arrive bare). Both shapes are accepted so the collapse cannot reject a line
1137
+ # one of the old copies would have taken.
1138
+ #
1139
+ # The type token is optional but still anchored: ``typ ?(\w*)`` also matched
1140
+ # ``typo host`` and ``typhoon``, which every parse it replaced rejected, and a
1141
+ # corrupt candidate line reaching the nomination gets a TURN permission and a
1142
+ # USE-CANDIDATE probe. ``(?!\w)`` keeps the widening to the one direction the
1143
+ # collapse needs.
1144
+ _ICE_CAND_RE = re.compile(
1145
+ r"(?:a=)?candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ(?: (\w+))?(?!\w)"
1146
+ )
1147
+
1148
+
1149
+ def _parse_candidate_line(line):
1150
+ """One ``a=candidate:`` line as ``(ip, port, typ)``, or None.
1151
+
1152
+ Shared with the trickle path, which reads candidates one at a time off
1153
+ ``iceCandidateReq`` rather than out of an SDP, and which kept its own copy
1154
+ of this pattern until the two could disagree about the same line.
1155
+ """
1156
+ m = _ICE_CAND_RE.match((line or "").strip())
1157
+ if not m:
1158
+ return None
1159
+ return m.group(1), int(m.group(2)), (m.group(3) or "")
1160
+
1161
+
1162
+ def _parse_answer_ice(sdp):
1163
+ """Pull ICE credentials and UDP candidates out of a camera's answer SDP.
1164
+
1165
+ Returns ``(ufrag, pwd, candidates, host)``: the first ice-ufrag and
1166
+ ice-pwd in the SDP, every udp candidate as an ``(ip, port)`` tuple in the
1167
+ order offered, and the first ``typ host`` candidate - which is the one SCTP
1168
+ is addressed to, and is not necessarily the first candidate listed.
1169
+
1170
+ Taking the FIRST credential pair is an observation, not a contract: our own
1171
+ offer carries distinct per-media credentials, and the nomination sends
1172
+ USE-CANDIDATE on both sockets with the single pair this returns. That works
1173
+ on the whole reference fleet, so those cameras evidently answer with one
1174
+ pair - but no captured answer in this repo proves it, and a camera that
1175
+ answered per-media credentials would need this split before it could be
1176
+ nominated on both.
1177
+
1178
+ One parser, used by both the nomination and the window's exit predicate.
1179
+ The exit means "the nomination this is about to reach will succeed", and it
1180
+ only means that while both sides read the same SDP the same way.
1181
+ """
1182
+ ufrag = ""
1183
+ pwd = ""
1184
+ cands: list = []
1185
+ host: tuple = ()
1186
+ for line in (sdp or "").splitlines():
1187
+ if line.startswith("a=ice-ufrag:") and not ufrag:
1188
+ ufrag = line[len("a=ice-ufrag:"):].strip()
1189
+ elif line.startswith("a=ice-pwd:") and not pwd:
1190
+ pwd = line[len("a=ice-pwd:"):].strip()
1191
+ elif line.startswith("a=candidate:"):
1192
+ parsed = _parse_candidate_line(line)
1193
+ if parsed:
1194
+ addr = (parsed[0], parsed[1])
1195
+ cands.append(addr)
1196
+ if parsed[2] == "host" and not host:
1197
+ host = addr
1198
+ return ufrag, pwd, cands, host
1199
+
1200
+
1201
+ def _answer_sdp_can_nominate(sdp) -> bool:
1202
+ """Whether this answer carries everything USE-CANDIDATE needs.
1203
+
1204
+ Deliberately the whole precondition and not a proxy for it: an answer with
1205
+ credentials but no candidate, or candidates but no credentials, leaves the
1206
+ nomination with nothing to address, and leaving the window on it would
1207
+ trade 4.3 s of responding for a session that never nominates at all.
1208
+ """
1209
+ ufrag, pwd, cands, _host = _parse_answer_ice(sdp)
1210
+ return bool(ufrag and pwd and cands)
1211
+
1212
+
1213
+ def _stun_window_answer_exit_due(*, stun_seen: bool,
1214
+ answer_ready: bool) -> bool:
1215
+ """Whether the STUN responder window has nothing left to wait for.
1216
+
1217
+ Every branch of the window carries the delay, so this is not scoped to one.
1218
+ Measured on two cold A001064 opens that took different branches:
1219
+
1220
+ * echo received, webrtcResp sent (the 20 s window): answer at +0.72 s,
1221
+ window ran to +5.05 s and left on 1.5 s of ICE silence.
1222
+ * no echo (the 2.5 s window): answer at +0.22 s, window ran the full
1223
+ 2.5 s cap and left on the deadline.
1224
+
1225
+ Both held the answer for the whole window. Nothing else in the window is
1226
+ load-bearing by then either: an SRTP packet already breaks out of it, a
1227
+ camera that never probes already leaves on ``_pre_stun_idle``, and the
1228
+ bridge thread picks the responder role back up ~30 ms later.
1229
+
1230
+ ``stun_seen`` is the camera's own evidence that it is running ICE: a probe
1231
+ this host actually answered, which is why it is set where the reply is sent
1232
+ and not where the request is parsed - a self-loop Data Indication is a
1233
+ binding request too, and it is our own address coming back through the
1234
+ relay. Without such a probe there is nothing to nominate a pair *with*, and
1235
+ a relay-only camera that answers early and probes late keeps the behaviour
1236
+ it has today.
1237
+ """
1238
+ return bool(stun_seen and answer_ready)
1239
+
1240
+
1113
1241
  def video_pt_from_answer_sdp(sdp_text: str) -> Optional[int]:
1114
1242
  """Which video payload type to narrow to, taken from the camera's answer SDP.
1115
1243
 
@@ -1343,13 +1471,23 @@ def _serve_video_pt(observed, answer, pinned) -> "Optional[int]":
1343
1471
 
1344
1472
  An observed payload type is fact -- it is what is arriving. A pin is our
1345
1473
  own constraint on the offer, so narrowing past it can only produce a stream
1346
- the camera was never asked to send. The answer is the weakest of the three:
1347
- measured 2026-08-23, the reference A001064 ANSWERS 97 (H.265) and SENDS 96
1348
- (H.264) in every observed session, and trusting the answer when video had
1349
- not yet been observed built an hevc-only SDP that no hevc ever filled --
1350
- ffmpeg could not determine dimensions, could not write the RTSP header, and
1351
- the serve died at startup. That was invisible until the exit reason stopped
1352
- being flushed out of the stderr tail by Non-monotonic DTS noise.
1474
+ the camera was never asked to send. The answer is the weakest of the three
1475
+ because it can be absent or stale, not because it lies: trusting it when
1476
+ video had not yet been observed once built an hevc-only SDP that no hevc
1477
+ ever filled -- ffmpeg could not determine dimensions, could not write the
1478
+ RTSP header, and the serve died at startup. That was invisible until the
1479
+ exit reason stopped being flushed out of the stderr tail by Non-monotonic
1480
+ DTS noise.
1481
+
1482
+ This docstring used to say the reference A001064 "ANSWERS 97 (H.265) and
1483
+ SENDS 96 (H.264) in every observed session". That is RETRACTED. Measured on
1484
+ 2026-09-04 by reading the first decrypted NAL of 44 cold opens: the answer
1485
+ and the wire agree every time. The camera simply picks a codec per session
1486
+ -- 40 of the 44 negotiated H.264 (payload begins 0x67, an H.264 SPS with
1487
+ profile_idc 0x4D level 0x1F, matching the avc1.4D001F go2rtc reports) and 4
1488
+ negotiated H.265 (0x40/0x42/0x44 -- VPS, SPS, PPS -- then 0x62 FU
1489
+ fragments). Both are served correctly today. The retracted claim reads as a
1490
+ demux bug and very nearly bought a fix for one that does not exist.
1353
1491
 
1354
1492
  Returns None when nothing is known; the caller keeps its own "could not
1355
1493
  narrow" path rather than having a payload type invented for it.
@@ -1637,16 +1775,55 @@ async def _sdes_await_answer_or_terminal(
1637
1775
  raise TimeoutError()
1638
1776
 
1639
1777
 
1778
+ # Seconds a role-reversal camera gets to echo our webrtcReq back.
1779
+ #
1780
+ # This was 2.0 s on the reading that A001064 "needs the resulting webrtcResp".
1781
+ # Measured over 18 hours of logs on 2026-09-03/04: of 61 SDES opens, the 17 that
1782
+ # took this wait timed out 17/17 at a mean of 2.086 s, and neither `camera
1783
+ # webrtcReq echo received` nor `webrtcResp sent (SDES` appears once in the whole
1784
+ # log. The webrtcResp was never built, and all 17 streamed regardless - so the
1785
+ # 2.0 s was 45 percent of a 4.2 s cold connect, spent on nothing.
1786
+ #
1787
+ # Shortened rather than removed, exactly as the livePlayReq echo wait was
1788
+ # (5.0 -> 0.25 s, 169/169 timeouts): the evidence says the wait times out, not
1789
+ # that the branch is wrong, so a camera that does echo promptly still gets its
1790
+ # webrtcResp. Raise AIDOT_SDES_ECHO_WAIT_S to put the old behaviour back
1791
+ # without a release.
1792
+ #
1793
+ # The measurement is one fleet. An echo that lands between this value and the
1794
+ # old 2.0 s would now miss the wait, and _cam_echo_received is a branch
1795
+ # selector, not just a log line: it decides whether the webrtcResp is built,
1796
+ # whether the ICE window runs 20 s or 2.5 s, and whether the quickConn
1797
+ # reconnect retry is armed. A late echo is therefore reported loudly at the
1798
+ # end of the STUN window rather than passing unnoticed, so an operator on a
1799
+ # fleet whose band is not empty gets a line naming the knob.
1800
+ _SDES_ECHO_WAIT_S = 0.25
1801
+
1802
+
1640
1803
  def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
1641
- """Seconds to block on the camera's webrtcReq echo before proceeding.
1804
+ """Seconds to block on the camera's webrtcReq echo. Never negative.
1642
1805
 
1643
1806
  The echo only ever arrives for role-reversal models (e.g. A001064), which
1644
- need the resulting webrtcResp and run with ``skip_liveplay`` False (they are
1645
- hard-excluded from sdes_fast_liveplay) - they keep the full 2.0s wait. For
1646
- A001513-class cameras (``skip_liveplay`` True, the default) the echo never
1647
- arrives and the webrtcResp-building branch is dead/redundant, so don't block
1648
- on it (saves ~2s of cold-start dead time)."""
1649
- return 0.0 if skip_liveplay else 2.0
1807
+ build a webrtcResp from it and run with ``skip_liveplay`` False (they are
1808
+ hard-excluded from sdes_fast_liveplay). For A001513-class cameras
1809
+ (``skip_liveplay`` True, the default) the echo never arrives at all, so they
1810
+ do not block on it.
1811
+
1812
+ Read per call and clamped, like its sibling
1813
+ :func:`_sdes_liveplay_echo_timeout`: a malformed env value is ignored rather
1814
+ than raising, because parsing it at import time makes one typo on an HA box
1815
+ an ImportError that takes down every camera, and freezing it at import means
1816
+ an operator changing it sees nothing until a full restart.
1817
+ """
1818
+ raw = _os.environ.get("AIDOT_SDES_ECHO_WAIT_S")
1819
+ if raw is not None:
1820
+ try:
1821
+ val = float(raw)
1822
+ except (TypeError, ValueError):
1823
+ val = None
1824
+ if val is not None and val >= 0.0:
1825
+ return 0.0 if skip_liveplay else val
1826
+ return 0.0 if skip_liveplay else _SDES_ECHO_WAIT_S
1650
1827
 
1651
1828
 
1652
1829
  # ffmpeg returns AVERROR(EPIPE) = -32 when its output consumer disappears, and a
@@ -3092,10 +3269,12 @@ class _SdesOpenMixin:
3092
3269
  # Only role-reversal models (A001064, _skip_lp False) echo our webrtcReq
3093
3270
  # and need the webrtcResp built below; for A001513-class (_skip_lp True,
3094
3271
  # default) the echo never arrives, so don't block ~2s on it.
3272
+ _echo_wait_s = _sdes_echo_wait_timeout(_skip_lp)
3273
+ _echo_wait_t0 = time.monotonic()
3095
3274
  try:
3096
3275
  await _asyncio.wait_for(
3097
3276
  _asyncio.shield(_echo_fut),
3098
- timeout=_sdes_echo_wait_timeout(_skip_lp),
3277
+ timeout=_echo_wait_s,
3099
3278
  )
3100
3279
  _cam_echo_received = True
3101
3280
  _status("camera webrtcReq echo received - building webrtcResp")
@@ -3252,7 +3431,17 @@ class _SdesOpenMixin:
3252
3431
  f" video={_ans_video_ip}:{_ans_video_port})"
3253
3432
  )
3254
3433
  except TimeoutError:
3255
- pass # no echo - camera uses a different signalling variant; proceed
3434
+ # No echo - camera uses a different signalling variant; proceed.
3435
+ # Logged rather than swallowed: `except TimeoutError: pass` is how
3436
+ # 2.0 s of dead time here stayed invisible, and the elapsed line is
3437
+ # what will show it if this wait ever starts being used.
3438
+ _LOGGER.info(
3439
+ "signaling-wait[%s] webrtcReq-echo elapsed=%dms"
3440
+ " (timeout=%.2fs)",
3441
+ getattr(self, "device_id", "?"),
3442
+ int((time.monotonic() - _echo_wait_t0) * 1000),
3443
+ _echo_wait_s,
3444
+ )
3256
3445
 
3257
3446
  # --- Announce our ICE candidates via MQTT (iceCandidateReq) ----------- #
3258
3447
  # The iOS app always sends iceCandidateReq after webrtcReq/webrtcResp.
@@ -3498,12 +3687,31 @@ class _SdesOpenMixin:
3498
3687
  _pre_stun_idle = 0.5 # exit early if no packet at all (non-ICE camera)
3499
3688
  _stun_deadline = time.monotonic() + _stun_max
3500
3689
  _last_pkt_t = time.monotonic()
3690
+ _stun_window_t0 = time.monotonic()
3501
3691
  for _rsock in (_audio_sock, _video_sock):
3502
3692
  try:
3503
3693
  _rsock.setblocking(False)
3504
3694
  except Exception:
3505
3695
  _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
3506
3696
  while time.monotonic() < _stun_deadline:
3697
+ # Answer-exit: the only reason to sit here is that the credentials
3698
+ # the nomination needs have not arrived yet. Once they have - and
3699
+ # the camera has proved it is running ICE by probing us - waiting
3700
+ # longer just delays USE-CANDIDATE, and the bridge thread below
3701
+ # picks the responder role straight back up. Checked before the
3702
+ # idle test because the answer usually lands while the camera is
3703
+ # still probing hard, so no idle threshold would ever be crossed.
3704
+ if _stun_window_answer_exit_due(
3705
+ stun_seen=_stun_seen,
3706
+ answer_ready=getattr(
3707
+ self, "_camera_answer_ice_ready_ts", None) is not None):
3708
+ _status(
3709
+ "STUN window: leaving early after %.2fs - answer ICE"
3710
+ " credentials in hand and camera probing; nominating now"
3711
+ " (%d binding request(s) answered)"
3712
+ % (time.monotonic() - _stun_window_t0, _stun_count)
3713
+ )
3714
+ break
3507
3715
  # Idle-exit: threshold depends on whether we've seen any STUN yet
3508
3716
  idle = time.monotonic() - _last_pkt_t
3509
3717
  if _stun_seen:
@@ -3575,8 +3783,15 @@ class _SdesOpenMixin:
3575
3783
  # replies) must NOT set _stun_seen or they'd trigger the 1.5s
3576
3784
  # idle-exit prematurely, before camera probes arrive.
3577
3785
  if _pkt[:2] == b'\x00\x01':
3578
- # Binding Request - reply with Binding Success Response
3579
- _stun_seen = True
3786
+ # Binding Request - reply with Binding Success Response.
3787
+ # _stun_seen is NOT set here. A self-loop Data
3788
+ # Indication - our own address echoed back through the
3789
+ # relay - is a Binding Request too, and the branch below
3790
+ # drops it precisely because it is not the camera.
3791
+ # Setting the flag here made one of those count as "the
3792
+ # camera has started ICE", which is the one thing the
3793
+ # answer-exit is gated on. It is set where a probe is
3794
+ # actually answered instead.
3580
3795
  if _turn_peer_ip_sw is None and _src[0] != _hp_host:
3581
3796
  _prefer_direct_stun[_sock] = True
3582
3797
  _tid = _pkt[8:20]
@@ -3624,6 +3839,7 @@ class _SdesOpenMixin:
3624
3839
  + _STUN_MAGIC + os.urandom(12)
3625
3840
  + _si_body)
3626
3841
  _sock.sendto(_send_ind, (_t_host_sw, _t_port_sw))
3842
+ _stun_seen = True
3627
3843
  elif _turn_peer_ip_sw and _is_self_peer_ip(
3628
3844
  _turn_peer_ip_sw, _turn_peer_port_sw):
3629
3845
  # Self-loop Data Indication (peer == our own
@@ -3640,6 +3856,7 @@ class _SdesOpenMixin:
3640
3856
  )
3641
3857
  else:
3642
3858
  _sock.sendto(_resp, _src)
3859
+ _stun_seen = True
3643
3860
  _stun_count += 1
3644
3861
  except Exception:
3645
3862
  _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
@@ -3729,6 +3946,20 @@ class _SdesOpenMixin:
3729
3946
  _last_pkt_t = time.monotonic()
3730
3947
  _stun_seen = False
3731
3948
  while time.monotonic() < _stun_deadline:
3949
+ # Same exit as the first window. A camera that quickConns after
3950
+ # signalling re-runs ICE here, and without this it sat the full
3951
+ # 8 s on credentials it already had - the same delay the first
3952
+ # window was fixed for, in the window the fix had not reached.
3953
+ if _stun_window_answer_exit_due(
3954
+ stun_seen=_stun_seen,
3955
+ answer_ready=getattr(
3956
+ self, "_camera_answer_ice_ready_ts", None) is not None):
3957
+ _status(
3958
+ "STUN window (retry %d): leaving early after %.2fs -"
3959
+ " answer ICE credentials in hand and camera probing"
3960
+ % (_sdes_retries,
3961
+ time.monotonic() - (_stun_deadline - 8.0)))
3962
+ break
3732
3963
  if _stun_seen and time.monotonic() - _last_pkt_t > _idle_limit:
3733
3964
  break
3734
3965
  try:
@@ -3747,7 +3978,6 @@ class _SdesOpenMixin:
3747
3978
  and _pkt_r[4:8] == _STUN_MAGIC):
3748
3979
  if _pkt_r[:2] != b'\x00\x01':
3749
3980
  continue # not a Binding Request; don't trigger idle-exit
3750
- _stun_seen = True
3751
3981
  _tid_r = _pkt_r[8:20]
3752
3982
  try:
3753
3983
  _resp_r = _build_stun_binding_success_response(
@@ -3760,6 +3990,7 @@ class _SdesOpenMixin:
3760
3990
  magic_cookie=_STUN_MAGIC,
3761
3991
  )
3762
3992
  _sk_r.sendto(_resp_r, _src_r)
3993
+ _stun_seen = True
3763
3994
  _stun_count += 1
3764
3995
  except Exception:
3765
3996
  _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_is_self_peer_ip', exc_info=True)
@@ -3784,12 +4015,17 @@ class _SdesOpenMixin:
3784
4015
  # call_soon_threadsafe(answer_fut.set_result, ...) from the MQTT thread is
3785
4016
  # queued but hasn't fired yet. One asyncio cycle resolves it.
3786
4017
  await asyncio.sleep(0)
3787
- # ...but one cycle only helps if the answer had already ARRIVED, and it
3788
- # has not: the STUN window above runs on a fixed schedule that closes
3789
- # ~2.4 s before the camera answers. Wait for it, because the ICE
3790
- # credentials it carries are what the nomination below needs - without
3791
- # them the camera is never nominated, stays in ICE "Checking", and never
3792
- # sends SRTP no matter how long the first-media wait runs.
4018
+ # ...but one cycle only helps if the answer had already ARRIVED. When it
4019
+ # has, the window above now leaves as soon as it is readable and this
4020
+ # harvest is a formality; when it has not, the window ran to its own
4021
+ # deadline and this wait is the whole story. Either way, wait for it:
4022
+ # the ICE credentials it carries are what the nomination below needs -
4023
+ # without them the camera is never nominated, stays in ICE "Checking",
4024
+ # and never sends SRTP no matter how long the first-media wait runs.
4025
+ # (This comment used to assert the window "closes ~2.4 s before the
4026
+ # camera answers". That stopped being true when the livePlayReq echo
4027
+ # wait was cut to 0.25 s, and the window then sat on the answer for
4028
+ # seconds - see _stun_window_answer_exit_due.)
3793
4029
  # shield() so a timeout here cannot cancel the future the real answer
3794
4030
  # await (and the DTLS-fallback path) still consume further down.
3795
4031
  # A TERMINAL refusal races the harvest. A camera at its viewer cap acks
@@ -4075,31 +4311,19 @@ class _SdesOpenMixin:
4075
4311
  # sending a binding request with the USE-CANDIDATE attribute (0x0025).
4076
4312
  # Without this, the camera stays in ICE "Checking" state indefinitely
4077
4313
  # (hence the continuous duplicate STUN probe log lines) and never streams.
4078
- import re as _re_ice
4079
4314
 
4080
4315
  # Parse camera's ICE credentials and UDP candidates from its answer SDP.
4081
- _cam_ice_ufrag: str = ""
4082
- _cam_ice_pwd: str = ""
4083
- _cam_ice_cands: list = [] # list of (ip, port) tuples
4084
-
4085
- _cam_ice_host: tuple = () # (ip, port) of typ host candidate for SCTP
4086
- if _pre_launch_answer_sdp:
4087
- for _ice_ln in _pre_launch_answer_sdp.splitlines():
4088
- if _ice_ln.startswith("a=ice-ufrag:") and not _cam_ice_ufrag:
4089
- _cam_ice_ufrag = _ice_ln[len("a=ice-ufrag:"):].strip()
4090
- elif _ice_ln.startswith("a=ice-pwd:") and not _cam_ice_pwd:
4091
- _cam_ice_pwd = _ice_ln[len("a=ice-pwd:"):].strip()
4092
- elif _ice_ln.startswith("a=candidate:"):
4093
- _cand_m = _re_ice.match(
4094
- r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ (\w+)",
4095
- _ice_ln,
4096
- )
4097
- if _cand_m:
4098
- _cip, _cport = _cand_m.group(1), int(_cand_m.group(2))
4099
- _ctyp = _cand_m.group(3)
4100
- _cam_ice_cands.append((_cip, _cport))
4101
- if _ctyp == "host" and not _cam_ice_host:
4102
- _cam_ice_host = (_cip, _cport)
4316
+ # Credentials and candidates from the camera's answer. _cam_ice_host is
4317
+ # the typ host candidate, which is what SCTP is addressed to.
4318
+ _cam_ice_ufrag: str
4319
+ _cam_ice_pwd: str
4320
+ _cam_ice_cands: list # list of (ip, port) tuples
4321
+ _cam_ice_host: tuple
4322
+ # Through the shared parser, not a second copy: the STUN window's early
4323
+ # exit above means "the nomination below will succeed", and it only
4324
+ # means that while both read the same SDP the same way.
4325
+ (_cam_ice_ufrag, _cam_ice_pwd, _cam_ice_cands,
4326
+ _cam_ice_host) = _parse_answer_ice(_pre_launch_answer_sdp)
4103
4327
 
4104
4328
  def _send_use_candidate(sock, our_ufrag, our_pwd, cam_ufrag, cam_pwd, cam_addr):
4105
4329
  """Send a STUN Binding Request with ICE-CONTROLLING + USE-CANDIDATE."""
@@ -4181,20 +4405,7 @@ class _SdesOpenMixin:
4181
4405
  """
4182
4406
  if not sdp_text:
4183
4407
  return 0
4184
- _u = _p = ""
4185
- _cands: list = []
4186
- for _ln in sdp_text.splitlines():
4187
- if _ln.startswith("a=ice-ufrag:") and not _u:
4188
- _u = _ln[len("a=ice-ufrag:"):].strip()
4189
- elif _ln.startswith("a=ice-pwd:") and not _p:
4190
- _p = _ln[len("a=ice-pwd:"):].strip()
4191
- elif _ln.startswith("a=candidate:"):
4192
- _m = _re_ice.match(
4193
- r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ (\w+)",
4194
- _ln,
4195
- )
4196
- if _m:
4197
- _cands.append((_m.group(1), int(_m.group(2))))
4408
+ _u, _p, _cands, _ = _parse_answer_ice(sdp_text)
4198
4409
  if not (_u and _p and _cands):
4199
4410
  return 0
4200
4411
  # Open the relay's door before probing, same invariant the setup and
@@ -6690,15 +6901,14 @@ class _SdesOpenMixin:
6690
6901
  while _tk_next < len(ice_cands_seen):
6691
6902
  _tk_line = (ice_cands_seen[_tk_next].get("candidate") or "")
6692
6903
  _tk_next += 1
6693
- _tk_m = _re_ice.match(
6694
- r"(?:a=)?candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ (\w+)",
6695
- _tk_line,
6696
- )
6697
- if not _tk_m:
6904
+ # The shared parser, so a candidate the answer path
6905
+ # nominates cannot be one this path silently skips.
6906
+ _tk_parsed = _parse_candidate_line(_tk_line)
6907
+ if not _tk_parsed:
6698
6908
  continue
6699
- _tk_ip = _tk_m.group(1)
6700
- _tk_port = int(_tk_m.group(2))
6701
- _tk_typ = _tk_m.group(3)
6909
+ _tk_ip = _tk_parsed[0]
6910
+ _tk_port = _tk_parsed[1]
6911
+ _tk_typ = _tk_parsed[2]
6702
6912
  if (_tk_ip, _tk_port) in _tk_seen:
6703
6913
  continue
6704
6914
  _tk_seen.add((_tk_ip, _tk_port))
@@ -7242,22 +7452,8 @@ class _SdesOpenMixin:
7242
7452
  # parse the camera's ICE credentials now and populate _bridge_uc_info
7243
7453
  # so the bridge can send USE-CANDIDATE on the next camera BindingReq.
7244
7454
  if not _bridge_uc_info["sent"] and not _bridge_uc_info["ufrag"] and _ans_sdp:
7245
- import re as _re_late_ice
7246
- _late_ufrag = _late_pwd = ""
7247
- _late_cands: list = []
7248
- for _al in _ans_sdp.splitlines():
7249
- if _al.startswith("a=ice-ufrag:") and not _late_ufrag:
7250
- _late_ufrag = _al[len("a=ice-ufrag:"):].strip()
7251
- elif _al.startswith("a=ice-pwd:") and not _late_pwd:
7252
- _late_pwd = _al[len("a=ice-pwd:"):].strip()
7253
- elif _al.startswith("a=candidate:"):
7254
- _cm = _re_late_ice.match(
7255
- r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ",
7256
- _al,
7257
- )
7258
- if _cm:
7259
- _late_cands.append(
7260
- (_cm.group(1), int(_cm.group(2))))
7455
+ _late_ufrag, _late_pwd, _late_cands, _ = _parse_answer_ice(
7456
+ _ans_sdp)
7261
7457
  if _late_ufrag and _late_pwd and _late_cands:
7262
7458
  _bridge_uc_info["ufrag"] = _late_ufrag
7263
7459
  _bridge_uc_info["pwd"] = _late_pwd
@@ -7460,7 +7656,6 @@ class _SdesOpenMixin:
7460
7656
  # Spawn a background task to pick it up and inject ICE credentials
7461
7657
  # into _bridge_uc_info so the bridge can send USE-CANDIDATE.
7462
7658
  if second_answer_fut is not None:
7463
- import re as _re_sa
7464
7659
  async def _late_second_answer_task():
7465
7660
  nonlocal _dc_answer_has_app
7466
7661
  try:
@@ -7471,21 +7666,8 @@ class _SdesOpenMixin:
7471
7666
  if (_la_sdp
7472
7667
  and not _bridge_uc_info["sent"]
7473
7668
  and not _bridge_uc_info["ufrag"]):
7474
- _la_ufrag = _la_pwd = ""
7475
- _la_cands: list = []
7476
- for _ll in _la_sdp.splitlines():
7477
- if _ll.startswith("a=ice-ufrag:") and not _la_ufrag:
7478
- _la_ufrag = _ll[len("a=ice-ufrag:"):].strip()
7479
- elif _ll.startswith("a=ice-pwd:") and not _la_pwd:
7480
- _la_pwd = _ll[len("a=ice-pwd:"):].strip()
7481
- elif _ll.startswith("a=candidate:"):
7482
- _cm2 = _re_sa.match(
7483
- r"a=candidate:\S+ \d+ udp \d+ ([\d.]+) (\d+) typ",
7484
- _ll,
7485
- )
7486
- if _cm2:
7487
- _la_cands.append(
7488
- (_cm2.group(1), int(_cm2.group(2))))
7669
+ (_la_ufrag, _la_pwd, _la_cands,
7670
+ _) = _parse_answer_ice(_la_sdp)
7489
7671
  if _la_ufrag and _la_pwd and _la_cands:
7490
7672
  _bridge_uc_info["ufrag"] = _la_ufrag
7491
7673
  _bridge_uc_info["pwd"] = _la_pwd
@@ -21,6 +21,7 @@ if TYPE_CHECKING: # import cost stays out of the runtime path
21
21
 
22
22
  from ..exceptions import AidotCameraBusy, AidotCameraNotReady
23
23
  from .constants import _LIVE_PLAY_NOT_READY, fallback_turn_uris, stun_server_uris
24
+ from .sdes_open import _answer_sdp_can_nominate
24
25
  from .webrtc import WebRTCSession
25
26
  from .protocol import (
26
27
  AvioResponseRouter,
@@ -65,6 +66,34 @@ def _resolve_future_threadsafe(loop, fut, value) -> None:
65
66
  loop.call_soon_threadsafe(_resolve)
66
67
 
67
68
 
69
+ def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
70
+ """Whether a webrtcResp came from the camera rather than from us.
71
+
72
+ The broker echoes our own SDES webrtcResp back on the account topic, it
73
+ carries our own peerid so the accept filter admits it, and its SDP has our
74
+ ice-ufrag, ice-pwd and candidates - so it satisfies the nomination
75
+ precondition just as the camera's answer does. Without this, our own echo
76
+ would set the answer-is-ready marker and the STUN window would leave having
77
+ heard nothing from the camera at all.
78
+
79
+ Scope, stated precisely: this gates only the marker. It does NOT stop the
80
+ echo resolving ``answer_fut``, which is where the nomination reads its SDP -
81
+ that routing is unchanged, and a session whose ``answer_fut`` takes the echo
82
+ nominates our own addresses whether the window leaves early or late. Fixing
83
+ that means applying this predicate at delivery, which changes established
84
+ behaviour this release has no measurement for.
85
+
86
+ ``srcAddr`` carries the account's prefix convention - ``2.`` device,
87
+ ``0.`` app, ``9.`` server - and our own messages go out as ``0.{userId}``,
88
+ which is the same test the webrtcReq echo already uses. Device-channel
89
+ messages that omit srcAddr are treated as the camera's: absent evidence,
90
+ keep the behaviour that existed before this gate.
91
+ """
92
+ if not src_addr:
93
+ return True
94
+ return not str(src_addr).startswith(f"0.{user_id}")
95
+
96
+
68
97
  def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer) -> None:
69
98
  """Route a webrtcResp answer to the first-answer or second-answer future.
70
99
 
@@ -811,6 +840,15 @@ class _WebRTCOpenMixin:
811
840
  # MQTT thread and read from the loop: a lone float rebind, never
812
841
  # read-modify-write, so no lock is needed.
813
842
  self._camera_device_seen_ts = None
843
+ # When this open first saw an answer carrying the ICE credentials and
844
+ # candidates the nomination needs (monotonic). Read by the SDES STUN
845
+ # responder window, which blocks the event loop and so cannot see
846
+ # answer_fut resolve - call_soon_threadsafe only QUEUES that. Stamped
847
+ # here, on the MQTT thread, so the window can leave the moment there is
848
+ # nothing left to wait for. Cleared per open, or the second open would
849
+ # leave its window instantly on the first open's answer. Same lone
850
+ # float rebind as above, so no lock.
851
+ self._camera_answer_ice_ready_ts = None
814
852
  # One-shot per open: the keep-alive re-assert fired when the camera
815
853
  # first announces itself. See the dispatcher below.
816
854
  _wake_keepalive_sent = [False]
@@ -994,6 +1032,13 @@ class _WebRTCOpenMixin:
994
1032
  )
995
1033
  )
996
1034
  return
1035
+ if (self._camera_answer_ice_ready_ts is None
1036
+ and _answer_is_from_the_camera(
1037
+ src_addr=(inner.get("srcAddr")
1038
+ or msg.get("srcAddr") or ""),
1039
+ user_id=user_id)
1040
+ and _answer_sdp_can_nominate(answer.get("sdp", ""))):
1041
+ self._camera_answer_ice_ready_ts = time.monotonic()
997
1042
  _deliver_webrtc_answer(
998
1043
  loop, answer_fut, second_answer_fut, answer
999
1044
  )