python-aidot-cameras 1.0.0b30__tar.gz → 1.0.0b32__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 (269) hide show
  1. {python_aidot_cameras-1.0.0b30/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b32}/PKG-INFO +2 -1
  2. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/README.md +1 -0
  3. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/client.py +77 -0
  4. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/hwaccel.py +3 -3
  5. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/protocol.py +17 -7
  6. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sdes.py +10 -1
  7. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sdes_open.py +248 -62
  8. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/pyproject.toml +1 -1
  9. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
  10. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/SOURCES.txt +4 -0
  11. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_env_seams.py +2 -1
  12. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_hwaccel_probe.py +1 -1
  13. python_aidot_cameras-1.0.0b32/tests/test_media_path_is_reported.py +88 -0
  14. python_aidot_cameras-1.0.0b32/tests/test_sdes_connection_mode.py +221 -0
  15. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_sprop.py +24 -5
  16. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_security_hardening.py +2 -2
  17. python_aidot_cameras-1.0.0b32/tests/test_serve_input_timeout.py +65 -0
  18. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_unstable_public.py +3 -3
  19. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_write_is_reported_truthfully.py +1 -1
  20. python_aidot_cameras-1.0.0b32/tests/test_stall_nudge_policy.py +51 -0
  21. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/LICENSE +0 -0
  22. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/MANIFEST.in +0 -0
  23. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/__init__.py +0 -0
  24. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/__main__.py +0 -0
  25. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_upstream.py +0 -0
  26. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/__init__.py +0 -0
  27. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  28. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  29. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  30. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  31. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  32. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  33. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  34. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  35. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  36. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  37. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  38. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  39. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  40. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  41. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  42. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  43. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  44. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  45. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  46. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  47. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  48. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  49. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  50. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  51. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  52. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  53. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  54. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  55. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  56. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  57. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  58. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  59. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  60. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/ble_gateway.py +0 -0
  61. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/__init__.py +0 -0
  62. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/constants.py +0 -0
  63. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/controls.py +0 -0
  64. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/go2rtc.py +0 -0
  65. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/lan_control.py +0 -0
  66. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/models.py +0 -0
  67. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/playback.py +0 -0
  68. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_events.py +0 -0
  69. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_listing.py +0 -0
  70. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_playback.py +0 -0
  71. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/tutk.py +0 -0
  72. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/webrtc.py +0 -0
  73. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/webrtc_open.py +0 -0
  74. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/client.py +0 -0
  75. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/cloud_auth.py +0 -0
  76. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/const.py +0 -0
  77. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/credentials.py +0 -0
  78. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/crypto.py +0 -0
  79. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/device_client.py +0 -0
  80. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/discover.py +0 -0
  81. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/exceptions.py +0 -0
  82. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/g711.py +0 -0
  83. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/lan_retry.py +0 -0
  84. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/aidot_cameras/py.typed +0 -0
  85. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  86. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  87. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/requires.txt +0 -0
  88. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  89. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/setup.cfg +0 -0
  90. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_aioice_compat.py +0 -0
  91. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_alarm_event.py +0 -0
  92. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_answer_sdp_video_pt.py +0 -0
  93. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_answer_section_selection.py +0 -0
  94. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_audio_mux_offline.py +0 -0
  95. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  96. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_avio_request.py +0 -0
  97. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_avio_response_parse.py +0 -0
  98. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_avio_response_router.py +0 -0
  99. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_backoff.py +0 -0
  100. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_battery_detection.py +0 -0
  101. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_battery_evidence_corroboration.py +0 -0
  102. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_battery_skip_turn.py +0 -0
  103. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_bitrate_campaign_arms.py +0 -0
  104. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_ble_gateway.py +0 -0
  105. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_bridge_state_sentinels.py +0 -0
  106. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_busy_backoff.py +0 -0
  107. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_canary_log_names_the_camera.py +0 -0
  108. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_cloud_count.py +0 -0
  109. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_cloud_plan.py +0 -0
  110. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_constants_imports_resolve.py +0 -0
  111. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_control_verdicts.py +0 -0
  112. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_deferred_hardening.py +0 -0
  113. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_device_cmd_seq_match.py +0 -0
  114. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_device_login_guard.py +0 -0
  115. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_device_user_info_cache.py +0 -0
  116. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_direct_ts_serve.py +0 -0
  117. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dormant_camera_cleanup.py +0 -0
  118. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_drain_is_cancellable.py +0 -0
  119. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_mux_terminal_write.py +0 -0
  120. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_not_ready_burst.py +0 -0
  121. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_pinning.py +0 -0
  122. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_resolution_ack.py +0 -0
  123. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_decode_skip.py +0 -0
  124. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_ice_wait.py +0 -0
  125. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_open_timeout.py +0 -0
  126. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_skip_signaling_wait.py +0 -0
  127. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_slow_probe.py +0 -0
  128. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_stdout_producer.py +0 -0
  129. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_video_presence.py +0 -0
  130. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_egress_guard.py +0 -0
  131. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  132. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  133. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_futile_keepalive.py +0 -0
  134. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  135. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_go2rtc.py +0 -0
  136. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_go2rtc_cli.py +0 -0
  137. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_h264_decode_rate_limit.py +0 -0
  138. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_highport_nomination.py +0 -0
  139. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_ice_config_cache.py +0 -0
  140. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_ice_peer_reflexive.py +0 -0
  141. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_idle_release_both_loops.py +0 -0
  142. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_keepalive_renew.py +0 -0
  143. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_key_restart_sdp.py +0 -0
  144. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_keyframe_prompter.py +0 -0
  145. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_lan_control.py +0 -0
  146. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  147. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_late_repeats_are_dropped.py +0 -0
  148. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_light_active_color_mode.py +0 -0
  149. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_live_session_carries_talk.py +0 -0
  150. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_live_stream_param.py +0 -0
  151. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_live_validate_policy.py +0 -0
  152. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_live_video_canary.py +0 -0
  153. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_local_only_controls.py +0 -0
  154. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_logging_caps.py +0 -0
  155. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_login_connect_deadline.py +0 -0
  156. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_login_reconnect_backoff.py +0 -0
  157. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_media_counted_only_when_plaintext.py +0 -0
  158. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_media_socket_rcvbuf.py +0 -0
  159. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_media_wait_floor.py +0 -0
  160. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_motion_poll.py +0 -0
  161. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_client_id_is_exact.py +0 -0
  162. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_credential_refetch.py +0 -0
  163. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_credential_self_heal.py +0 -0
  164. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_password_not_persisted.py +0 -0
  165. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_session_reconnect.py +0 -0
  166. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  167. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_nack.py +0 -0
  168. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  169. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_narrow_pc_ice.py +0 -0
  170. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_no_key_material_in_logs.py +0 -0
  171. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_no_self_referential_go2rtc.py +0 -0
  172. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_no_undefined_names.py +0 -0
  173. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_offer_bandwidth_line.py +0 -0
  174. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  175. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_offline_keepalive_pause.py +0 -0
  176. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_on_frame_is_an_av_frame.py +0 -0
  177. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_open_fail_logger.py +0 -0
  178. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_open_gate_delay.py +0 -0
  179. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_peerid_reuse.py +0 -0
  180. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_persistent_mqtt.py +0 -0
  181. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_persistent_mqtt_robustness.py +0 -0
  182. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_playback_tls.py +0 -0
  183. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_post_merge_hardening.py +0 -0
  184. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  185. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_public_signature_matches_impl.py +0 -0
  186. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_quality_campaign_in_session.py +0 -0
  187. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_recent_recordings.py +0 -0
  188. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_remb.py +0 -0
  189. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  190. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_resent_frames_are_dropped.py +0 -0
  191. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_resolution_persists.py +0 -0
  192. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_restart_cycle.py +0 -0
  193. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_retry_policy.py +0 -0
  194. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_scripts_importable.py +0 -0
  195. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sctp_init_ack_tsn.py +0 -0
  196. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sd_card_presence.py +0 -0
  197. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sd_event_decode.py +0 -0
  198. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sd_event_probe_payloads.py +0 -0
  199. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sd_listing.py +0 -0
  200. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sd_playback.py +0 -0
  201. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_adaptive.py +0 -0
  202. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_avio_dispatch.py +0 -0
  203. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_bridge_break_gate.py +0 -0
  204. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_echo_wait_timeout.py +0 -0
  205. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_fast_liveplay.py +0 -0
  206. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  207. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_first_media_stall_report.py +0 -0
  208. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_idle_release.py +0 -0
  209. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_nack_bridge.py +0 -0
  210. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_offer_video_order.py +0 -0
  211. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_offer_video_pin.py +0 -0
  212. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_open_cleanup.py +0 -0
  213. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_sctp_abort.py +0 -0
  214. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_audio.py +0 -0
  215. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_cmd.py +0 -0
  216. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_stderr_drain.py +0 -0
  217. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_talk.py +0 -0
  218. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_teardown_exit_log.py +0 -0
  219. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_terminal_ack.py +0 -0
  220. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_terminal_ack_latency.py +0 -0
  221. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_tmmbr_bridge.py +0 -0
  222. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_watchdog.py +0 -0
  223. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdp_narrowing_properties.py +0 -0
  224. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sdp_payload_type_narrowing.py +0 -0
  225. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  226. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_serializable_login_info.py +0 -0
  227. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_serve_loss_tolerance.py +0 -0
  228. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_serve_pt_respects_the_pin.py +0 -0
  229. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_serve_relay.py +0 -0
  230. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  231. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_session_stats.py +0 -0
  232. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_shared_house_seam.py +0 -0
  233. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_snapshot_cancellation.py +0 -0
  234. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_speak.py +0 -0
  235. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_capture_gate.py +0 -0
  236. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_staleness.py +0 -0
  237. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_srtp_rx_rekey.py +0 -0
  238. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_srtp_tx_key_note.py +0 -0
  239. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  240. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  241. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  242. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  243. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  244. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stream_cap.py +0 -0
  245. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stream_drain_ownership.py +0 -0
  246. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stream_idle.py +0 -0
  247. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stream_slot_cap.py +0 -0
  248. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_stream_teardown.py +0 -0
  249. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_talk.py +0 -0
  250. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  251. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_tap_to_mux_end_to_end.py +0 -0
  252. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_terminal_ack.py +0 -0
  253. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_tmmbr.py +0 -0
  254. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_token_file_robustness.py +0 -0
  255. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_token_refresh.py +0 -0
  256. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_turn_create_permission.py +0 -0
  257. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  258. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_upstream_compat.py +0 -0
  259. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_validator_shares_one_token.py +0 -0
  260. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_video_profile_log.py +0 -0
  261. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_video_pts_dts.py +0 -0
  262. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_video_ts_unwrap.py +0 -0
  263. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_check_throttle.py +0 -0
  264. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_presence.py +0 -0
  265. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_present_push_mode.py +0 -0
  266. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_wait_or_event.py +0 -0
  267. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_wake_gate_camera_evidence.py +0 -0
  268. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/tests/test_wake_readiness_retry.py +0 -0
  269. {python_aidot_cameras-1.0.0b30 → python_aidot_cameras-1.0.0b32}/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.0b30
3
+ Version: 1.0.0b32
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
@@ -415,6 +415,7 @@ audio, idle release, the sprop cache path) are documented in
415
415
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
416
416
  | `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
417
417
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
418
+ | `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
418
419
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
419
420
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
420
421
  | `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) |
@@ -375,6 +375,7 @@ audio, idle release, the sprop cache path) are documented in
375
375
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
376
376
  | `AIDOT_FAST_CONNECT` | Enable LAN-direct "fast connect" (STUN-only, skips several cloud signaling waits) when truthy. On-LAN only - off-subnet/strict-NAT viewers must leave it off. | unset (off) |
377
377
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
378
+ | `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
378
379
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
379
380
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
380
381
  | `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) |
@@ -566,6 +566,36 @@ _SERVE_REORDER_QUEUE = int(os.environ.get("AIDOT_SERVE_REORDER_QUEUE", "500"))
566
566
  #: failure this is meant to prevent.
567
567
  _SERVE_MAX_DELAY_US = int(os.environ.get("AIDOT_SERVE_MAX_DELAY_US", "500000"))
568
568
 
569
+ #: How long (seconds) the serve ffmpeg waits with NO packets on any RTP socket
570
+ #: before its SDP input errors out ("Error during demuxing: Operation timed
571
+ #: out") and the serve dies. ffmpeg's own default is 10 s (READ_PACKET_TIMEOUT_S,
572
+ #: applied through the sdp demuxer's ``listen_timeout`` option). Measured on an
573
+ #: A001064 (2026-08-24/25): the camera episodically stops transmitting for
574
+ #: >=10 s mid-session, and every reopen afterwards succeeds immediately, so the
575
+ #: link recovers on a 10-20 s scale. A mains camera therefore gets 30 s to ride
576
+ #: the gap out; a battery camera keeps ffmpeg's 10 s default, because its stops
577
+ #: are real sleeps that only a reopen (with its wake) ends - waiting longer just
578
+ #: freezes the picture. ``AIDOT_SERVE_INPUT_TIMEOUT_S`` overrides both.
579
+ _SERVE_INPUT_TIMEOUT_MAINS_S = 30
580
+ _SERVE_INPUT_TIMEOUT_BATTERY_S = 10
581
+
582
+
583
+ def _resolve_serve_input_timeout_s(is_battery: bool) -> int:
584
+ """Seconds the serve's SDP input tolerates total silence before erroring.
585
+
586
+ Pure policy (unit-testable): the env override wins for every camera; else
587
+ mains rides out a transmit gap and battery keeps the fast default.
588
+ """
589
+ _env = os.environ.get("AIDOT_SERVE_INPUT_TIMEOUT_S")
590
+ if _env:
591
+ try:
592
+ return max(1, int(_env))
593
+ except ValueError:
594
+ pass
595
+ return (_SERVE_INPUT_TIMEOUT_BATTERY_S if is_battery
596
+ else _SERVE_INPUT_TIMEOUT_MAINS_S)
597
+
598
+
569
599
  #: How long to wait after a camera answers -50002 / -50015 ("no free session").
570
600
  #: Both loops used to wait _MAX_DELAY (300s) on the belief that the camera
571
601
  #: "releases slowly". Measured 2026-08-07 on an A001064: reopening 2s after a
@@ -630,6 +660,7 @@ def _build_sdes_serve_cmd(
630
660
  audio_gain_db: float = -8.0,
631
661
  push_video_only: bool = False,
632
662
  video_decoder: Optional[list] = None,
663
+ input_timeout_s: Optional[int] = None,
633
664
  ) -> list:
634
665
  """Build the ffmpeg argv for the SDES bridge serve.
635
666
 
@@ -778,6 +809,13 @@ def _build_sdes_serve_cmd(
778
809
  # before giving up on a gap rather than stalling the pipeline.
779
810
  "-reorder_queue_size", str(_SERVE_REORDER_QUEUE),
780
811
  "-max_delay", str(_SERVE_MAX_DELAY_US),
812
+ # Input silence tolerance: the sdp demuxer's ``listen_timeout`` doubles
813
+ # as its packet-read timeout (READ_PACKET_TIMEOUT_S, ffmpeg default
814
+ # 10 s). See _resolve_serve_input_timeout_s for the mains/battery
815
+ # policy. Input option, so it must sit before -i; None keeps ffmpeg's
816
+ # default (no behavior change for callers that do not opt in).
817
+ *(["-listen_timeout", str(int(input_timeout_s))]
818
+ if input_timeout_s is not None else []),
781
819
  "-i", sdp_path,
782
820
  *dest_args,
783
821
  ]
@@ -3540,6 +3578,7 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
3540
3578
  stream_idle_s: Optional[float] = None,
3541
3579
  sdes_fast_liveplay: Optional[bool] = None,
3542
3580
  sdes_skip_turn: Optional[bool] = None,
3581
+ sdes_connection_mode: Optional[str] = None,
3543
3582
  sdes_adaptive: Optional[bool] = None,
3544
3583
  sdes_audio_gain_db: Optional[float] = None,
3545
3584
  ) -> None:
@@ -3616,6 +3655,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
3616
3655
  self._sdes_fast_liveplay_opt = sdes_fast_liveplay
3617
3656
  if sdes_skip_turn is not None:
3618
3657
  self._sdes_skip_turn_opt = sdes_skip_turn
3658
+ if sdes_connection_mode is not None:
3659
+ self._sdes_connection_mode_opt = sdes_connection_mode
3619
3660
  if sdes_adaptive is not None:
3620
3661
  self._sdes_adaptive_opt = sdes_adaptive
3621
3662
  if self._stream_task is not None and not self._stream_task.done():
@@ -4701,6 +4742,33 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4701
4742
  )
4702
4743
  return False
4703
4744
 
4745
+ def _resolve_sdes_connection_mode(self) -> str:
4746
+ """Which media path the session should offer: auto, lan, or relay.
4747
+
4748
+ ``auto`` (default) offers every reachable candidate and lets ICE
4749
+ priority prefer the LAN, with the relay as last resort - the shipped
4750
+ behaviour, verified on the full fleet (six of seven direct, only the
4751
+ unreachable unit relayed). ``lan`` drops the relay entirely (routed
4752
+ through _resolve_sdes_skip_turn so the battery force-keep applies).
4753
+ ``relay`` offers only the relay candidate and force-keeps the
4754
+ pre-allocation.
4755
+
4756
+ Per-open ``sdes_connection_mode`` wins; else AIDOT_SDES_CONNECTION_MODE;
4757
+ else auto. Anything unrecognised is auto - this is read while opening
4758
+ a stream, and a typo must not stop video.
4759
+ """
4760
+ for raw in (getattr(self, "_sdes_connection_mode_opt", None),
4761
+ os.environ.get("AIDOT_SDES_CONNECTION_MODE")):
4762
+ if raw is None:
4763
+ continue
4764
+ mode = str(raw).strip().lower()
4765
+ if mode in ("auto", "lan", "relay"):
4766
+ return mode
4767
+ _LOGGER.warning(
4768
+ "camera %s: unknown sdes_connection_mode %r; using auto",
4769
+ getattr(self, "device_id", "?"), raw)
4770
+ return "auto"
4771
+
4704
4772
  def _resolve_sdes_skip_turn(self) -> bool:
4705
4773
  """EXPERIMENTAL (opt-in, default off): skip the blocking SDES TURN relay
4706
4774
  pre-allocation, for cameras reachable LAN-direct.
@@ -4730,6 +4798,15 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
4730
4798
  candidate on the HA segment."""
4731
4799
  if getattr(self, "is_battery_camera", False):
4732
4800
  return False
4801
+ # The connection mode composes here rather than in a parallel path, so
4802
+ # everything hanging off this resolver (instrumentation, the adaptive
4803
+ # interplay) sees one answer. relay force-keeps the pre-allocation -
4804
+ # forcing the relay while skipping its allocation would offer nothing.
4805
+ _mode = self._resolve_sdes_connection_mode()
4806
+ if _mode == "lan":
4807
+ return True
4808
+ if _mode == "relay":
4809
+ return False
4733
4810
  opt = getattr(self, "_sdes_skip_turn_opt", None)
4734
4811
  if opt is not None:
4735
4812
  return bool(opt)
@@ -40,7 +40,7 @@ import threading
40
40
  import time
41
41
  from typing import List, Optional
42
42
 
43
- from .protocol import _SPROP_DIR # reuse the same per-user cache root
43
+ from .protocol import _sprop_dir # reuse the same per-user cache root
44
44
 
45
45
  _LOGGER = logging.getLogger(__name__)
46
46
 
@@ -139,7 +139,7 @@ def _ffmpeg_identity() -> str:
139
139
 
140
140
 
141
141
  def _cache_path() -> str:
142
- return os.path.join(_SPROP_DIR, "decoders.json")
142
+ return os.path.join(_sprop_dir(), "decoders.json")
143
143
 
144
144
 
145
145
  def _load_cache() -> dict:
@@ -152,7 +152,7 @@ def _load_cache() -> dict:
152
152
 
153
153
  def _save_cache(data: dict) -> None:
154
154
  try:
155
- os.makedirs(_SPROP_DIR, exist_ok=True)
155
+ os.makedirs(_sprop_dir(), exist_ok=True)
156
156
  tmp = _cache_path() + ".tmp"
157
157
  with open(tmp, "w") as fh:
158
158
  json.dump(data, fh)
@@ -684,8 +684,18 @@ _XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME") or os.path.join(
684
684
  os.path.expanduser("~"), ".config")
685
685
 
686
686
 
687
- _SPROP_DIR = os.environ.get("AIDOT_SPROP_DIR") or os.path.join(
688
- _XDG_CONFIG_HOME, "aidot", "sprop")
687
+ def _sprop_dir() -> str:
688
+ """The sprop cache directory - AIDOT_SPROP_DIR read at CALL time.
689
+
690
+ A consumer embedded in a larger process (the Home Assistant integration)
691
+ can only learn its persistent storage path at setup, after this module is
692
+ long imported; a module-level snapshot of the env var silently ignored
693
+ that setting and the cache landed back in the container-ephemeral home
694
+ directory, so every container recreation cost a startup-churn window.
695
+ Same lesson as cloud_auth's country default: bind config at call time.
696
+ """
697
+ return os.environ.get("AIDOT_SPROP_DIR") or os.path.join(
698
+ _XDG_CONFIG_HOME, "aidot", "sprop")
689
699
 
690
700
 
691
701
  def _extract_param_sets_from_rtp(pkt: bytes) -> dict:
@@ -740,12 +750,12 @@ def _build_sprop(sps: bytes, pps: bytes) -> str:
740
750
  def _sprop_cache_path(devid: str) -> str:
741
751
  # `devid` originates from the cloud/device and is interpolated into a
742
752
  # filesystem path, so a value containing "/", "\\", ".." or an absolute path
743
- # could escape _SPROP_DIR (arbitrary read/write). Reduce it to a safe,
753
+ # could escape the sprop dir (arbitrary read/write). Reduce it to a safe,
744
754
  # collision-resistant filename component.
745
755
  safe = re.sub(r"[^A-Za-z0-9_-]", "_", devid or "")
746
756
  if not safe or len(devid or "") > 128:
747
757
  safe = hashlib.sha256((devid or "").encode("utf-8", "replace")).hexdigest()
748
- return os.path.join(_SPROP_DIR, f"{safe}.sprop")
758
+ return os.path.join(_sprop_dir(), f"{safe}.sprop")
749
759
 
750
760
 
751
761
  def _sprop_unstable_path(devid: str) -> str:
@@ -834,14 +844,14 @@ def _save_sprop(devid: str, sprop: str) -> bool:
834
844
  devid,
835
845
  )
836
846
  try:
837
- os.makedirs(_SPROP_DIR, exist_ok=True)
847
+ os.makedirs(_sprop_dir(), exist_ok=True)
838
848
  with open(_sprop_unstable_path(devid), "w") as fh:
839
849
  fh.write("parameter sets changed between sessions\n")
840
850
  os.remove(_sprop_cache_path(devid))
841
851
  except OSError:
842
852
  pass
843
853
  return False
844
- os.makedirs(_SPROP_DIR, exist_ok=True)
854
+ os.makedirs(_sprop_dir(), exist_ok=True)
845
855
  tmp = _sprop_cache_path(devid) + ".tmp"
846
856
  with open(tmp, "w") as fh:
847
857
  fh.write(sprop)
@@ -851,7 +861,7 @@ def _save_sprop(devid: str, sprop: str) -> bool:
851
861
  # Surface (don't swallow): if the cache dir isn't writable the whole
852
862
  # sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
853
863
  _LOGGER.warning("sprop cache write failed (%s): %s - set AIDOT_SPROP_DIR "
854
- "to a writable path", _SPROP_DIR, exc)
864
+ "to a writable path", _sprop_dir(), exc)
855
865
  return False
856
866
 
857
867
 
@@ -53,6 +53,7 @@ class SdesSession(AvioRequestMixin):
53
53
  teardown_requested=None,
54
54
  first_video_pt=None,
55
55
  first_audio_pt=None,
56
+ media_path=None,
56
57
  device_id=None,
57
58
  responses=None,
58
59
  abort_chan=None,
@@ -81,6 +82,12 @@ class SdesSession(AvioRequestMixin):
81
82
  # sent (video 96/97/98, audio 0/8), or None before first media.
82
83
  self._first_video_pt = first_video_pt if first_video_pt is not None else [None]
83
84
  self._first_audio_pt = first_audio_pt if first_audio_pt is not None else [None]
85
+ # Shared with the bridge thread: [0] becomes "direct" or "relay" when
86
+ # the first media packet arrives, classified by its source address
87
+ # against the session's TURN servers. The connection mode is only a
88
+ # preference - the camera decides where to send from - and this is the
89
+ # receipt that says what actually happened.
90
+ self._media_path = media_path if media_path is not None else [None]
84
91
  # Mutable one-element list shared with the bridge thread: [0] flips True
85
92
  # the moment ANY locally-initiated ffmpeg-kill path fires (this stop(),
86
93
  # the cold-open _reap(), the key-restart proc replace, or the DTLS-
@@ -229,7 +236,8 @@ class SdesSession(AvioRequestMixin):
229
236
  the DTLS path allows. These counters are the supported substitute:
230
237
  non-zero ``packets``/``bytes`` means real media reached the consumer.
231
238
 
232
- Returns ``{packets, bytes, last_media_monotonic, video_pt, audio_pt}``;
239
+ Returns ``{packets, bytes, last_media_monotonic, video_pt, audio_pt,
240
+ media_path}``;
233
241
  the payload types are ``None`` until the camera's first packet of that
234
242
  kind arrives.
235
243
  """
@@ -239,6 +247,7 @@ class SdesSession(AvioRequestMixin):
239
247
  "last_media_monotonic": self._media_progress[0],
240
248
  "video_pt": self._first_video_pt[0],
241
249
  "audio_pt": self._first_audio_pt[0],
250
+ "media_path": self._media_path[0],
242
251
  }
243
252
 
244
253
  def _drained_stderr_tail(self) -> bytes: