python-aidot-cameras 1.0.0rc8__tar.gz → 1.0.0rc9__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 (286) hide show
  1. {python_aidot_cameras-1.0.0rc8/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc9}/PKG-INFO +2 -2
  2. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/README.md +1 -1
  3. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sdes_open.py +77 -14
  4. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/webrtc_open.py +31 -15
  5. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/pyproject.toml +1 -1
  6. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9/python_aidot_cameras.egg-info}/PKG-INFO +2 -2
  7. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nominates_as_soon_as_the_answer_lands.py +124 -1
  8. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_echo_wait_is_not_dead_time.py +58 -0
  9. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/LICENSE +0 -0
  10. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/__init__.py +0 -0
  12. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/__main__.py +0 -0
  13. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_upstream.py +0 -0
  14. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  16. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  18. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  20. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  21. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  22. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  23. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  24. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  25. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  26. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  27. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  28. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  29. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  30. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  31. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  32. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  33. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  34. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  35. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  36. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  37. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  38. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  39. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  40. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  41. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  43. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  44. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  45. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  46. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  47. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  48. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/ble_gateway.py +0 -0
  49. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/__init__.py +0 -0
  50. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/client.py +0 -0
  51. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/constants.py +0 -0
  52. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/controls.py +0 -0
  53. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/go2rtc.py +0 -0
  54. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/hwaccel.py +0 -0
  55. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/lan_control.py +0 -0
  56. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/models.py +0 -0
  57. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/playback.py +0 -0
  58. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/protocol.py +0 -0
  59. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_events.py +0 -0
  60. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_listing.py +0 -0
  61. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sd_playback.py +0 -0
  62. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/sdes.py +0 -0
  63. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/tutk.py +0 -0
  64. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/camera/webrtc.py +0 -0
  65. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/client.py +0 -0
  66. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/cloud_auth.py +0 -0
  67. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/const.py +0 -0
  68. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/credentials.py +0 -0
  69. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/crypto.py +0 -0
  70. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/device_client.py +0 -0
  71. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/discover.py +0 -0
  72. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/exceptions.py +0 -0
  73. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/g711.py +0 -0
  74. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/lan_retry.py +0 -0
  75. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/aidot_cameras/py.typed +0 -0
  76. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  77. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  78. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  79. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/requires.txt +0 -0
  80. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  81. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/setup.cfg +0 -0
  82. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_aioice_compat.py +0 -0
  83. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_alarm_event.py +0 -0
  84. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_pt_kinds.py +0 -0
  85. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_sdp_video_pt.py +0 -0
  86. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_answer_section_selection.py +0 -0
  87. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_audio_mux_offline.py +0 -0
  88. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  89. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_request.py +0 -0
  90. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_response_parse.py +0 -0
  91. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_avio_response_router.py +0 -0
  92. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_backoff.py +0 -0
  93. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_detection.py +0 -0
  94. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_evidence_corroboration.py +0 -0
  95. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_skip_turn.py +0 -0
  96. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  97. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_battery_wake_gate_app_parity.py +0 -0
  98. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_bitrate_campaign_arms.py +0 -0
  99. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ble_gateway.py +0 -0
  100. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_bridge_state_sentinels.py +0 -0
  101. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_busy_backoff.py +0 -0
  102. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_camera_properties.py +0 -0
  103. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_canary_log_names_the_camera.py +0 -0
  104. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_cloud_count.py +0 -0
  105. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_cloud_plan.py +0 -0
  106. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_constants_imports_resolve.py +0 -0
  107. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_control_verdicts.py +0 -0
  108. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_deferred_hardening.py +0 -0
  109. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_cmd_seq_match.py +0 -0
  110. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_login_guard.py +0 -0
  111. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_device_user_info_cache.py +0 -0
  112. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_direct_ts_keyframe_signal.py +0 -0
  113. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_direct_ts_serve.py +0 -0
  114. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dormant_camera_cleanup.py +0 -0
  115. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_drain_is_cancellable.py +0 -0
  116. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_mux_terminal_write.py +0 -0
  117. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_not_ready_burst.py +0 -0
  118. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_pinning.py +0 -0
  119. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_resolution_ack.py +0 -0
  120. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_decode_skip.py +0 -0
  121. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_ice_wait.py +0 -0
  122. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_serve_open_timeout.py +0 -0
  123. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_skip_signaling_wait.py +0 -0
  124. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_slow_probe.py +0 -0
  125. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_stdout_producer.py +0 -0
  126. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_dtls_video_presence.py +0 -0
  127. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_egress_guard.py +0 -0
  128. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_env_seams.py +0 -0
  129. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  130. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  131. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_futile_keepalive.py +0 -0
  132. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  133. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_go2rtc.py +0 -0
  134. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_go2rtc_cli.py +0 -0
  135. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_h264_decode_rate_limit.py +0 -0
  136. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_highport_nomination.py +0 -0
  137. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_hwaccel_probe.py +0 -0
  138. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ice_config_cache.py +0 -0
  139. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ice_peer_reflexive.py +0 -0
  140. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_idle_release_both_loops.py +0 -0
  141. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_keepalive_renew.py +0 -0
  142. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_key_restart_sdp.py +0 -0
  143. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_keyframe_prompter.py +0 -0
  144. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_lan_control.py +0 -0
  145. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  146. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_late_repeats_are_dropped.py +0 -0
  147. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_light_active_color_mode.py +0 -0
  148. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_session_carries_talk.py +0 -0
  149. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_stream_param.py +0 -0
  150. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_validate_policy.py +0 -0
  151. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_live_video_canary.py +0 -0
  152. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_liveplay_echo_timeout.py +0 -0
  153. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_local_only_controls.py +0 -0
  154. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_logging_caps.py +0 -0
  155. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_login_connect_deadline.py +0 -0
  156. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_login_reconnect_backoff.py +0 -0
  157. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_counted_only_when_plaintext.py +0 -0
  158. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_path_is_reported.py +0 -0
  159. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_socket_rcvbuf.py +0 -0
  160. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_media_wait_floor.py +0 -0
  161. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_motion_poll.py +0 -0
  162. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_client_id_is_exact.py +0 -0
  163. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_credential_refetch.py +0 -0
  164. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_credential_self_heal.py +0 -0
  165. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_password_not_persisted.py +0 -0
  166. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_session_reconnect.py +0 -0
  167. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  168. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nack.py +0 -0
  169. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  170. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_narrow_pc_ice.py +0 -0
  171. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_key_material_in_logs.py +0 -0
  172. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_self_referential_go2rtc.py +0 -0
  173. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_no_undefined_names.py +0 -0
  174. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_off_subnet_candidate.py +0 -0
  175. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offer_bandwidth_line.py +0 -0
  176. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  177. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_offline_keepalive_pause.py +0 -0
  178. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_on_frame_is_an_av_frame.py +0 -0
  179. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_open_fail_logger.py +0 -0
  180. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_open_gate_delay.py +0 -0
  181. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_peer_id_client_type.py +0 -0
  182. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_peerid_reuse.py +0 -0
  183. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_persistent_mqtt.py +0 -0
  184. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_persistent_mqtt_robustness.py +0 -0
  185. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_playback_tls.py +0 -0
  186. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_post_merge_hardening.py +0 -0
  187. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  188. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_public_signature_matches_impl.py +0 -0
  189. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_publish_target_readiness.py +0 -0
  190. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_quality_campaign_in_session.py +0 -0
  191. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_reboot_action.py +0 -0
  192. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_recent_recordings.py +0 -0
  193. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_remb.py +0 -0
  194. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  195. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_resent_frames_are_dropped.py +0 -0
  196. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_resolution_persists.py +0 -0
  197. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_restart_cycle.py +0 -0
  198. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_retry_policy.py +0 -0
  199. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_scripts_importable.py +0 -0
  200. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_init_ack_tsn.py +0 -0
  201. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_reassembly.py +0 -0
  202. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sctp_sack.py +0 -0
  203. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_card_presence.py +0 -0
  204. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_event_decode.py +0 -0
  205. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_event_probe_payloads.py +0 -0
  206. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_listing.py +0 -0
  207. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sd_playback.py +0 -0
  208. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_adaptive.py +0 -0
  209. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_avio_dispatch.py +0 -0
  210. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_bridge_break_gate.py +0 -0
  211. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_connection_mode.py +0 -0
  212. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_fast_liveplay.py +0 -0
  213. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  214. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_first_media_stall_report.py +0 -0
  215. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_ice_teardown.py +0 -0
  216. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_idle_release.py +0 -0
  217. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_nack_bridge.py +0 -0
  218. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_offer_video_order.py +0 -0
  219. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_offer_video_pin.py +0 -0
  220. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_open_cleanup.py +0 -0
  221. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_sctp_abort.py +0 -0
  222. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_audio.py +0 -0
  223. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_cmd.py +0 -0
  224. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_serve_stderr_drain.py +0 -0
  225. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_session_cap.py +0 -0
  226. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_sprop.py +0 -0
  227. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_talk.py +0 -0
  228. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_teardown_exit_log.py +0 -0
  229. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_terminal_ack.py +0 -0
  230. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_terminal_ack_latency.py +0 -0
  231. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_tmmbr_bridge.py +0 -0
  232. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdes_watchdog.py +0 -0
  233. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdp_narrowing_properties.py +0 -0
  234. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sdp_payload_type_narrowing.py +0 -0
  235. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_security_hardening.py +0 -0
  236. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  237. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serializable_login_info.py +0 -0
  238. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_input_timeout.py +0 -0
  239. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_loss_tolerance.py +0 -0
  240. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_pt_respects_the_pin.py +0 -0
  241. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_relay.py +0 -0
  242. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  243. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_session_stats.py +0 -0
  244. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_shared_house_seam.py +0 -0
  245. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  246. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_snapshot_cancellation.py +0 -0
  247. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_speak.py +0 -0
  248. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_capture_gate.py +0 -0
  249. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_staleness.py +0 -0
  250. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_unstable_public.py +0 -0
  251. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  252. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_srtp_rx_rekey.py +0 -0
  253. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_srtp_tx_key_note.py +0 -0
  254. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stable_terminal_id.py +0 -0
  255. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_nudge_policy.py +0 -0
  256. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  257. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  258. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  259. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  260. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  261. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_cap.py +0 -0
  262. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_drain_ownership.py +0 -0
  263. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_idle.py +0 -0
  264. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_slot_cap.py +0 -0
  265. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_stream_teardown.py +0 -0
  266. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_talk.py +0 -0
  267. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  268. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tap_to_mux_end_to_end.py +0 -0
  269. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_terminal_ack.py +0 -0
  270. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tmmbr.py +0 -0
  271. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_token_file_robustness.py +0 -0
  272. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_token_refresh.py +0 -0
  273. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_turn_create_permission.py +0 -0
  274. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  275. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_upstream_compat.py +0 -0
  276. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_validator_shares_one_token.py +0 -0
  277. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_profile_log.py +0 -0
  278. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_pts_dts.py +0 -0
  279. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_video_ts_unwrap.py +0 -0
  280. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_check_throttle.py +0 -0
  281. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_presence.py +0 -0
  282. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_viewer_present_push_mode.py +0 -0
  283. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wait_or_event.py +0 -0
  284. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wake_gate_camera_evidence.py +0 -0
  285. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/tests/test_wake_readiness_retry.py +0 -0
  286. {python_aidot_cameras-1.0.0rc8 → python_aidot_cameras-1.0.0rc9}/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.0rc8
3
+ Version: 1.0.0rc9
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,7 +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
+ | `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. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
576
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) |
577
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`) |
578
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,7 +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
+ | `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. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
536
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) |
537
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`) |
538
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) |
@@ -1167,13 +1167,17 @@ def _parse_answer_ice(sdp):
1167
1167
  order offered, and the first ``typ host`` candidate - which is the one SCTP
1168
1168
  is addressed to, and is not necessarily the first candidate listed.
1169
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.
1170
+ Taking the FIRST credential pair is now measured, not assumed. 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, which is
1173
+ only correct if the camera answers one pair for the whole session. Logging
1174
+ every a=ice-ufrag / a=ice-pwd line of the answer per m-section across four
1175
+ cold opens on two models (A001064 mains, A001513 battery) on 2026-09-04:
1176
+ every answer carried ONE pair, repeated identically in the audio, video and
1177
+ application sections. The shape is pinned in
1178
+ test_the_first_credential_pair_is_the_only_pair; a camera that answered
1179
+ per-media credentials would need this split before it could be nominated on
1180
+ both sockets.
1177
1181
 
1178
1182
  One parser, used by both the nomination and the window's exit predicate.
1179
1183
  The exit means "the nomination this is about to reach will succeed", and it
@@ -1210,6 +1214,19 @@ def _answer_sdp_can_nominate(sdp) -> bool:
1210
1214
  return bool(ufrag and pwd and cands)
1211
1215
 
1212
1216
 
1217
+ def _answer_ready_for_this_open(stamp, open_started_at) -> bool:
1218
+ """Whether the answer-ready marker was set by the open now running.
1219
+
1220
+ The marker is per-camera state driving a per-open decision. It is cleared
1221
+ at the start of every open, which covers the ordinary sequential case, but
1222
+ two opens overlapping on one camera object would otherwise let one leave its
1223
+ STUN window on the other's answer - or lose its own stamp to the other's
1224
+ reset. Comparing against this open's own start makes a stale stamp inert
1225
+ instead.
1226
+ """
1227
+ return stamp is not None and stamp >= open_started_at
1228
+
1229
+
1213
1230
  def _stun_window_answer_exit_due(*, stun_seen: bool,
1214
1231
  answer_ready: bool) -> bool:
1215
1232
  """Whether the STUN responder window has nothing left to wait for.
@@ -1798,11 +1815,23 @@ async def _sdes_await_answer_or_terminal(
1798
1815
  # end of the STUN window rather than passing unnoticed, so an operator on a
1799
1816
  # fleet whose band is not empty gets a line naming the knob.
1800
1817
  _SDES_ECHO_WAIT_S = 0.25
1818
+ #: What the wait was before it was measured, and what a device that has actually
1819
+ #: produced an echo goes back to.
1820
+ _SDES_ECHO_WAIT_LEGACY_S = 2.0
1801
1821
 
1802
1822
 
1803
- def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
1823
+ def _sdes_echo_wait_timeout(skip_liveplay: bool,
1824
+ echo_seen: bool = False) -> float:
1804
1825
  """Seconds to block on the camera's webrtcReq echo. Never negative.
1805
1826
 
1827
+ ``echo_seen`` is this device's own history: once an echo has been observed
1828
+ from it - during the wait or, more usefully, after the wait had already
1829
+ expired - every later open on that device waits the full
1830
+ ``_SDES_ECHO_WAIT_LEGACY_S`` again. That is what keeps the shortened wait
1831
+ honest on a fleet the 17/17 measurement never saw: such a camera pays the
1832
+ miss once, says so in the log, and is never shortened again. An explicit
1833
+ env value still wins over both.
1834
+
1806
1835
  The echo only ever arrives for role-reversal models (e.g. A001064), which
1807
1836
  build a webrtcResp from it and run with ``skip_liveplay`` False (they are
1808
1837
  hard-excluded from sdes_fast_liveplay). For A001513-class cameras
@@ -1823,7 +1852,9 @@ def _sdes_echo_wait_timeout(skip_liveplay: bool) -> float:
1823
1852
  val = None
1824
1853
  if val is not None and val >= 0.0:
1825
1854
  return 0.0 if skip_liveplay else val
1826
- return 0.0 if skip_liveplay else _SDES_ECHO_WAIT_S
1855
+ if skip_liveplay:
1856
+ return 0.0
1857
+ return _SDES_ECHO_WAIT_LEGACY_S if echo_seen else _SDES_ECHO_WAIT_S
1827
1858
 
1828
1859
 
1829
1860
  # ffmpeg returns AVERROR(EPIPE) = -32 when its output consumer disappears, and a
@@ -2315,6 +2346,10 @@ class _SdesOpenMixin:
2315
2346
  SDP answer, writes it to a temp file, and launches ffmpeg to receive and
2316
2347
  record the SRTP stream.
2317
2348
  """
2349
+ # When this open began. The answer-ready marker lives on the camera
2350
+ # object, not the open, so a marker older than this belongs to a
2351
+ # previous open and must not fire this one's early exit.
2352
+ _open_started_at = time.monotonic()
2318
2353
  from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _resolve_serve_input_timeout_s, _spawn_bg # lazy: break client<->sdes_open cycle
2319
2354
  import base64
2320
2355
  import subprocess
@@ -3269,7 +3304,8 @@ class _SdesOpenMixin:
3269
3304
  # Only role-reversal models (A001064, _skip_lp False) echo our webrtcReq
3270
3305
  # and need the webrtcResp built below; for A001513-class (_skip_lp True,
3271
3306
  # default) the echo never arrives, so don't block ~2s on it.
3272
- _echo_wait_s = _sdes_echo_wait_timeout(_skip_lp)
3307
+ _echo_wait_s = _sdes_echo_wait_timeout(
3308
+ _skip_lp, echo_seen=bool(getattr(self, "_sdes_echo_seen", False)))
3273
3309
  _echo_wait_t0 = time.monotonic()
3274
3310
  try:
3275
3311
  await _asyncio.wait_for(
@@ -3277,6 +3313,7 @@ class _SdesOpenMixin:
3277
3313
  timeout=_echo_wait_s,
3278
3314
  )
3279
3315
  _cam_echo_received = True
3316
+ self._sdes_echo_seen = True
3280
3317
  _status("camera webrtcReq echo received - building webrtcResp")
3281
3318
  # Seed _sdes_turn_entries from the echo's IceServerList if the HTTP
3282
3319
  # ice_config fetch returned nothing (empty list). The echo carries
@@ -3703,8 +3740,9 @@ class _SdesOpenMixin:
3703
3740
  # still probing hard, so no idle threshold would ever be crossed.
3704
3741
  if _stun_window_answer_exit_due(
3705
3742
  stun_seen=_stun_seen,
3706
- answer_ready=getattr(
3707
- self, "_camera_answer_ice_ready_ts", None) is not None):
3743
+ answer_ready=_answer_ready_for_this_open(
3744
+ getattr(self, "_camera_answer_ice_ready_ts", None),
3745
+ _open_started_at)):
3708
3746
  _status(
3709
3747
  "STUN window: leaving early after %.2fs - answer ICE"
3710
3748
  " credentials in hand and camera probing; nominating now"
@@ -3866,6 +3904,30 @@ class _SdesOpenMixin:
3866
3904
  break # inner per-socket loop
3867
3905
  if _srtp_detected:
3868
3906
  break # outer while loop
3907
+ # An echo that missed the shortened wait but turned up anyway. Recorded,
3908
+ # not acted on: the webrtcResp this would have built has to be sent
3909
+ # BEFORE the relay allocation and the ICE window, so building it now
3910
+ # would be a reordering with no camera to measure it against. What it
3911
+ # does do is make the NEXT open on this device wait the full legacy
3912
+ # window - so a fleet whose echo band is not empty pays the miss once
3913
+ # and says so, instead of losing the branch silently forever.
3914
+ if (not _cam_echo_received and _echo_fut is not None
3915
+ and not getattr(self, "_sdes_echo_seen", False)):
3916
+ try:
3917
+ _late_echo = _echo_fut.done() and not _echo_fut.cancelled()
3918
+ except Exception:
3919
+ _late_echo = False
3920
+ if _late_echo:
3921
+ self._sdes_echo_seen = True
3922
+ _LOGGER.warning(
3923
+ "camera %s: the webrtcReq echo arrived %.2fs after a %.2fs"
3924
+ " wait expired, so no webrtcResp was built for this open."
3925
+ " Later opens on this camera will wait %.1fs again. Set"
3926
+ " AIDOT_SDES_ECHO_WAIT_S=%.1f to make that permanent.",
3927
+ getattr(self, "device_id", "?"),
3928
+ time.monotonic() - _echo_wait_t0, _echo_wait_s,
3929
+ _SDES_ECHO_WAIT_LEGACY_S, _SDES_ECHO_WAIT_LEGACY_S,
3930
+ )
3869
3931
  for _rsock in (_audio_sock, _video_sock):
3870
3932
  try:
3871
3933
  _rsock.setblocking(True)
@@ -3952,8 +4014,9 @@ class _SdesOpenMixin:
3952
4014
  # window was fixed for, in the window the fix had not reached.
3953
4015
  if _stun_window_answer_exit_due(
3954
4016
  stun_seen=_stun_seen,
3955
- answer_ready=getattr(
3956
- self, "_camera_answer_ice_ready_ts", None) is not None):
4017
+ answer_ready=_answer_ready_for_this_open(
4018
+ getattr(self, "_camera_answer_ice_ready_ts", None),
4019
+ _open_started_at)):
3957
4020
  _status(
3958
4021
  "STUN window (retry %d): leaving early after %.2fs -"
3959
4022
  " answer ICE credentials in hand and camera probing"
@@ -76,12 +76,12 @@ def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
76
76
  would set the answer-is-ready marker and the STUN window would leave having
77
77
  heard nothing from the camera at all.
78
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.
79
+ It gates delivery as well as the marker. ``answer_fut`` is where the
80
+ nomination reads its SDP, so an echo that resolves it makes the nomination
81
+ address our own host and srflx candidates with our own credentials - a
82
+ session that can never be nominated, whether the window leaves early or
83
+ late. Nothing downstream wants our own SDP under any circumstance: the
84
+ SDES path built that answer itself and already holds every value in it.
85
85
 
86
86
  ``srcAddr`` carries the account's prefix convention - ``2.`` device,
87
87
  ``0.`` app, ``9.`` server - and our own messages go out as ``0.{userId}``,
@@ -94,12 +94,19 @@ def _answer_is_from_the_camera(*, src_addr, user_id) -> bool:
94
94
  return not str(src_addr).startswith(f"0.{user_id}")
95
95
 
96
96
 
97
- def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer) -> None:
97
+ def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer,
98
+ from_camera: bool = True) -> None:
98
99
  """Route a webrtcResp answer to the first-answer or second-answer future.
99
100
 
100
- Called from the MQTT (non-loop) thread. The first accepted answer resolves
101
- ``answer_fut``; a later one is the camera's real answer (the first was the
102
- broker echo of our own webrtcResp) and resolves ``second_answer_fut``.
101
+ Called from the MQTT (non-loop) thread. The first accepted answer from the
102
+ CAMERA resolves ``answer_fut``; a later one resolves ``second_answer_fut``,
103
+ which the late-ICE-credential recovery reads.
104
+
105
+ ``from_camera`` False means the broker echoed our own webrtcResp back at us.
106
+ It is dropped rather than routed: it carries our SDP, so resolving
107
+ ``answer_fut`` with it makes the nomination address our own candidates, and
108
+ resolving ``second_answer_fut`` feeds the same values to the late-credential
109
+ recovery. See :func:`_answer_is_from_the_camera`.
103
110
 
104
111
  One hop, for the reason in ``_resolve_future_threadsafe``: two answers
105
112
  drained inside one loop stall would otherwise both take the first branch,
@@ -108,6 +115,13 @@ def _deliver_webrtc_answer(loop, answer_fut, second_answer_fut, answer) -> None:
108
115
  about ``answer_fut`` is true.
109
116
  """
110
117
  def _deliver() -> None:
118
+ if not from_camera:
119
+ _LOGGER.debug(
120
+ "SDES: dropping the broker echo of our own webrtcResp"
121
+ " (len=%d) - it carries our SDP, not the camera's",
122
+ len(answer.get("sdp", "")),
123
+ )
124
+ return
111
125
  if not answer_fut.done():
112
126
  answer_fut.set_result(answer)
113
127
  return
@@ -1032,15 +1046,17 @@ class _WebRTCOpenMixin:
1032
1046
  )
1033
1047
  )
1034
1048
  return
1049
+ _from_cam = _answer_is_from_the_camera(
1050
+ src_addr=(inner.get("srcAddr")
1051
+ or msg.get("srcAddr") or ""),
1052
+ user_id=user_id)
1035
1053
  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)
1054
+ and _from_cam
1040
1055
  and _answer_sdp_can_nominate(answer.get("sdp", ""))):
1041
1056
  self._camera_answer_ice_ready_ts = time.monotonic()
1042
1057
  _deliver_webrtc_answer(
1043
- loop, answer_fut, second_answer_fut, answer
1058
+ loop, answer_fut, second_answer_fut, answer,
1059
+ from_camera=_from_cam,
1044
1060
  )
1045
1061
  elif method == "iceCandidateReq":
1046
1062
  resp_pid = inner.get("peerid")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0rc8"
7
+ version = "1.0.0rc9"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0rc8
3
+ Version: 1.0.0rc9
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,7 +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
+ | `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. Measured on an A001064: time to first media 4195 ms -> 2489 ms. **This value is the wait for a camera never seen to echo.** An echo observed at any point, including after the wait has already expired, is remembered for that camera and every later open on it waits `2.0` s again - so a fleet whose echo band is not empty pays the miss once, says so in the log, and is never shortened again. Setting this explicitly overrides both. Read on every open and clamped at 0, so a malformed or negative value falls back to the default instead of raising. | `0.25` (`2.0` once an echo has been seen) |
576
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) |
577
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`) |
578
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) |
@@ -17,8 +17,12 @@ import inspect
17
17
 
18
18
  import pytest
19
19
 
20
- from aidot_cameras.camera.webrtc_open import _answer_is_from_the_camera
20
+ from aidot_cameras.camera.webrtc_open import (
21
+ _answer_is_from_the_camera,
22
+ _deliver_webrtc_answer,
23
+ )
21
24
  from aidot_cameras.camera.sdes_open import (
25
+ _answer_ready_for_this_open,
22
26
  _answer_sdp_can_nominate,
23
27
  _parse_answer_ice,
24
28
  _stun_window_answer_exit_due,
@@ -279,3 +283,122 @@ def test_the_candidate_parse_accepts_a_line_that_ends_at_typ():
279
283
  _u, _p, cands, host = _parse_answer_ice(sdp)
280
284
  assert cands == [("192.168.0.124", 35488)]
281
285
  assert host == ()
286
+
287
+
288
+ # --------------------------------------------------------------------------- #
289
+ # Our own echo must not reach the future the nomination reads
290
+ # --------------------------------------------------------------------------- #
291
+
292
+ def test_our_own_echo_is_dropped_rather_than_routed():
293
+ """Gating only the readiness marker left the failure it was written to
294
+ prevent fully reachable: `answer_fut` is where the nomination reads its SDP,
295
+ so an echo that resolves it makes USE-CANDIDATE address our own host and
296
+ srflx candidates with our own credentials. Routing it to second_answer_fut
297
+ instead just feeds the same values to the late-credential recovery."""
298
+ import asyncio
299
+
300
+ async def _run():
301
+ loop = asyncio.get_running_loop()
302
+ first, second = loop.create_future(), loop.create_future()
303
+ _deliver_webrtc_answer(loop, first, second,
304
+ {"sdp": _OUR_ANSWER}, from_camera=False)
305
+ await asyncio.sleep(0)
306
+ assert not first.done(), "our own SDP must not resolve answer_fut"
307
+ assert not second.done(), "nor the future the late recovery reads"
308
+
309
+ _deliver_webrtc_answer(loop, first, second,
310
+ {"sdp": _ANSWER}, from_camera=True)
311
+ await asyncio.sleep(0)
312
+ assert first.done() and first.result()["sdp"] == _ANSWER
313
+
314
+ _deliver_webrtc_answer(loop, first, second,
315
+ {"sdp": _ANSWER}, from_camera=True)
316
+ await asyncio.sleep(0)
317
+ assert second.done(), "a later camera answer still reaches the recovery"
318
+
319
+ asyncio.run(_run())
320
+
321
+
322
+ def test_delivery_defaults_to_treating_an_answer_as_the_cameras():
323
+ """Every other caller predates the flag; none of them must start dropping
324
+ answers because a keyword was added."""
325
+ import inspect
326
+
327
+ sig = inspect.signature(_deliver_webrtc_answer)
328
+ assert sig.parameters["from_camera"].default is True
329
+
330
+
331
+ # --------------------------------------------------------------------------- #
332
+ # The marker belongs to one open
333
+ # --------------------------------------------------------------------------- #
334
+
335
+ def test_a_marker_from_this_open_arms_the_exit():
336
+ assert _answer_ready_for_this_open(100.0, 99.0) is True
337
+ assert _answer_ready_for_this_open(99.0, 99.0) is True
338
+
339
+
340
+ def test_a_marker_left_by_an_earlier_open_is_inert():
341
+ """The marker is per-camera state driving a per-open decision. The per-open
342
+ reset covers the sequential case; two opens overlapping on one camera object
343
+ would otherwise let one leave its window on the other's answer."""
344
+ assert _answer_ready_for_this_open(98.9, 99.0) is False
345
+
346
+
347
+ def test_no_marker_at_all_is_not_ready():
348
+ assert _answer_ready_for_this_open(None, 99.0) is False
349
+
350
+
351
+ def test_the_window_checks_the_marker_belongs_to_this_open():
352
+ src = _open_source()
353
+ assert "_answer_ready_for_this_open" in src
354
+ assert src.count("_answer_ready_for_this_open") >= 2, (
355
+ "both STUN windows must use it, not just the first")
356
+
357
+
358
+ # --------------------------------------------------------------------------- #
359
+ # One credential pair, measured rather than assumed
360
+ # --------------------------------------------------------------------------- #
361
+ #
362
+ # The nomination sends USE-CANDIDATE on BOTH sockets with the single ufrag/pwd
363
+ # this parser returns, computing the video socket's MESSAGE-INTEGRITY with what
364
+ # the parser found first. That is only correct if the camera answers one pair
365
+ # for the whole session, which nothing here proved.
366
+ #
367
+ # Measured 2026-09-04 by logging every a=ice-ufrag / a=ice-pwd line of the
368
+ # answer, per m-section, across four cold opens on two models (A001064 mains,
369
+ # A001513 battery): every answer carried ONE pair, repeated identically in the
370
+ # audio, video and application sections. Values below are synthesised - the real
371
+ # ones are per-session credentials and do not belong in a repo - but the shape
372
+ # is the shape that was captured.
373
+
374
+ _ONE_PAIR_ANSWER = "\r\n".join([
375
+ "v=0",
376
+ "m=video 9 RTP/SAVP 96",
377
+ "a=ice-ufrag:9z7F",
378
+ "a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
379
+ "a=candidate:1 1 udp 2130706431 192.168.0.124 33912 typ host",
380
+ "m=audio 9 RTP/SAVP 8",
381
+ "a=ice-ufrag:9z7F",
382
+ "a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
383
+ "m=application 9 SCTP webrtc-datachannel",
384
+ "a=ice-ufrag:9z7F",
385
+ "a=ice-pwd:mDe5uxBe1msnVNMlu8BwzQPy",
386
+ ])
387
+
388
+
389
+ def test_the_first_credential_pair_is_the_only_pair():
390
+ """So nominating both sockets with it is right, not merely lucky."""
391
+ ufrag, pwd, cands, _host = _parse_answer_ice(_ONE_PAIR_ANSWER)
392
+ assert ufrag == "9z7F"
393
+ assert pwd == "mDe5uxBe1msnVNMlu8BwzQPy"
394
+ assert cands == [("192.168.0.124", 33912)]
395
+
396
+ pairs = {
397
+ (ln[len("a=ice-ufrag:"):].strip() if ln.startswith("a=ice-ufrag:")
398
+ else ln[len("a=ice-pwd:"):].strip())
399
+ for ln in _ONE_PAIR_ANSWER.splitlines()
400
+ if ln.startswith(("a=ice-ufrag:", "a=ice-pwd:"))
401
+ }
402
+ assert pairs == {ufrag, pwd}, (
403
+ "this answer carries more than one credential pair, so the single pair"
404
+ " the nomination uses on both sockets is no longer the whole story")
@@ -94,3 +94,61 @@ def test_the_timeout_is_reported_rather_than_swallowed():
94
94
 
95
95
  src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
96
96
  assert "webrtcReq-echo elapsed" in src
97
+
98
+
99
+ # --------------------------------------------------------------------------- #
100
+ # A fleet whose echo band is not empty
101
+ # --------------------------------------------------------------------------- #
102
+ #
103
+ # The 17/17 measurement is one deployment. An echo landing between the new wait
104
+ # and the old 2.0 s would miss it, and that flag is a branch selector: it
105
+ # decides whether the webrtcResp is built, whether the ICE window runs 20 s or
106
+ # 2.5 s, and whether the quickConn reconnect retry is armed. So the shortened
107
+ # wait is not unconditional - it is the wait for a device that has never been
108
+ # seen to echo.
109
+
110
+ def test_a_device_that_has_echoed_goes_back_to_the_full_wait():
111
+ assert _sdes_echo_wait_timeout(False, echo_seen=True) == 2.0
112
+
113
+
114
+ def test_a_device_that_has_never_echoed_keeps_the_short_wait():
115
+ assert _sdes_echo_wait_timeout(False, echo_seen=False) == 0.25
116
+
117
+
118
+ def test_history_does_not_resurrect_the_wait_for_cameras_that_never_take_it():
119
+ """A001513-class cameras wait 0.0 s and are not part of this at all."""
120
+ assert _sdes_echo_wait_timeout(True, echo_seen=True) == 0.0
121
+
122
+
123
+ def test_an_explicit_override_still_wins_over_the_history(monkeypatch):
124
+ monkeypatch.setenv("AIDOT_SDES_ECHO_WAIT_S", "0.5")
125
+ assert _sdes_echo_wait_timeout(False, echo_seen=True) == 0.5
126
+
127
+
128
+ def test_the_history_defaults_off_so_the_first_open_is_still_fast():
129
+ """Callers that predate the flag must not start paying 2.0 s."""
130
+ assert _sdes_echo_wait_timeout(False) == 0.25
131
+
132
+
133
+ def test_a_late_echo_is_recorded_so_the_next_open_waits_for_it():
134
+ """Recorded, not acted on: the webrtcResp has to be sent before the relay
135
+ allocation and the ICE window, so building it after the window would be a
136
+ reordering with no camera to measure it against. Making the NEXT open wait
137
+ is the part that can be done safely."""
138
+ import inspect
139
+
140
+ from aidot_cameras.camera.client import CameraMixin
141
+
142
+ src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
143
+ assert "_sdes_echo_seen" in src
144
+ assert "AIDOT_SDES_ECHO_WAIT_S=%.1f" in src, (
145
+ "the line must name the knob that makes it permanent")
146
+
147
+
148
+ def test_the_wait_reads_the_devices_history():
149
+ import inspect
150
+
151
+ from aidot_cameras.camera.client import CameraMixin
152
+
153
+ src = inspect.getsource(CameraMixin._open_sdes_stream_impl)
154
+ assert "echo_seen=" in src