python-aidot-cameras 1.0.0rc4__tar.gz → 1.0.0rc6__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 (282) hide show
  1. {python_aidot_cameras-1.0.0rc4/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc6}/PKG-INFO +166 -16
  2. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/README.md +165 -15
  3. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/__main__.py +35 -12
  4. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/client.py +87 -10
  5. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sdes_open.py +67 -0
  6. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/pyproject.toml +1 -1
  7. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6/python_aidot_cameras.egg-info}/PKG-INFO +166 -16
  8. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  9. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_camera_properties.py +64 -0
  10. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_go2rtc_cli.py +58 -2
  11. python_aidot_cameras-1.0.0rc6/tests/test_off_subnet_candidate.py +68 -0
  12. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/LICENSE +0 -0
  13. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/MANIFEST.in +0 -0
  14. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_upstream.py +0 -0
  16. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  18. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  20. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  22. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  23. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  24. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  25. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  26. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  27. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  28. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  29. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  30. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  31. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  32. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  33. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  34. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  35. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  36. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  37. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  38. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  39. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  40. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  41. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  43. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  44. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  45. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  46. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  47. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  48. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  49. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  50. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/ble_gateway.py +0 -0
  51. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/__init__.py +0 -0
  52. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/constants.py +0 -0
  53. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/controls.py +0 -0
  54. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/go2rtc.py +0 -0
  55. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/hwaccel.py +0 -0
  56. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/lan_control.py +0 -0
  57. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/models.py +0 -0
  58. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/playback.py +0 -0
  59. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/protocol.py +0 -0
  60. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_events.py +0 -0
  61. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_listing.py +0 -0
  62. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sd_playback.py +0 -0
  63. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/sdes.py +0 -0
  64. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/tutk.py +0 -0
  65. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/webrtc.py +0 -0
  66. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/camera/webrtc_open.py +0 -0
  67. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/client.py +0 -0
  68. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/cloud_auth.py +0 -0
  69. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/const.py +0 -0
  70. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/credentials.py +0 -0
  71. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/crypto.py +0 -0
  72. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/device_client.py +0 -0
  73. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/discover.py +0 -0
  74. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/exceptions.py +0 -0
  75. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/g711.py +0 -0
  76. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/lan_retry.py +0 -0
  77. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/aidot_cameras/py.typed +0 -0
  78. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  79. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  80. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/requires.txt +0 -0
  81. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  82. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/setup.cfg +0 -0
  83. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_aioice_compat.py +0 -0
  84. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_alarm_event.py +0 -0
  85. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_pt_kinds.py +0 -0
  86. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_sdp_video_pt.py +0 -0
  87. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_answer_section_selection.py +0 -0
  88. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_audio_mux_offline.py +0 -0
  89. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  90. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_request.py +0 -0
  91. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_response_parse.py +0 -0
  92. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_avio_response_router.py +0 -0
  93. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_backoff.py +0 -0
  94. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_detection.py +0 -0
  95. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_evidence_corroboration.py +0 -0
  96. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_battery_skip_turn.py +0 -0
  97. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_bitrate_campaign_arms.py +0 -0
  98. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ble_gateway.py +0 -0
  99. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_bridge_state_sentinels.py +0 -0
  100. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_busy_backoff.py +0 -0
  101. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_canary_log_names_the_camera.py +0 -0
  102. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_cloud_count.py +0 -0
  103. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_cloud_plan.py +0 -0
  104. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_constants_imports_resolve.py +0 -0
  105. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_control_verdicts.py +0 -0
  106. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_deferred_hardening.py +0 -0
  107. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_cmd_seq_match.py +0 -0
  108. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_login_guard.py +0 -0
  109. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_device_user_info_cache.py +0 -0
  110. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_direct_ts_keyframe_signal.py +0 -0
  111. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_direct_ts_serve.py +0 -0
  112. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dormant_camera_cleanup.py +0 -0
  113. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_drain_is_cancellable.py +0 -0
  114. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_mux_terminal_write.py +0 -0
  115. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_not_ready_burst.py +0 -0
  116. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_pinning.py +0 -0
  117. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_resolution_ack.py +0 -0
  118. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_decode_skip.py +0 -0
  119. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_ice_wait.py +0 -0
  120. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_serve_open_timeout.py +0 -0
  121. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_skip_signaling_wait.py +0 -0
  122. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_slow_probe.py +0 -0
  123. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_stdout_producer.py +0 -0
  124. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_dtls_video_presence.py +0 -0
  125. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_egress_guard.py +0 -0
  126. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_env_seams.py +0 -0
  127. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  128. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  129. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_futile_keepalive.py +0 -0
  130. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  131. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_go2rtc.py +0 -0
  132. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_h264_decode_rate_limit.py +0 -0
  133. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_highport_nomination.py +0 -0
  134. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_hwaccel_probe.py +0 -0
  135. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ice_config_cache.py +0 -0
  136. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ice_peer_reflexive.py +0 -0
  137. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_idle_release_both_loops.py +0 -0
  138. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_keepalive_renew.py +0 -0
  139. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_key_restart_sdp.py +0 -0
  140. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_keyframe_prompter.py +0 -0
  141. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_lan_control.py +0 -0
  142. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  143. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_late_repeats_are_dropped.py +0 -0
  144. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_light_active_color_mode.py +0 -0
  145. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_session_carries_talk.py +0 -0
  146. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_stream_param.py +0 -0
  147. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_validate_policy.py +0 -0
  148. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_live_video_canary.py +0 -0
  149. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_liveplay_echo_timeout.py +0 -0
  150. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_local_only_controls.py +0 -0
  151. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_logging_caps.py +0 -0
  152. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_login_connect_deadline.py +0 -0
  153. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_login_reconnect_backoff.py +0 -0
  154. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_counted_only_when_plaintext.py +0 -0
  155. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_path_is_reported.py +0 -0
  156. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_socket_rcvbuf.py +0 -0
  157. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_media_wait_floor.py +0 -0
  158. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_motion_poll.py +0 -0
  159. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_client_id_is_exact.py +0 -0
  160. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_credential_refetch.py +0 -0
  161. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_credential_self_heal.py +0 -0
  162. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_password_not_persisted.py +0 -0
  163. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_session_reconnect.py +0 -0
  164. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  165. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_nack.py +0 -0
  166. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  167. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_narrow_pc_ice.py +0 -0
  168. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_key_material_in_logs.py +0 -0
  169. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_self_referential_go2rtc.py +0 -0
  170. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_no_undefined_names.py +0 -0
  171. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offer_bandwidth_line.py +0 -0
  172. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  173. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_offline_keepalive_pause.py +0 -0
  174. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_on_frame_is_an_av_frame.py +0 -0
  175. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_open_fail_logger.py +0 -0
  176. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_open_gate_delay.py +0 -0
  177. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_peer_id_client_type.py +0 -0
  178. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_peerid_reuse.py +0 -0
  179. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_persistent_mqtt.py +0 -0
  180. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_persistent_mqtt_robustness.py +0 -0
  181. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_playback_tls.py +0 -0
  182. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_post_merge_hardening.py +0 -0
  183. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  184. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_public_signature_matches_impl.py +0 -0
  185. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_publish_target_readiness.py +0 -0
  186. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_quality_campaign_in_session.py +0 -0
  187. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_reboot_action.py +0 -0
  188. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_recent_recordings.py +0 -0
  189. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_remb.py +0 -0
  190. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  191. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_resent_frames_are_dropped.py +0 -0
  192. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_resolution_persists.py +0 -0
  193. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_restart_cycle.py +0 -0
  194. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_retry_policy.py +0 -0
  195. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_scripts_importable.py +0 -0
  196. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_init_ack_tsn.py +0 -0
  197. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_reassembly.py +0 -0
  198. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sctp_sack.py +0 -0
  199. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_card_presence.py +0 -0
  200. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_event_decode.py +0 -0
  201. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_event_probe_payloads.py +0 -0
  202. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_listing.py +0 -0
  203. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sd_playback.py +0 -0
  204. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_adaptive.py +0 -0
  205. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_avio_dispatch.py +0 -0
  206. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_bridge_break_gate.py +0 -0
  207. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_connection_mode.py +0 -0
  208. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_echo_wait_timeout.py +0 -0
  209. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_fast_liveplay.py +0 -0
  210. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  211. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_first_media_stall_report.py +0 -0
  212. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_ice_teardown.py +0 -0
  213. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_idle_release.py +0 -0
  214. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_nack_bridge.py +0 -0
  215. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_offer_video_order.py +0 -0
  216. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_offer_video_pin.py +0 -0
  217. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_open_cleanup.py +0 -0
  218. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_sctp_abort.py +0 -0
  219. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_audio.py +0 -0
  220. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_cmd.py +0 -0
  221. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_serve_stderr_drain.py +0 -0
  222. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_session_cap.py +0 -0
  223. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_sprop.py +0 -0
  224. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_talk.py +0 -0
  225. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_teardown_exit_log.py +0 -0
  226. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_terminal_ack.py +0 -0
  227. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_terminal_ack_latency.py +0 -0
  228. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_tmmbr_bridge.py +0 -0
  229. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdes_watchdog.py +0 -0
  230. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdp_narrowing_properties.py +0 -0
  231. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sdp_payload_type_narrowing.py +0 -0
  232. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_security_hardening.py +0 -0
  233. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  234. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serializable_login_info.py +0 -0
  235. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_input_timeout.py +0 -0
  236. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_loss_tolerance.py +0 -0
  237. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_pt_respects_the_pin.py +0 -0
  238. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_relay.py +0 -0
  239. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  240. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_session_stats.py +0 -0
  241. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_shared_house_seam.py +0 -0
  242. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_snapshot_cancellation.py +0 -0
  243. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_speak.py +0 -0
  244. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_capture_gate.py +0 -0
  245. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_staleness.py +0 -0
  246. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_unstable_public.py +0 -0
  247. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  248. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_srtp_rx_rekey.py +0 -0
  249. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_srtp_tx_key_note.py +0 -0
  250. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stable_terminal_id.py +0 -0
  251. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_nudge_policy.py +0 -0
  252. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  253. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  254. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  255. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  256. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  257. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_cap.py +0 -0
  258. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_drain_ownership.py +0 -0
  259. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_idle.py +0 -0
  260. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_slot_cap.py +0 -0
  261. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_stream_teardown.py +0 -0
  262. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_talk.py +0 -0
  263. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  264. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tap_to_mux_end_to_end.py +0 -0
  265. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_terminal_ack.py +0 -0
  266. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tmmbr.py +0 -0
  267. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_token_file_robustness.py +0 -0
  268. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_token_refresh.py +0 -0
  269. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_turn_create_permission.py +0 -0
  270. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  271. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_upstream_compat.py +0 -0
  272. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_validator_shares_one_token.py +0 -0
  273. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_profile_log.py +0 -0
  274. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_pts_dts.py +0 -0
  275. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_video_ts_unwrap.py +0 -0
  276. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_check_throttle.py +0 -0
  277. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_presence.py +0 -0
  278. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_viewer_present_push_mode.py +0 -0
  279. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wait_or_event.py +0 -0
  280. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wake_gate_camera_evidence.py +0 -0
  281. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/tests/test_wake_readiness_retry.py +0 -0
  282. {python_aidot_cameras-1.0.0rc4 → python_aidot_cameras-1.0.0rc6}/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.0rc4
3
+ Version: 1.0.0rc6
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.
@@ -419,6 +568,7 @@ audio, idle release, the sprop cache path) are documented in
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
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). 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) |
571
+ | `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
572
  | `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
573
  | `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
574
  | `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.
@@ -379,6 +528,7 @@ audio, idle release, the sprop cache path) are documented in
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
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). 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) |
531
+ | `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
532
  | `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
533
  | `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
534
  | `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,28 @@ _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)."""
94
+ """Tri-state env flag: truthy -> True, falsy -> False, anything else -> None.
95
+
96
+ ``None`` means "no opinion", so ``start_keepalive`` leaves the knob unset and
97
+ the library's own resolver reads the environment itself. That matters because
98
+ the library's resolvers do not share one convention: ``AIDOT_FAST_CONNECT`` is
99
+ a truthy allowlist over a default of off, while ``AIDOT_SDES_SERVE_AUDIO`` is a
100
+ falsy denylist over a default of *on*. No single reader here can mirror both,
101
+ so anything not plainly true or false must not become an override - reading
102
+ only the literal ``"1"`` made ``AIDOT_FAST_CONNECT=true`` an explicit False that
103
+ disabled the feature the user asked for, and returning False for an
104
+ unrecognised value would do the same to ``AIDOT_SDES_SERVE_AUDIO=`` (an
105
+ ordinary empty-value shape in a systemd unit or a compose file), silently
106
+ dropping audio the library would have served."""
93
107
  raw = os.environ.get(name)
94
108
  if raw is None:
95
109
  return None
96
- return raw == "1"
110
+ val = raw.strip().lower()
111
+ if val in ("1", "true", "yes", "on"):
112
+ return True
113
+ if val in ("0", "false", "no", "off"):
114
+ return False
115
+ return None
97
116
 
98
117
 
99
118
  async def _cameras(client: AidotClient) -> list[dict]:
@@ -187,14 +206,18 @@ async def cmd_stream(dev_id: str, output_url: str) -> int:
187
206
  "stdout for a go2rtc exec: consumer."
188
207
  )
189
208
  elif output_url.startswith("rtsp") and not dc.is_sdes_camera:
209
+ # The RTSP muxer refuses the mux's ADTS AAC, so this path transcodes
210
+ # audio to G.711 A-law; '-' is the form that keeps 48 kHz AAC.
190
211
  _LOGGER.info(
191
- "DTLS RTSP push: publishing video + AAC audio to %s.", output_url
212
+ "DTLS RTSP push: publishing video + G.711 audio to %s. Pass '-' "
213
+ "instead to keep the mux's 48 kHz AAC.", output_url
192
214
  )
193
215
  elif not output_url.startswith("rtsp") and dc.is_sdes_camera:
194
216
  _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}.",
217
+ "SDES camera but output %r is not rtsp:// - only an http(s) "
218
+ "output is served for a consumer to pull; anything else is "
219
+ "treated as a publish target and will fail. For go2rtc exec, "
220
+ "pass {output} and let go2rtc substitute its own rtsp:// URL.",
198
221
  output_url,
199
222
  )
200
223
  await dc.async_login()