python-aidot-cameras 1.0.0rc5__tar.gz → 1.0.0rc7__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 (285) hide show
  1. {python_aidot_cameras-1.0.0rc5/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc7}/PKG-INFO +169 -17
  2. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/README.md +168 -16
  3. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/__main__.py +48 -15
  4. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/client.py +2 -2
  5. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sdes_open.py +229 -6
  6. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/webrtc_open.py +121 -21
  7. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/pyproject.toml +1 -1
  8. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7/python_aidot_cameras.egg-info}/PKG-INFO +169 -17
  9. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/SOURCES.txt +4 -0
  10. python_aidot_cameras-1.0.0rc7/tests/test_battery_stale_offer_is_abandoned_early.py +150 -0
  11. python_aidot_cameras-1.0.0rc7/tests/test_battery_wake_gate_app_parity.py +89 -0
  12. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_go2rtc_cli.py +57 -7
  13. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_liveplay_echo_timeout.py +11 -5
  14. python_aidot_cameras-1.0.0rc7/tests/test_off_subnet_candidate.py +68 -0
  15. python_aidot_cameras-1.0.0rc7/tests/test_signalling_is_attributable_to_a_camera.py +150 -0
  16. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/LICENSE +0 -0
  17. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/MANIFEST.in +0 -0
  18. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_upstream.py +0 -0
  20. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  22. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  23. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  24. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  25. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  26. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  27. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  28. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  29. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  30. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  31. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  32. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  33. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  34. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  35. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  36. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  37. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  38. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  39. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  40. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  41. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  42. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  43. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  44. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  45. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  46. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  47. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  48. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  49. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  50. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  51. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  52. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  53. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  54. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/ble_gateway.py +0 -0
  55. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/__init__.py +0 -0
  56. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/constants.py +0 -0
  57. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/controls.py +0 -0
  58. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/go2rtc.py +0 -0
  59. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/hwaccel.py +0 -0
  60. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/lan_control.py +0 -0
  61. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/models.py +0 -0
  62. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/playback.py +0 -0
  63. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/protocol.py +0 -0
  64. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_events.py +0 -0
  65. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_listing.py +0 -0
  66. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sd_playback.py +0 -0
  67. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/sdes.py +0 -0
  68. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/tutk.py +0 -0
  69. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/camera/webrtc.py +0 -0
  70. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/client.py +0 -0
  71. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/cloud_auth.py +0 -0
  72. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/const.py +0 -0
  73. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/credentials.py +0 -0
  74. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/crypto.py +0 -0
  75. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/device_client.py +0 -0
  76. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/discover.py +0 -0
  77. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/exceptions.py +0 -0
  78. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/g711.py +0 -0
  79. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/lan_retry.py +0 -0
  80. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/aidot_cameras/py.typed +0 -0
  81. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  82. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  83. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/requires.txt +0 -0
  84. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  85. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/setup.cfg +0 -0
  86. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_aioice_compat.py +0 -0
  87. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_alarm_event.py +0 -0
  88. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_pt_kinds.py +0 -0
  89. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_sdp_video_pt.py +0 -0
  90. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_answer_section_selection.py +0 -0
  91. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_audio_mux_offline.py +0 -0
  92. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  93. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_request.py +0 -0
  94. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_response_parse.py +0 -0
  95. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_avio_response_router.py +0 -0
  96. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_backoff.py +0 -0
  97. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_detection.py +0 -0
  98. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_evidence_corroboration.py +0 -0
  99. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_battery_skip_turn.py +0 -0
  100. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_bitrate_campaign_arms.py +0 -0
  101. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ble_gateway.py +0 -0
  102. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_bridge_state_sentinels.py +0 -0
  103. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_busy_backoff.py +0 -0
  104. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_camera_properties.py +0 -0
  105. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_canary_log_names_the_camera.py +0 -0
  106. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_cloud_count.py +0 -0
  107. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_cloud_plan.py +0 -0
  108. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_constants_imports_resolve.py +0 -0
  109. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_control_verdicts.py +0 -0
  110. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_deferred_hardening.py +0 -0
  111. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_cmd_seq_match.py +0 -0
  112. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_login_guard.py +0 -0
  113. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_device_user_info_cache.py +0 -0
  114. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_direct_ts_keyframe_signal.py +0 -0
  115. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_direct_ts_serve.py +0 -0
  116. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dormant_camera_cleanup.py +0 -0
  117. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_drain_is_cancellable.py +0 -0
  118. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_mux_terminal_write.py +0 -0
  119. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_not_ready_burst.py +0 -0
  120. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_pinning.py +0 -0
  121. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_resolution_ack.py +0 -0
  122. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_decode_skip.py +0 -0
  123. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_ice_wait.py +0 -0
  124. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_serve_open_timeout.py +0 -0
  125. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_skip_signaling_wait.py +0 -0
  126. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_slow_probe.py +0 -0
  127. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_stdout_producer.py +0 -0
  128. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_dtls_video_presence.py +0 -0
  129. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_egress_guard.py +0 -0
  130. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_env_seams.py +0 -0
  131. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  132. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  133. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_futile_keepalive.py +0 -0
  134. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  135. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_go2rtc.py +0 -0
  136. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_h264_decode_rate_limit.py +0 -0
  137. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_highport_nomination.py +0 -0
  138. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_hwaccel_probe.py +0 -0
  139. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ice_config_cache.py +0 -0
  140. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ice_peer_reflexive.py +0 -0
  141. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_idle_release_both_loops.py +0 -0
  142. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_keepalive_renew.py +0 -0
  143. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_key_restart_sdp.py +0 -0
  144. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_keyframe_prompter.py +0 -0
  145. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_lan_control.py +0 -0
  146. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  147. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_late_repeats_are_dropped.py +0 -0
  148. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_light_active_color_mode.py +0 -0
  149. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_session_carries_talk.py +0 -0
  150. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_stream_param.py +0 -0
  151. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_validate_policy.py +0 -0
  152. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_live_video_canary.py +0 -0
  153. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_local_only_controls.py +0 -0
  154. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_logging_caps.py +0 -0
  155. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_login_connect_deadline.py +0 -0
  156. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_login_reconnect_backoff.py +0 -0
  157. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_counted_only_when_plaintext.py +0 -0
  158. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_path_is_reported.py +0 -0
  159. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_socket_rcvbuf.py +0 -0
  160. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_media_wait_floor.py +0 -0
  161. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_motion_poll.py +0 -0
  162. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_client_id_is_exact.py +0 -0
  163. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_credential_refetch.py +0 -0
  164. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_credential_self_heal.py +0 -0
  165. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_password_not_persisted.py +0 -0
  166. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_session_reconnect.py +0 -0
  167. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  168. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_nack.py +0 -0
  169. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  170. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_narrow_pc_ice.py +0 -0
  171. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_key_material_in_logs.py +0 -0
  172. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_self_referential_go2rtc.py +0 -0
  173. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_no_undefined_names.py +0 -0
  174. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offer_bandwidth_line.py +0 -0
  175. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  176. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_offline_keepalive_pause.py +0 -0
  177. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_on_frame_is_an_av_frame.py +0 -0
  178. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_open_fail_logger.py +0 -0
  179. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_open_gate_delay.py +0 -0
  180. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_peer_id_client_type.py +0 -0
  181. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_peerid_reuse.py +0 -0
  182. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_persistent_mqtt.py +0 -0
  183. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_persistent_mqtt_robustness.py +0 -0
  184. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_playback_tls.py +0 -0
  185. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_post_merge_hardening.py +0 -0
  186. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  187. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_public_signature_matches_impl.py +0 -0
  188. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_publish_target_readiness.py +0 -0
  189. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_quality_campaign_in_session.py +0 -0
  190. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_reboot_action.py +0 -0
  191. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_recent_recordings.py +0 -0
  192. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_remb.py +0 -0
  193. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  194. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_resent_frames_are_dropped.py +0 -0
  195. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_resolution_persists.py +0 -0
  196. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_restart_cycle.py +0 -0
  197. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_retry_policy.py +0 -0
  198. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_scripts_importable.py +0 -0
  199. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_init_ack_tsn.py +0 -0
  200. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_reassembly.py +0 -0
  201. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sctp_sack.py +0 -0
  202. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_card_presence.py +0 -0
  203. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_event_decode.py +0 -0
  204. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_event_probe_payloads.py +0 -0
  205. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_listing.py +0 -0
  206. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sd_playback.py +0 -0
  207. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_adaptive.py +0 -0
  208. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_avio_dispatch.py +0 -0
  209. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_bridge_break_gate.py +0 -0
  210. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_connection_mode.py +0 -0
  211. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_echo_wait_timeout.py +0 -0
  212. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_fast_liveplay.py +0 -0
  213. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  214. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_first_media_stall_report.py +0 -0
  215. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_ice_teardown.py +0 -0
  216. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_idle_release.py +0 -0
  217. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_nack_bridge.py +0 -0
  218. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_offer_video_order.py +0 -0
  219. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_offer_video_pin.py +0 -0
  220. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_open_cleanup.py +0 -0
  221. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_sctp_abort.py +0 -0
  222. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_audio.py +0 -0
  223. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_cmd.py +0 -0
  224. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_serve_stderr_drain.py +0 -0
  225. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_session_cap.py +0 -0
  226. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_sprop.py +0 -0
  227. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_talk.py +0 -0
  228. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_teardown_exit_log.py +0 -0
  229. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_terminal_ack.py +0 -0
  230. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_terminal_ack_latency.py +0 -0
  231. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_tmmbr_bridge.py +0 -0
  232. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdes_watchdog.py +0 -0
  233. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdp_narrowing_properties.py +0 -0
  234. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sdp_payload_type_narrowing.py +0 -0
  235. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_security_hardening.py +0 -0
  236. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  237. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serializable_login_info.py +0 -0
  238. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_input_timeout.py +0 -0
  239. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_loss_tolerance.py +0 -0
  240. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_pt_respects_the_pin.py +0 -0
  241. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_relay.py +0 -0
  242. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  243. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_session_stats.py +0 -0
  244. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_shared_house_seam.py +0 -0
  245. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_snapshot_cancellation.py +0 -0
  246. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_speak.py +0 -0
  247. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_capture_gate.py +0 -0
  248. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_staleness.py +0 -0
  249. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_unstable_public.py +0 -0
  250. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  251. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_srtp_rx_rekey.py +0 -0
  252. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_srtp_tx_key_note.py +0 -0
  253. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stable_terminal_id.py +0 -0
  254. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_nudge_policy.py +0 -0
  255. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  256. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  257. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  258. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  259. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  260. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_cap.py +0 -0
  261. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_drain_ownership.py +0 -0
  262. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_idle.py +0 -0
  263. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_slot_cap.py +0 -0
  264. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_stream_teardown.py +0 -0
  265. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_talk.py +0 -0
  266. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  267. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tap_to_mux_end_to_end.py +0 -0
  268. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_terminal_ack.py +0 -0
  269. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tmmbr.py +0 -0
  270. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_token_file_robustness.py +0 -0
  271. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_token_refresh.py +0 -0
  272. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_turn_create_permission.py +0 -0
  273. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  274. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_upstream_compat.py +0 -0
  275. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_validator_shares_one_token.py +0 -0
  276. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_profile_log.py +0 -0
  277. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_pts_dts.py +0 -0
  278. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_video_ts_unwrap.py +0 -0
  279. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_check_throttle.py +0 -0
  280. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_presence.py +0 -0
  281. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_viewer_present_push_mode.py +0 -0
  282. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wait_or_event.py +0 -0
  283. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wake_gate_camera_evidence.py +0 -0
  284. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/tests/test_wake_readiness_retry.py +0 -0
  285. {python_aidot_cameras-1.0.0rc5 → python_aidot_cameras-1.0.0rc7}/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.0rc5
3
+ Version: 1.0.0rc7
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
@@ -200,9 +200,40 @@ pip install "python-aidot-cameras[webrtc] @ git+https://github.com/cbrightly/pyt
200
200
 
201
201
  ## Standalone CLI: `aidot-go2rtc`
202
202
 
203
- Bridge a camera into [go2rtc](https://github.com/AlexxIT/go2rtc) (or any
204
- RTSP/HTTP consumer) **without Home Assistant**. Installing the package provides
205
- the `aidot-go2rtc` console script; for an isolated tool install use pipx or uv:
203
+ ### What it does
204
+
205
+ These cameras do not speak RTSP. They stream over WebRTC, negotiated through
206
+ AiDot's cloud with a per-session key exchange, which is why you cannot simply
207
+ point VLC or Frigate at one and get a picture.
208
+
209
+ `aidot-go2rtc` does that negotiation for you and hands the result out as
210
+ something ordinary: an MPEG-TS stream on stdout, an HTTP endpoint, or an RTSP
211
+ push into [go2rtc](https://github.com/AlexxIT/go2rtc). One camera per process.
212
+ From there anything that reads RTSP or TS can consume it.
213
+
214
+ ### Why you would use it
215
+
216
+ - **You want these cameras in something that is not Home Assistant** -- Frigate,
217
+ a standalone go2rtc, an NVR, a browser, `ffmpeg` writing to disk.
218
+ - **You want the streaming stack outside Home Assistant's Python environment,**
219
+ so its dependencies cannot collide with Home Assistant's pins.
220
+ - **You are debugging.** `--list` shows every camera and which transport it uses,
221
+ and a single camera can be streamed in isolation with the env knobs below,
222
+ which is far easier to reason about than the same thing inside Home Assistant.
223
+
224
+ ### When you do not need it
225
+
226
+ **If you use Home Assistant, install the integration instead** -- it does all of
227
+ this for you, and running both at once means two clients competing for the same
228
+ camera. A camera serves a limited number of concurrent viewers and holds a slot
229
+ for about 120 s after one leaves, so the loser gets a clean handshake and then no
230
+ media at all. The limit belongs to the device, so a second AiDot account with the
231
+ house shared to it does not avoid it.
232
+
233
+ ### Installing
234
+
235
+ Installing the package provides the `aidot-go2rtc` console script; for an
236
+ isolated tool install use pipx or uv:
206
237
 
207
238
  ```bash
208
239
  pipx install "python-aidot-cameras[webrtc]"
@@ -212,16 +243,48 @@ uv tool install "python-aidot-cameras[webrtc]"
212
243
  export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
213
244
  aidot-go2rtc --list # discover cameras + their transport
214
245
 
215
- # Stream one camera. Pick the form that matches how you are running it:
216
- aidot-go2rtc <device_id> - # DTLS: MPEG-TS to stdout
217
- aidot-go2rtc <device_id> '{output}' # SDES: go2rtc exec: only
218
- aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # either: serve, then pull
246
+ # Stream one camera. The second argument is WHERE the media should go:
247
+ aidot-go2rtc <device_id> - # to stdout
248
+ aidot-go2rtc <device_id> '{output}' # go2rtc fills this in
249
+ aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull
219
250
  ```
220
251
 
221
- `'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
222
- through as an RTSP push URL. Both transports can push, so `{output}` works on
223
- any camera - but on a DTLS one prefer `-`, which carries the mux's 48 kHz AAC
224
- untouched where the push has to transcode audio down to 8 kHz G.711.
252
+ **Which form to use.** `--list` tells you each camera's transport, and that
253
+ decides it:
254
+
255
+ | your camera | use | why |
256
+ | --- | --- | --- |
257
+ | **DTLS** (mains, e.g. A000088) | `-` | keeps the mux's 48 kHz AAC. The push path has to transcode audio down to 8 kHz G.711 |
258
+ | **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
259
+ | either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
260
+
261
+ `'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
262
+ URL when it launches the process from an `exec:` source -- quote it so your
263
+ shell does not eat the braces. It is not something to type at a shell: run by
264
+ hand the literal text is taken as a publish target and the push fails. To try
265
+ the push path by hand, put a real `rtsp://` URL there instead. Both transports
266
+ can push, so `{output}` works on any camera; the table is about which gives the
267
+ better result, not what is possible.
268
+
269
+ ### Wiring it into go2rtc
270
+
271
+ `--list` prints a paste-ready `streams:` block for every camera on the account,
272
+ so you rarely have to write this by hand. It looks like this:
273
+
274
+ ```yaml
275
+ streams:
276
+ front_door: exec:aidot-go2rtc 1234abcd... {output}
277
+ driveway: exec:aidot-go2rtc 5678efgh... -
278
+ ```
279
+
280
+ go2rtc launches one process per stream, on demand, and substitutes `{output}`
281
+ with that stream's push URL.
282
+
283
+ The process needs the same `AIDOT_*` environment variables you used above. It is
284
+ launched as a child of go2rtc, so the practical answer is to put them where
285
+ go2rtc itself gets its environment: export them in the shell before starting it,
286
+ or set them in its service unit or container definition. If a stream fails
287
+ immediately with an authentication error, that is the thing to check first.
225
288
 
226
289
  **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
227
290
  stream entirely through an ffmpeg subprocess, and pip cannot install a system
@@ -299,6 +362,91 @@ await session.async_stop_talk()
299
362
  See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
300
363
  snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
301
364
 
365
+ ## Camera controls and state
366
+
367
+ Controls are on the camera device client. Writes are confirmed by reading the
368
+ value back on each model that offers them, because this firmware acknowledges
369
+ writes it then ignores.
370
+
371
+ ```python
372
+ await device_client.async_set_motion_detection(True)
373
+ await device_client.async_set_night_vision("auto") # auto | on | off
374
+ await device_client.async_set_speaker_volume(50)
375
+ await device_client.async_reboot() # see below
376
+ ```
377
+
378
+ ### Reboot
379
+
380
+ ```python
381
+ sent = await device_client.async_reboot()
382
+ ```
383
+
384
+ `async_reboot` sends the same `devActionReq({action: "RebootFunc", in: []})` the
385
+ vendor app's camera settings page sends. Two things to know:
386
+
387
+ * **It reports that the request was SENT, not that the camera rebooted.** A
388
+ reboot succeeds by taking the camera off the network, so waiting for an
389
+ acknowledgement would report a working reboot as a failure. Do not read
390
+ `True` as "it came back".
391
+ * **It refuses when the cloud explicitly reports the camera offline**, mirroring
392
+ the app, which only offers the button on a reachable device.
393
+
394
+ The camera is away for a few seconds afterwards, during which a viewer sees a
395
+ session that connects and delivers nothing.
396
+
397
+ ### Sound detection
398
+
399
+ The cameras can listen for glass breaking, a smoke alarm, a baby crying and a
400
+ dog barking.
401
+
402
+ ```python
403
+ flags = await device_client.async_get_sound_detection()
404
+ # {'sound_enable': False, 'all_sound': True, 'glass_Break': False,
405
+ # 'smoke_T3': False, 'smoke_T4': False, 'baby_cry': False}
406
+
407
+ await device_client.async_set_sound_detection("glass_Break", True)
408
+ ```
409
+
410
+ The keys come from the camera, not from a fixed table, so a model reporting a
411
+ detector not listed above still works. A write is read-modify-write: the current
412
+ list is fetched and sent back with one flag changed, rather than against a
413
+ payload schema invented here.
414
+
415
+ **`None` means unknown, never "all off".** A camera that does not answer -- a
416
+ battery camera asleep, or a model without the feature -- returns `None`, and
417
+ treating that as a set of disabled detectors would report a state the camera
418
+ never claimed. A write refuses outright when there is no current list to echo.
419
+
420
+ ### WiFi and SD card
421
+
422
+ ```python
423
+ await device_client.async_get_wifi_info() # {'ssid': ..., 'rssi': 63}
424
+ await device_client.async_get_sd_card_info() # {'present': True, 'total': 29838,
425
+ # 'used': 28848, 'raw': [...]}
426
+ ```
427
+
428
+ Only confirmed fields are named. `SDcardBaseInfo` answers positionally and its
429
+ fourth value was nearly called "free" -- but on a card reading 29838 total and
430
+ 28848 used it returns 3, which is not a remainder (990) and looks like a
431
+ percentage. It stays unnamed in `raw` rather than shipping a field that reports
432
+ 3 MB free on a 30 GB card. The units of `total` and `used` are unconfirmed too,
433
+ so the numbers pass through exactly as the camera reports them.
434
+
435
+ ### Reading anything else the camera answers
436
+
437
+ ```python
438
+ out = await device_client.async_query_device_action("getWorkMode")
439
+ ```
440
+
441
+ `async_query_device_action` sends a read-only `devActionReq` and returns the
442
+ camera's `out` payload. The cameras answer more actions than this library wraps
443
+ -- `getWorkMode`, `getAutoAlarm`, `getRoiHuman` (human/vehicle/package
444
+ detection), `getRoiPrivacy` and others. `None` means no reply, which is not the
445
+ same as "unsupported".
446
+
447
+ **Do not use it to send actions that change something** unless you know the
448
+ payload shape. `SDcardFormatFunc` is one of the actions the cameras accept.
449
+
302
450
  ## Getting an RTSP URL
303
451
 
304
452
  Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
@@ -317,7 +465,7 @@ keep the output around:
317
465
  $ pip install "python-aidot-cameras[webrtc]"
318
466
  $ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
319
467
  $ aidot-go2rtc --list
320
- 1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
468
+ 1a2b3c4d... LK.IPC.A000088 DTLS (stdout)
321
469
  5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
322
470
  ```
323
471
 
@@ -356,9 +504,10 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
356
504
  - **Which camera is which.** `--list` reports the transport per device. DTLS is
357
505
  the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
358
506
  form is not silent - the CLI tells you which one to use and exits.
359
- - **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
360
- DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
361
- A-law, because 8 kHz AAC plays silent in a lot of browsers).
507
+ - **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
508
+ 8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
509
+ on the MSE path have no mapping for G.711 at all. Audio is on by default; see
510
+ `AIDOT_SDES_SERVE_AUDIO` to turn it off.
362
511
  - **A stream that starts with no video** and picks it up a few seconds later is
363
512
  normal: the mux waits for a keyframe so the first GOP is decodable. A stream
364
513
  that stays audio-only is a camera that never sent one - retry the view.
@@ -418,7 +567,10 @@ audio, idle release, the sprop cache path) are documented in
418
567
  | `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`) |
419
568
  | `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) |
420
569
  | `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) |
421
- | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
570
+ | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). It is pure latency: across 22 h of one deployment the wait ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. The fast-liveplay path kept 1.5 s until it was measured on its own - 22 runs, 22 timeouts at 1500-1501 ms, no echo ever - and now shares the same value; that was 1.25 s of a 5.4 s cold connect. Still honoured, so a broker that does echo short-circuits it. | 0.25 s |
571
+ | `AIDOT_BATTERY_WAKE_GATE_S` | Hold a **battery** camera's SDP offer until the camera itself answers, in seconds. **Measured and shipped off (`0`).** This is the official client's own shape - its live view fires the keep-alive, sends the wake, and shows a sleeping placeholder rather than opening a session until the device reports itself awake - and on this camera family it is self-defeating, because the live-play signalling is what wakes the camera: withholding the offer withholds the wake. At a 20 s budget, on a camera settled for ten minutes, the gate ran its whole budget with the camera silent, the offer went out at +20.9 s, the camera's own `wakeupStatus` arrived at +23.6 s (after the offer), and first media at +27.0 s - against 5.4-10.0 s on the same camera with no gate. Kept as a lever for anyone re-testing on other firmware. | `0` (off) |
572
+ | `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
573
+ | `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
422
574
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
423
575
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
424
576
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
@@ -160,9 +160,40 @@ pip install "python-aidot-cameras[webrtc] @ git+https://github.com/cbrightly/pyt
160
160
 
161
161
  ## Standalone CLI: `aidot-go2rtc`
162
162
 
163
- Bridge a camera into [go2rtc](https://github.com/AlexxIT/go2rtc) (or any
164
- RTSP/HTTP consumer) **without Home Assistant**. Installing the package provides
165
- the `aidot-go2rtc` console script; for an isolated tool install use pipx or uv:
163
+ ### What it does
164
+
165
+ These cameras do not speak RTSP. They stream over WebRTC, negotiated through
166
+ AiDot's cloud with a per-session key exchange, which is why you cannot simply
167
+ point VLC or Frigate at one and get a picture.
168
+
169
+ `aidot-go2rtc` does that negotiation for you and hands the result out as
170
+ something ordinary: an MPEG-TS stream on stdout, an HTTP endpoint, or an RTSP
171
+ push into [go2rtc](https://github.com/AlexxIT/go2rtc). One camera per process.
172
+ From there anything that reads RTSP or TS can consume it.
173
+
174
+ ### Why you would use it
175
+
176
+ - **You want these cameras in something that is not Home Assistant** -- Frigate,
177
+ a standalone go2rtc, an NVR, a browser, `ffmpeg` writing to disk.
178
+ - **You want the streaming stack outside Home Assistant's Python environment,**
179
+ so its dependencies cannot collide with Home Assistant's pins.
180
+ - **You are debugging.** `--list` shows every camera and which transport it uses,
181
+ and a single camera can be streamed in isolation with the env knobs below,
182
+ which is far easier to reason about than the same thing inside Home Assistant.
183
+
184
+ ### When you do not need it
185
+
186
+ **If you use Home Assistant, install the integration instead** -- it does all of
187
+ this for you, and running both at once means two clients competing for the same
188
+ camera. A camera serves a limited number of concurrent viewers and holds a slot
189
+ for about 120 s after one leaves, so the loser gets a clean handshake and then no
190
+ media at all. The limit belongs to the device, so a second AiDot account with the
191
+ house shared to it does not avoid it.
192
+
193
+ ### Installing
194
+
195
+ Installing the package provides the `aidot-go2rtc` console script; for an
196
+ isolated tool install use pipx or uv:
166
197
 
167
198
  ```bash
168
199
  pipx install "python-aidot-cameras[webrtc]"
@@ -172,16 +203,48 @@ uv tool install "python-aidot-cameras[webrtc]"
172
203
  export AIDOT_USERNAME=... AIDOT_PASSWORD=... # or AIDOT_TOKEN_FILE, see below
173
204
  aidot-go2rtc --list # discover cameras + their transport
174
205
 
175
- # Stream one camera. Pick the form that matches how you are running it:
176
- aidot-go2rtc <device_id> - # DTLS: MPEG-TS to stdout
177
- aidot-go2rtc <device_id> '{output}' # SDES: go2rtc exec: only
178
- aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # either: serve, then pull
206
+ # Stream one camera. The second argument is WHERE the media should go:
207
+ aidot-go2rtc <device_id> - # to stdout
208
+ aidot-go2rtc <device_id> '{output}' # go2rtc fills this in
209
+ aidot-go2rtc <device_id> http://127.0.0.1:8555/cam.ts # serve, then pull
179
210
  ```
180
211
 
181
- `'{output}'` is a placeholder go2rtc substitutes; run by hand it is passed
182
- through as an RTSP push URL. Both transports can push, so `{output}` works on
183
- any camera - but on a DTLS one prefer `-`, which carries the mux's 48 kHz AAC
184
- untouched where the push has to transcode audio down to 8 kHz G.711.
212
+ **Which form to use.** `--list` tells you each camera's transport, and that
213
+ decides it:
214
+
215
+ | your camera | use | why |
216
+ | --- | --- | --- |
217
+ | **DTLS** (mains, e.g. A000088) | `-` | keeps the mux's 48 kHz AAC. The push path has to transcode audio down to 8 kHz G.711 |
218
+ | **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
219
+ | either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
220
+
221
+ `'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
222
+ URL when it launches the process from an `exec:` source -- quote it so your
223
+ shell does not eat the braces. It is not something to type at a shell: run by
224
+ hand the literal text is taken as a publish target and the push fails. To try
225
+ the push path by hand, put a real `rtsp://` URL there instead. Both transports
226
+ can push, so `{output}` works on any camera; the table is about which gives the
227
+ better result, not what is possible.
228
+
229
+ ### Wiring it into go2rtc
230
+
231
+ `--list` prints a paste-ready `streams:` block for every camera on the account,
232
+ so you rarely have to write this by hand. It looks like this:
233
+
234
+ ```yaml
235
+ streams:
236
+ front_door: exec:aidot-go2rtc 1234abcd... {output}
237
+ driveway: exec:aidot-go2rtc 5678efgh... -
238
+ ```
239
+
240
+ go2rtc launches one process per stream, on demand, and substitutes `{output}`
241
+ with that stream's push URL.
242
+
243
+ The process needs the same `AIDOT_*` environment variables you used above. It is
244
+ launched as a child of go2rtc, so the practical answer is to put them where
245
+ go2rtc itself gets its environment: export them in the shell before starting it,
246
+ or set them in its service unit or container definition. If a stream fails
247
+ immediately with an authentication error, that is the thing to check first.
185
248
 
186
249
  **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
187
250
  stream entirely through an ffmpeg subprocess, and pip cannot install a system
@@ -259,6 +322,91 @@ await session.async_stop_talk()
259
322
  See [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md) for the full camera API (streaming,
260
323
  snapshots, recordings, motion polling, two-way audio, and LAN-direct media).
261
324
 
325
+ ## Camera controls and state
326
+
327
+ Controls are on the camera device client. Writes are confirmed by reading the
328
+ value back on each model that offers them, because this firmware acknowledges
329
+ writes it then ignores.
330
+
331
+ ```python
332
+ await device_client.async_set_motion_detection(True)
333
+ await device_client.async_set_night_vision("auto") # auto | on | off
334
+ await device_client.async_set_speaker_volume(50)
335
+ await device_client.async_reboot() # see below
336
+ ```
337
+
338
+ ### Reboot
339
+
340
+ ```python
341
+ sent = await device_client.async_reboot()
342
+ ```
343
+
344
+ `async_reboot` sends the same `devActionReq({action: "RebootFunc", in: []})` the
345
+ vendor app's camera settings page sends. Two things to know:
346
+
347
+ * **It reports that the request was SENT, not that the camera rebooted.** A
348
+ reboot succeeds by taking the camera off the network, so waiting for an
349
+ acknowledgement would report a working reboot as a failure. Do not read
350
+ `True` as "it came back".
351
+ * **It refuses when the cloud explicitly reports the camera offline**, mirroring
352
+ the app, which only offers the button on a reachable device.
353
+
354
+ The camera is away for a few seconds afterwards, during which a viewer sees a
355
+ session that connects and delivers nothing.
356
+
357
+ ### Sound detection
358
+
359
+ The cameras can listen for glass breaking, a smoke alarm, a baby crying and a
360
+ dog barking.
361
+
362
+ ```python
363
+ flags = await device_client.async_get_sound_detection()
364
+ # {'sound_enable': False, 'all_sound': True, 'glass_Break': False,
365
+ # 'smoke_T3': False, 'smoke_T4': False, 'baby_cry': False}
366
+
367
+ await device_client.async_set_sound_detection("glass_Break", True)
368
+ ```
369
+
370
+ The keys come from the camera, not from a fixed table, so a model reporting a
371
+ detector not listed above still works. A write is read-modify-write: the current
372
+ list is fetched and sent back with one flag changed, rather than against a
373
+ payload schema invented here.
374
+
375
+ **`None` means unknown, never "all off".** A camera that does not answer -- a
376
+ battery camera asleep, or a model without the feature -- returns `None`, and
377
+ treating that as a set of disabled detectors would report a state the camera
378
+ never claimed. A write refuses outright when there is no current list to echo.
379
+
380
+ ### WiFi and SD card
381
+
382
+ ```python
383
+ await device_client.async_get_wifi_info() # {'ssid': ..., 'rssi': 63}
384
+ await device_client.async_get_sd_card_info() # {'present': True, 'total': 29838,
385
+ # 'used': 28848, 'raw': [...]}
386
+ ```
387
+
388
+ Only confirmed fields are named. `SDcardBaseInfo` answers positionally and its
389
+ fourth value was nearly called "free" -- but on a card reading 29838 total and
390
+ 28848 used it returns 3, which is not a remainder (990) and looks like a
391
+ percentage. It stays unnamed in `raw` rather than shipping a field that reports
392
+ 3 MB free on a 30 GB card. The units of `total` and `used` are unconfirmed too,
393
+ so the numbers pass through exactly as the camera reports them.
394
+
395
+ ### Reading anything else the camera answers
396
+
397
+ ```python
398
+ out = await device_client.async_query_device_action("getWorkMode")
399
+ ```
400
+
401
+ `async_query_device_action` sends a read-only `devActionReq` and returns the
402
+ camera's `out` payload. The cameras answer more actions than this library wraps
403
+ -- `getWorkMode`, `getAutoAlarm`, `getRoiHuman` (human/vehicle/package
404
+ detection), `getRoiPrivacy` and others. `None` means no reply, which is not the
405
+ same as "unsupported".
406
+
407
+ **Do not use it to send actions that change something** unless you know the
408
+ payload shape. `SDcardFormatFunc` is one of the actions the cameras accept.
409
+
262
410
  ## Getting an RTSP URL
263
411
 
264
412
  Want a plain `rtsp://` URL for Frigate, an NVR, or VLC? You can have one, but
@@ -277,7 +425,7 @@ keep the output around:
277
425
  $ pip install "python-aidot-cameras[webrtc]"
278
426
  $ export AIDOT_USERNAME='you@example.com' AIDOT_PASSWORD='...' AIDOT_COUNTRY=US
279
427
  $ aidot-go2rtc --list
280
- 1a2b3c4d... LK.IPC.A000088 DTLS (http-pull)
428
+ 1a2b3c4d... LK.IPC.A000088 DTLS (stdout)
281
429
  5e6f7a8b... LK.IPC.A001064 SDES (rtsp-push)
282
430
  ```
283
431
 
@@ -316,9 +464,10 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
316
464
  - **Which camera is which.** `--list` reports the transport per device. DTLS is
317
465
  the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
318
466
  form is not silent - the CLI tells you which one to use and exits.
319
- - **Audio.** The SDES `{output}` push carries G.711 audio alongside video; the
320
- DTLS `-` producer carries AAC (48 kHz mono, resampled from the camera's 8 kHz
321
- A-law, because 8 kHz AAC plays silent in a lot of browsers).
467
+ - **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
468
+ 8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
469
+ on the MSE path have no mapping for G.711 at all. Audio is on by default; see
470
+ `AIDOT_SDES_SERVE_AUDIO` to turn it off.
322
471
  - **A stream that starts with no video** and picks it up a few seconds later is
323
472
  normal: the mux waits for a keyframe so the first GOP is decodable. A stream
324
473
  that stays audio-only is a camera that never sent one - retry the view.
@@ -378,7 +527,10 @@ audio, idle release, the sprop cache path) are documented in
378
527
  | `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`) |
379
528
  | `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) |
380
529
  | `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) |
381
- | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
530
+ | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). It is pure latency: across 22 h of one deployment the wait ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. The fast-liveplay path kept 1.5 s until it was measured on its own - 22 runs, 22 timeouts at 1500-1501 ms, no echo ever - and now shares the same value; that was 1.25 s of a 5.4 s cold connect. Still honoured, so a broker that does echo short-circuits it. | 0.25 s |
531
+ | `AIDOT_BATTERY_WAKE_GATE_S` | Hold a **battery** camera's SDP offer until the camera itself answers, in seconds. **Measured and shipped off (`0`).** This is the official client's own shape - its live view fires the keep-alive, sends the wake, and shows a sleeping placeholder rather than opening a session until the device reports itself awake - and on this camera family it is self-defeating, because the live-play signalling is what wakes the camera: withholding the offer withholds the wake. At a 20 s budget, on a camera settled for ten minutes, the gate ran its whole budget with the camera silent, the offer went out at +20.9 s, the camera's own `wakeupStatus` arrived at +23.6 s (after the offer), and first media at +27.0 s - against 5.4-10.0 s on the same camera with no gate. Kept as a lever for anyone re-testing on other firmware. | `0` (off) |
532
+ | `AIDOT_BATTERY_STALE_OFFER_GRACE_S` | Backstop for a **battery** attempt that stalls anyway, in seconds (`0` disables it). When the camera was silent as the first-media wait began, has since turned up, and has still sent no media after this long, the attempt is abandoned to the retry instead of holding the full 75 s window - a stalled camera answers and then sends nothing, while the retry's fresh offer is served in about 5 s. Measured from the camera's first sighting rather than its latest message, because a camera emitting an event every 5 s while its handshake goes nowhere would otherwise push the decision out until it had gone back to sleep. Sized clear of the slowest healthy open (10.7 s) so a merely slow attempt is never mistaken for a stalled one. | `15` |
533
+ | `AIDOT_SDES_SERVE_AUDIO` | Include the camera's audio in what an SDES camera serves, on both the RTSP-push and http-serve paths. **On by default** for parity with the official app; set to `0`/`false`/`no`/`off` for video only, which is what a consumer that cannot cope with the audio wants. The standalone CLI reads this; inside Home Assistant the per-camera **Camera audio** switch is the same setting and should be used instead. | enabled (on) |
382
534
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
383
535
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
384
536
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
@@ -39,18 +39,20 @@ Authentication (in precedence order):
39
39
  Optional stream knobs:
40
40
 
41
41
  AIDOT_FAST_CONNECT=1 LAN-direct mode (skip TURN relay; same-subnet only)
42
- AIDOT_SDES_SERVE_AUDIO=1 include audio on the SDES *http-serve* path (off by
43
- default - the AAC-under-loss deadlock). The RTSP-push
44
- path always carries G711 audio regardless.
42
+ AIDOT_SDES_SERVE_AUDIO=0 audio is ON by default on both SDES output paths
43
+ (app parity). Set 0/false/no/off for video only,
44
+ which is what a consumer that cannot cope with the
45
+ audio wants. Both paths encode AAC 48 kHz.
45
46
 
46
47
  go2rtc.yaml example (SDES camera - native RTSP push, video + audio):
47
48
 
48
49
  streams:
49
50
  frontdoor: exec:aidot-go2rtc 0a1b2c3d... {output}
50
51
 
51
- A DTLS camera (A000088) has no RTSP-push path, but a stdout producer works the
52
- same lazy way. Pass ``-`` as the output and this process writes MPEG-TS (video
53
- plus AAC audio) to stdout, which go2rtc's exec: source reads directly:
52
+ A DTLS camera (A000088) can push RTSP too, but a stdout producer is the better
53
+ default for it and works the same lazy way. Pass ``-`` as the output and this
54
+ process writes MPEG-TS (video plus AAC audio) to stdout, which go2rtc's exec:
55
+ source reads directly:
54
56
 
55
57
  streams:
56
58
  garage: exec:aidot-go2rtc 0a1b2c3d... -
@@ -89,11 +91,39 @@ _LOGGER = logging.getLogger("aidot.go2rtc")
89
91
 
90
92
 
91
93
  def _env_bool(name: str) -> bool | None:
92
- """Tri-state env flag: '1' -> True, '0' -> False, unset -> None (library default)."""
93
- raw = os.environ.get(name)
94
- if raw is None:
95
- return None
96
- return raw == "1"
94
+ """Tri-state env flag: truthy -> True, falsy -> False, anything else -> None.
95
+
96
+ ``None`` is the whole point, and it is not timidity. A non-``None`` here is
97
+ passed to ``start_keepalive``, which stores it as the per-camera option - and
98
+ every library resolver checks that option *before* it looks at the
99
+ environment. So a value returned from here does not merely beat the env var,
100
+ it outranks the library's entire resolution chain. This reader knows the
101
+ spelling of a flag but not what any knob defaults to, so it must decline
102
+ rather than guess: unset, blank, or a word it cannot read all mean "no
103
+ opinion", leaving the decision where the default actually lives.
104
+
105
+ The library states each knob as "a recognised token flips it, anything else
106
+ keeps the default", spelled as a truthy allowlist where the default is off
107
+ (``AIDOT_FAST_CONNECT``) and a falsy denylist where the default is on
108
+ (``AIDOT_SDES_SERVE_AUDIO``). Reading only the literal ``"1"`` broke that:
109
+ ``AIDOT_FAST_CONNECT=true`` became an explicit False and *disabled* the
110
+ feature. Folding an unreadable value to False would break it the other way,
111
+ turning ``AIDOT_SDES_SERVE_AUDIO=`` - an ordinary empty-value shape in a
112
+ systemd unit or a compose file - into a silent loss of audio.
113
+
114
+ Not every knob is safe to route through this. A handful still compare against
115
+ a bare ``"0"`` (``AIDOT_SERVE_RELAY``, ``AIDOT_INCLUDE_SHARED_HOUSES``), where
116
+ a word spelling reads as true whatever it says; passing one of those through
117
+ here would hand the library an override it disagrees with. Check the
118
+ resolver before adding a knob to ``cmd_stream``."""
119
+ # Unset and unreadable are the same answer, so let the default reach the
120
+ # tuples rather than branching on it separately.
121
+ val = os.environ.get(name, "").strip().lower()
122
+ if val in ("1", "true", "yes", "on"):
123
+ return True
124
+ if val in ("0", "false", "no", "off"):
125
+ return False
126
+ return None
97
127
 
98
128
 
99
129
  async def _cameras(client: AidotClient) -> list[dict]:
@@ -187,14 +217,17 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
187
217
  "stdout for a go2rtc exec: consumer."
188
218
  )
189
219
  elif output_url.startswith("rtsp") and not dc.is_sdes_camera:
220
+ # Why G.711 here: see the codec_args branch in camera/client.py.
190
221
  _LOGGER.info(
191
- "DTLS RTSP push: publishing video + AAC audio to %s.", output_url
222
+ "DTLS RTSP push: publishing video + G.711 audio to %s. Pass '-' "
223
+ "instead to keep the mux's 48 kHz AAC.", output_url
192
224
  )
193
225
  elif not output_url.startswith("rtsp") and dc.is_sdes_camera:
194
226
  _LOGGER.warning(
195
- "SDES camera but output %r is not rtsp:// - the library will "
196
- "HTTP-serve (video only by default) instead of pushing audio+video. "
197
- "For go2rtc exec, pass {output}.",
227
+ "SDES camera but output %r is not rtsp:// - only an http(s) "
228
+ "output is served for a consumer to pull; anything else is "
229
+ "treated as a publish target and will fail. For go2rtc exec, "
230
+ "pass {output} and let go2rtc substitute its own rtsp:// URL.",
198
231
  output_url,
199
232
  )
200
233
  await dc.async_login()
@@ -5970,8 +5970,8 @@ class CameraMixin(_CameraControlsMixin, _CameraSdMixin, _WebRTCOpenMixin, _SdesO
5970
5970
  # `-bsf:a aac_adtstoasc` does not save it: the header is written
5971
5971
  # before the filter has produced extradata. So transcode to G.711
5972
5972
  # A-law, which is RTP-native, costs almost nothing at 8 kHz mono,
5973
- # and is exactly what the SDES push has always carried - the two
5974
- # push paths now put the same thing on the wire.
5973
+ # and was what the SDES push carried until that path moved to AAC
5974
+ # (fMP4/MSE has no G.711 mapping, so browsers got silence there).
5975
5975
  codec_args = ["-c:v", "copy", "-c:a", "pcm_alaw", "-ar", "8000", "-ac", "1"]
5976
5976
  out_args = ["-f", "rtsp", "-rtsp_transport", "tcp", serve_url]
5977
5977
  out_target = asyncio.subprocess.DEVNULL