python-aidot-cameras 1.0.0b31__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.0b31/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b32}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/client.py +38 -0
  3. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sdes_open.py +104 -10
  4. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/pyproject.toml +1 -1
  5. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
  7. python_aidot_cameras-1.0.0b32/tests/test_serve_input_timeout.py +65 -0
  8. python_aidot_cameras-1.0.0b32/tests/test_stall_nudge_policy.py +51 -0
  9. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/LICENSE +0 -0
  10. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/README.md +0 -0
  12. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/__init__.py +0 -0
  13. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/__main__.py +0 -0
  14. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_upstream.py +0 -0
  15. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/__init__.py +0 -0
  16. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  17. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  19. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  20. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  21. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  22. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  23. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  24. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  25. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  26. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  27. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  28. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  29. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  30. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  31. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  32. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  33. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  34. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  35. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  36. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  37. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  38. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  39. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  40. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  41. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  42. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  43. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  44. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  45. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  46. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  47. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  48. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  49. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/ble_gateway.py +0 -0
  50. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/__init__.py +0 -0
  51. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/constants.py +0 -0
  52. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/controls.py +0 -0
  53. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/go2rtc.py +0 -0
  54. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/hwaccel.py +0 -0
  55. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/lan_control.py +0 -0
  56. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/models.py +0 -0
  57. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/playback.py +0 -0
  58. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/protocol.py +0 -0
  59. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_events.py +0 -0
  60. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_listing.py +0 -0
  61. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sd_playback.py +0 -0
  62. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/sdes.py +0 -0
  63. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/tutk.py +0 -0
  64. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/webrtc.py +0 -0
  65. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/camera/webrtc_open.py +0 -0
  66. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/client.py +0 -0
  67. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/cloud_auth.py +0 -0
  68. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/const.py +0 -0
  69. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/credentials.py +0 -0
  70. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/crypto.py +0 -0
  71. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/device_client.py +0 -0
  72. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/discover.py +0 -0
  73. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/exceptions.py +0 -0
  74. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/g711.py +0 -0
  75. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/lan_retry.py +0 -0
  76. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/aidot_cameras/py.typed +0 -0
  77. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  78. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  79. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/requires.txt +0 -0
  80. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  81. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/setup.cfg +0 -0
  82. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_aioice_compat.py +0 -0
  83. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_alarm_event.py +0 -0
  84. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_answer_sdp_video_pt.py +0 -0
  85. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_answer_section_selection.py +0 -0
  86. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_audio_mux_offline.py +0 -0
  87. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  88. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_avio_request.py +0 -0
  89. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_avio_response_parse.py +0 -0
  90. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_avio_response_router.py +0 -0
  91. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_backoff.py +0 -0
  92. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_battery_detection.py +0 -0
  93. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_battery_evidence_corroboration.py +0 -0
  94. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_battery_skip_turn.py +0 -0
  95. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_bitrate_campaign_arms.py +0 -0
  96. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_ble_gateway.py +0 -0
  97. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_bridge_state_sentinels.py +0 -0
  98. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_busy_backoff.py +0 -0
  99. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_canary_log_names_the_camera.py +0 -0
  100. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_cloud_count.py +0 -0
  101. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_cloud_plan.py +0 -0
  102. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_constants_imports_resolve.py +0 -0
  103. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_control_verdicts.py +0 -0
  104. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_deferred_hardening.py +0 -0
  105. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_device_cmd_seq_match.py +0 -0
  106. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_device_login_guard.py +0 -0
  107. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_device_user_info_cache.py +0 -0
  108. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_direct_ts_serve.py +0 -0
  109. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dormant_camera_cleanup.py +0 -0
  110. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_drain_is_cancellable.py +0 -0
  111. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_mux_terminal_write.py +0 -0
  112. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_not_ready_burst.py +0 -0
  113. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_pinning.py +0 -0
  114. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_resolution_ack.py +0 -0
  115. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_decode_skip.py +0 -0
  116. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_ice_wait.py +0 -0
  117. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_serve_open_timeout.py +0 -0
  118. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_skip_signaling_wait.py +0 -0
  119. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_slow_probe.py +0 -0
  120. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_stdout_producer.py +0 -0
  121. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_dtls_video_presence.py +0 -0
  122. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_egress_guard.py +0 -0
  123. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_env_seams.py +0 -0
  124. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  125. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  126. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_futile_keepalive.py +0 -0
  127. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  128. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_go2rtc.py +0 -0
  129. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_go2rtc_cli.py +0 -0
  130. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_h264_decode_rate_limit.py +0 -0
  131. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_highport_nomination.py +0 -0
  132. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_hwaccel_probe.py +0 -0
  133. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_ice_config_cache.py +0 -0
  134. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_ice_peer_reflexive.py +0 -0
  135. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_idle_release_both_loops.py +0 -0
  136. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_keepalive_renew.py +0 -0
  137. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_key_restart_sdp.py +0 -0
  138. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_keyframe_prompter.py +0 -0
  139. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_lan_control.py +0 -0
  140. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  141. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_late_repeats_are_dropped.py +0 -0
  142. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_light_active_color_mode.py +0 -0
  143. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_live_session_carries_talk.py +0 -0
  144. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_live_stream_param.py +0 -0
  145. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_live_validate_policy.py +0 -0
  146. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_live_video_canary.py +0 -0
  147. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_local_only_controls.py +0 -0
  148. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_logging_caps.py +0 -0
  149. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_login_connect_deadline.py +0 -0
  150. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_login_reconnect_backoff.py +0 -0
  151. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_media_counted_only_when_plaintext.py +0 -0
  152. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_media_path_is_reported.py +0 -0
  153. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_media_socket_rcvbuf.py +0 -0
  154. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_media_wait_floor.py +0 -0
  155. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_motion_poll.py +0 -0
  156. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_client_id_is_exact.py +0 -0
  157. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_credential_refetch.py +0 -0
  158. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_credential_self_heal.py +0 -0
  159. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_password_not_persisted.py +0 -0
  160. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_session_reconnect.py +0 -0
  161. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  162. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_nack.py +0 -0
  163. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  164. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_narrow_pc_ice.py +0 -0
  165. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_no_key_material_in_logs.py +0 -0
  166. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_no_self_referential_go2rtc.py +0 -0
  167. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_no_undefined_names.py +0 -0
  168. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_offer_bandwidth_line.py +0 -0
  169. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  170. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_offline_keepalive_pause.py +0 -0
  171. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_on_frame_is_an_av_frame.py +0 -0
  172. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_open_fail_logger.py +0 -0
  173. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_open_gate_delay.py +0 -0
  174. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_peerid_reuse.py +0 -0
  175. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_persistent_mqtt.py +0 -0
  176. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_persistent_mqtt_robustness.py +0 -0
  177. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_playback_tls.py +0 -0
  178. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_post_merge_hardening.py +0 -0
  179. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  180. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_public_signature_matches_impl.py +0 -0
  181. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_quality_campaign_in_session.py +0 -0
  182. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_recent_recordings.py +0 -0
  183. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_remb.py +0 -0
  184. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  185. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_resent_frames_are_dropped.py +0 -0
  186. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_resolution_persists.py +0 -0
  187. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_restart_cycle.py +0 -0
  188. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_retry_policy.py +0 -0
  189. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_scripts_importable.py +0 -0
  190. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sctp_init_ack_tsn.py +0 -0
  191. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sd_card_presence.py +0 -0
  192. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sd_event_decode.py +0 -0
  193. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sd_event_probe_payloads.py +0 -0
  194. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sd_listing.py +0 -0
  195. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sd_playback.py +0 -0
  196. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_adaptive.py +0 -0
  197. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_avio_dispatch.py +0 -0
  198. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_bridge_break_gate.py +0 -0
  199. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_connection_mode.py +0 -0
  200. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_echo_wait_timeout.py +0 -0
  201. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_fast_liveplay.py +0 -0
  202. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  203. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_first_media_stall_report.py +0 -0
  204. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_idle_release.py +0 -0
  205. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_nack_bridge.py +0 -0
  206. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_offer_video_order.py +0 -0
  207. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_offer_video_pin.py +0 -0
  208. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_open_cleanup.py +0 -0
  209. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_sctp_abort.py +0 -0
  210. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_audio.py +0 -0
  211. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_cmd.py +0 -0
  212. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_serve_stderr_drain.py +0 -0
  213. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_sprop.py +0 -0
  214. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_talk.py +0 -0
  215. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_teardown_exit_log.py +0 -0
  216. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_terminal_ack.py +0 -0
  217. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_terminal_ack_latency.py +0 -0
  218. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_tmmbr_bridge.py +0 -0
  219. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdes_watchdog.py +0 -0
  220. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdp_narrowing_properties.py +0 -0
  221. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sdp_payload_type_narrowing.py +0 -0
  222. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_security_hardening.py +0 -0
  223. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  224. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_serializable_login_info.py +0 -0
  225. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_serve_loss_tolerance.py +0 -0
  226. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_serve_pt_respects_the_pin.py +0 -0
  227. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_serve_relay.py +0 -0
  228. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  229. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_session_stats.py +0 -0
  230. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_shared_house_seam.py +0 -0
  231. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_snapshot_cancellation.py +0 -0
  232. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_speak.py +0 -0
  233. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_capture_gate.py +0 -0
  234. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_staleness.py +0 -0
  235. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_unstable_public.py +0 -0
  236. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  237. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_srtp_rx_rekey.py +0 -0
  238. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_srtp_tx_key_note.py +0 -0
  239. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  240. {python_aidot_cameras-1.0.0b31 → 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.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  242. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  243. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  244. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stream_cap.py +0 -0
  245. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stream_drain_ownership.py +0 -0
  246. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stream_idle.py +0 -0
  247. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stream_slot_cap.py +0 -0
  248. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_stream_teardown.py +0 -0
  249. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_talk.py +0 -0
  250. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  251. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_tap_to_mux_end_to_end.py +0 -0
  252. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_terminal_ack.py +0 -0
  253. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_tmmbr.py +0 -0
  254. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_token_file_robustness.py +0 -0
  255. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_token_refresh.py +0 -0
  256. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_turn_create_permission.py +0 -0
  257. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  258. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_upstream_compat.py +0 -0
  259. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_validator_shares_one_token.py +0 -0
  260. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_video_profile_log.py +0 -0
  261. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_video_pts_dts.py +0 -0
  262. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_video_ts_unwrap.py +0 -0
  263. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_check_throttle.py +0 -0
  264. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_presence.py +0 -0
  265. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_viewer_present_push_mode.py +0 -0
  266. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_wait_or_event.py +0 -0
  267. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_wake_gate_camera_evidence.py +0 -0
  268. {python_aidot_cameras-1.0.0b31 → python_aidot_cameras-1.0.0b32}/tests/test_wake_readiness_retry.py +0 -0
  269. {python_aidot_cameras-1.0.0b31 → 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.0b31
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
@@ -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
  ]
@@ -1218,6 +1218,42 @@ def _classify_ffmpeg_exit(rc: int, teardown_requested: bool) -> int:
1218
1218
  return logging.WARNING
1219
1219
 
1220
1220
 
1221
+ #: Mid-session stall nudge (see _stall_nudge_due): enabled by default;
1222
+ #: ``AIDOT_SDES_STALL_NUDGE=0`` turns it off and
1223
+ #: ``AIDOT_SDES_STALL_NUDGE_AFTER_S`` moves the trigger point.
1224
+ _STALL_NUDGE_ENABLED = os.environ.get("AIDOT_SDES_STALL_NUDGE", "1") != "0"
1225
+ _STALL_NUDGE_AFTER_S = float(
1226
+ os.environ.get("AIDOT_SDES_STALL_NUDGE_AFTER_S", "2.5"))
1227
+
1228
+
1229
+ def _stall_nudge_due(
1230
+ *,
1231
+ silence_s: float,
1232
+ nudges_sent: int,
1233
+ since_last_nudge_s: float,
1234
+ stall_after_s: float = 2.5,
1235
+ max_nudges: int = 3,
1236
+ interval_s: float = 2.0,
1237
+ ) -> bool:
1238
+ """Whether the bridge should re-send AVIO LIVING for a mid-session stall.
1239
+
1240
+ The A001064 episodically stops transmitting mid-session with no teardown
1241
+ signal; every reopen afterwards succeeds immediately, so the camera is
1242
+ awake and answering handshakes seconds after it stopped (measured
1243
+ 2026-08-24/25). LIVING is the message that starts media on a fresh
1244
+ session, so re-sending it on the live session is the cheapest possible
1245
+ revive attempt. Bounded (a few sends, spaced out) so a camera that is
1246
+ truly asleep or out of range is not spammed - the serve's input timeout
1247
+ and the keepalive reopen remain the unchanged fallback. Pure function so
1248
+ the policy is unit-testable without a live bridge.
1249
+ """
1250
+ if silence_s <= stall_after_s:
1251
+ return False
1252
+ if nudges_sent >= max_nudges:
1253
+ return False
1254
+ return since_last_nudge_s >= interval_s
1255
+
1256
+
1221
1257
  def _bridge_should_break(rc, teardown_requested: bool) -> bool:
1222
1258
  """Whether the bridge observe loop should end on this poll() result.
1223
1259
 
@@ -1623,7 +1659,7 @@ class _SdesOpenMixin:
1623
1659
  SDP answer, writes it to a temp file, and launches ffmpeg to receive and
1624
1660
  record the SRTP stream.
1625
1661
  """
1626
- from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _spawn_bg # lazy: break client<->sdes_open cycle
1662
+ from .client import CameraMixin, _build_sdes_serve_cmd, _ffmpeg_path, _resolve_serve_input_timeout_s, _spawn_bg # lazy: break client<->sdes_open cycle
1627
1663
  import base64
1628
1664
  import subprocess
1629
1665
 
@@ -4099,6 +4135,9 @@ class _SdesOpenMixin:
4099
4135
  _trigger_peer = None # camera's real addr when relayed
4100
4136
  _sdes_probe_received = False # True after first 0xC8 probe from camera
4101
4137
  _last_hb_ts = 0.0 # time of last AVIO HEARTBEAT send
4138
+ _stall_nudges_sent = 0 # LIVING re-sends for the CURRENT stall
4139
+ _stall_last_nudge = 0.0 # wall time of the last stall nudge
4140
+ _stall_active = False # media had started, then went silent
4102
4141
  # One-shot guard for the "ffmpeg exited" log below: while the held
4103
4142
  # proc keeps reporting the same stale exit code across a
4104
4143
  # teardown-window skip (see _bridge_should_break), only the first
@@ -4537,12 +4576,17 @@ class _SdesOpenMixin:
4537
4576
  except Exception as _dw_e:
4538
4577
  _status(f"SDES DC: DCEP_WAIT LIVING err: {_dw_e}")
4539
4578
 
4540
- # Periodic retrigger: resend AVIO LIVING every 2s until probe
4541
- # received (camera acknowledged our trigger).
4542
- if (_avio_living_sent
4543
- and not _sdes_probe_received
4544
- and _trigger_bs is not None
4545
- and _time_br.time() - _last_trigger_ts >= 2.0):
4579
+ # One LIVING wire-build, shared by the pre-probe retrigger
4580
+ # below and the mid-session stall nudge: resend the AVIO
4581
+ # LIVING(5376) that starts media, encrypted then plaintext
4582
+ # exactly like the original trigger.
4583
+ # Default-arg binding (not closure capture): re-defined each
4584
+ # tick, so the CURRENT trigger socket/addrs are bound at
4585
+ # definition time and B023 cannot bite.
4586
+ def _resend_avio_living(
4587
+ _tb=_trigger_bs,
4588
+ _tsrc=_trigger_bsrc,
4589
+ _tpeer=_trigger_peer) -> None:
4546
4590
  import struct as _st_re2
4547
4591
  import random as _r_re2
4548
4592
  _re_ts = int(_time_br.time() * 1000)
@@ -4568,15 +4612,63 @@ class _SdesOpenMixin:
4568
4612
  _rsz = len(_rp)
4569
4613
  try:
4570
4614
  _br_send_to_cam(
4571
- _trigger_bs,
4615
+ _tb,
4572
4616
  bytes([0xC8, 0x00, _rsz >> 8, _rsz & 0xFF]) + _rp,
4573
- _trigger_bsrc,
4574
- _trigger_peer,
4617
+ _tsrc,
4618
+ _tpeer,
4575
4619
  )
4576
4620
  except Exception:
4577
4621
  _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_bridge_fn', exc_info=True)
4622
+
4623
+ # Periodic retrigger: resend AVIO LIVING every 2s until probe
4624
+ # received (camera acknowledged our trigger).
4625
+ if (_avio_living_sent
4626
+ and not _sdes_probe_received
4627
+ and _trigger_bs is not None
4628
+ and _time_br.time() - _last_trigger_ts >= 2.0):
4629
+ _resend_avio_living()
4578
4630
  _last_trigger_ts = _time_br.time()
4579
4631
 
4632
+ # Mid-session stall nudge: the camera episodically stops
4633
+ # transmitting with no teardown signal (A001064, measured
4634
+ # 2026-08-24/25), the serve's input then times out and the
4635
+ # session dies - yet the camera answers a FRESH handshake
4636
+ # immediately afterwards. So before letting it die, re-send
4637
+ # the LIVING that starts media. The AVIO HEARTBEAT alone
4638
+ # does not do this: heartbeats kept flowing right through
4639
+ # measured stalls while the media stayed silent. Bounded by
4640
+ # _stall_nudge_due; the input timeout and the keepalive
4641
+ # reopen remain the unchanged fallback.
4642
+ if (_STALL_NUDGE_ENABLED
4643
+ and _sdes_probe_received
4644
+ and _trigger_bs is not None
4645
+ and _media_progress[0] > 0.0):
4646
+ _stall_silence = (
4647
+ _time_br.monotonic() - _media_progress[0])
4648
+ if _stall_silence <= _STALL_NUDGE_AFTER_S:
4649
+ if _stall_active:
4650
+ _stall_active = False
4651
+ _stall_nudges_sent = 0
4652
+ # Ask for an immediate IDR so recovery is not
4653
+ # hostage to the 30s PLI cadence.
4654
+ _bridge_fn._last_pli_ts = 0.0
4655
+ _status(
4656
+ "SDES: media resumed after stall"
4657
+ " - PLI re-armed")
4658
+ elif _stall_nudge_due(
4659
+ silence_s=_stall_silence,
4660
+ nudges_sent=_stall_nudges_sent,
4661
+ since_last_nudge_s=(
4662
+ _time_br.time() - _stall_last_nudge),
4663
+ stall_after_s=_STALL_NUDGE_AFTER_S):
4664
+ _stall_active = True
4665
+ _stall_nudges_sent += 1
4666
+ _stall_last_nudge = _time_br.time()
4667
+ _status(
4668
+ f"SDES: no media for {_stall_silence:.1f}s"
4669
+ f" - LIVING nudge {_stall_nudges_sent}/3")
4670
+ _resend_avio_living()
4671
+
4580
4672
  for _bs in _rl:
4581
4673
  try:
4582
4674
  _bpkt, _bsrc = _bs.recvfrom(4096)
@@ -6278,6 +6370,8 @@ class _SdesOpenMixin:
6278
6370
  audio_gain_db=self._resolve_sdes_audio_gain_db(),
6279
6371
  push_video_only=_push_video_only,
6280
6372
  video_decoder=_video_decoder,
6373
+ input_timeout_s=_resolve_serve_input_timeout_s(
6374
+ bool(getattr(self, "is_battery_camera", False))),
6281
6375
  )
6282
6376
  # Audio matters even more than video here. A multi-PT m-line makes ffmpeg
6283
6377
  # bind the depacketizer to the FIRST payload type and silently discard the
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0b31"
7
+ version = "1.0.0b32"
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.0b31
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
@@ -218,6 +218,7 @@ tests/test_sdp_payload_type_narrowing.py
218
218
  tests/test_security_hardening.py
219
219
  tests/test_self_peer_is_an_address_not_an_ip.py
220
220
  tests/test_serializable_login_info.py
221
+ tests/test_serve_input_timeout.py
221
222
  tests/test_serve_loss_tolerance.py
222
223
  tests/test_serve_pt_respects_the_pin.py
223
224
  tests/test_serve_relay.py
@@ -232,6 +233,7 @@ tests/test_sprop_unstable_public.py
232
233
  tests/test_sprop_write_is_reported_truthfully.py
233
234
  tests/test_srtp_rx_rekey.py
234
235
  tests/test_srtp_tx_key_note.py
236
+ tests/test_stall_nudge_policy.py
235
237
  tests/test_stall_report_says_what_the_answer_carried.py
236
238
  tests/test_stall_report_says_whether_the_trigger_was_answered.py
237
239
  tests/test_stall_report_separates_silence_from_undecryptable.py
@@ -0,0 +1,65 @@
1
+ """Unit tests for the serve's SDP-input silence tolerance.
2
+
3
+ The sdp demuxer's ``listen_timeout`` doubles as its packet-read timeout
4
+ (ffmpeg READ_PACKET_TIMEOUT_S, default 10 s): 10 s with no packets on any RTP
5
+ socket errors the input ("Error during demuxing: Operation timed out") and the
6
+ serve dies. Measured on an A001064, the camera episodically stops
7
+ transmitting for >=10 s mid-session and the link recovers on a 10-20 s scale,
8
+ so a mains camera rides the gap out with a longer window while a battery
9
+ camera keeps the fast default (its stops are real sleeps that only a reopen
10
+ ends).
11
+ """
12
+ import os
13
+ import sys
14
+
15
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
16
+
17
+ from aidot_cameras.camera.client import ( # noqa: E402
18
+ _SERVE_INPUT_TIMEOUT_BATTERY_S,
19
+ _SERVE_INPUT_TIMEOUT_MAINS_S,
20
+ _build_sdes_serve_cmd as build,
21
+ _resolve_serve_input_timeout_s as resolve,
22
+ )
23
+
24
+
25
+ def test_mains_gets_the_long_window():
26
+ assert resolve(False) == _SERVE_INPUT_TIMEOUT_MAINS_S == 30
27
+
28
+
29
+ def test_battery_keeps_ffmpeg_default():
30
+ # 10 is ffmpeg's own READ_PACKET_TIMEOUT_S: passing it changes nothing for
31
+ # battery cameras, whose mid-session stops are real sleeps.
32
+ assert resolve(True) == _SERVE_INPUT_TIMEOUT_BATTERY_S == 10
33
+
34
+
35
+ def test_env_override_wins_for_both(monkeypatch):
36
+ monkeypatch.setenv("AIDOT_SERVE_INPUT_TIMEOUT_S", "45")
37
+ assert resolve(False) == 45
38
+ assert resolve(True) == 45
39
+
40
+
41
+ def test_env_override_garbage_falls_back(monkeypatch):
42
+ monkeypatch.setenv("AIDOT_SERVE_INPUT_TIMEOUT_S", "soon")
43
+ assert resolve(False) == _SERVE_INPUT_TIMEOUT_MAINS_S
44
+ assert resolve(True) == _SERVE_INPUT_TIMEOUT_BATTERY_S
45
+
46
+
47
+ def test_env_override_is_floored_at_one_second(monkeypatch):
48
+ monkeypatch.setenv("AIDOT_SERVE_INPUT_TIMEOUT_S", "0")
49
+ assert resolve(False) == 1
50
+
51
+
52
+ def test_builder_places_listen_timeout_before_input():
53
+ cmd = build(sdp_path="/x.sdp", rtsp_push_url="rtsp://127.0.0.1:8554/cam",
54
+ input_timeout_s=30)
55
+ i = cmd.index("-listen_timeout")
56
+ assert cmd[i + 1] == "30"
57
+ # Input option: must precede -i, or ffmpeg reads it as an output option.
58
+ assert i < cmd.index("-i")
59
+
60
+
61
+ def test_builder_default_is_unchanged_behavior():
62
+ # None -> no flag at all, so callers that do not opt in keep ffmpeg's own
63
+ # default (and the argv stays byte-identical to pre-change builds).
64
+ cmd = build(sdp_path="/x.sdp", rtsp_push_url="rtsp://127.0.0.1:8554/cam")
65
+ assert "-listen_timeout" not in cmd
@@ -0,0 +1,51 @@
1
+ """Unit tests for _stall_nudge_due - the mid-session stall-nudge policy.
2
+
3
+ The A001064 episodically stops transmitting mid-session with no teardown
4
+ signal, yet answers a fresh handshake immediately afterwards. The bridge
5
+ therefore re-sends the AVIO LIVING that starts media before letting the
6
+ serve's input timeout kill the session. This policy decides when: bounded
7
+ sends, spaced out, and never before a real stall threshold - the input
8
+ timeout and the keepalive reopen remain the fallback.
9
+ """
10
+ import os
11
+ import sys
12
+
13
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
14
+
15
+ from aidot_cameras.camera.sdes_open import _stall_nudge_due as due # noqa: E402
16
+
17
+
18
+ def test_no_nudge_while_media_flows():
19
+ assert not due(silence_s=0.4, nudges_sent=0, since_last_nudge_s=99.0)
20
+
21
+
22
+ def test_no_nudge_at_or_below_threshold():
23
+ assert not due(silence_s=2.5, nudges_sent=0, since_last_nudge_s=99.0)
24
+
25
+
26
+ def test_first_nudge_past_threshold():
27
+ assert due(silence_s=2.6, nudges_sent=0, since_last_nudge_s=99.0)
28
+
29
+
30
+ def test_nudges_are_spaced():
31
+ # 1 s after the previous nudge: too soon; 2 s: due again.
32
+ assert not due(silence_s=5.0, nudges_sent=1, since_last_nudge_s=1.0)
33
+ assert due(silence_s=5.0, nudges_sent=1, since_last_nudge_s=2.0)
34
+
35
+
36
+ def test_nudges_are_bounded():
37
+ # After max_nudges the camera is presumed truly asleep/out of range and
38
+ # the fallback path (input timeout -> reopen) takes over unmolested.
39
+ assert not due(silence_s=60.0, nudges_sent=3, since_last_nudge_s=99.0)
40
+
41
+
42
+ def test_threshold_is_parameterised():
43
+ assert due(silence_s=1.1, nudges_sent=0, since_last_nudge_s=99.0,
44
+ stall_after_s=1.0)
45
+ assert not due(silence_s=1.1, nudges_sent=0, since_last_nudge_s=99.0,
46
+ stall_after_s=2.5)
47
+
48
+
49
+ def test_max_is_parameterised():
50
+ assert due(silence_s=60.0, nudges_sent=3, since_last_nudge_s=99.0,
51
+ max_nudges=5)