python-aidot-cameras 1.0.0rc44__tar.gz → 1.0.0rc45__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 (337) hide show
  1. {python_aidot_cameras-1.0.0rc44/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc45}/PKG-INFO +47 -41
  2. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/README.md +46 -40
  3. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/client.py +88 -16
  4. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/protocol.py +95 -9
  5. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/rtsp_publish.py +2 -1
  6. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sdes_open.py +178 -28
  7. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/ts_fanout.py +23 -0
  8. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/ts_tee.py +11 -3
  9. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/webrtc_open.py +5 -16
  10. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/lan_retry.py +89 -5
  11. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/pyproject.toml +1 -1
  12. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45/python_aidot_cameras.egg-info}/PKG-INFO +47 -41
  13. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/SOURCES.txt +3 -0
  14. python_aidot_cameras-1.0.0rc45/tests/test_first_media_abandon_rules.py +116 -0
  15. python_aidot_cameras-1.0.0rc45/tests/test_idle_release_both_loops.py +112 -0
  16. python_aidot_cameras-1.0.0rc45/tests/test_lan_login_accessors.py +85 -0
  17. python_aidot_cameras-1.0.0rc45/tests/test_lan_retry_silences_upstream_over_ceiling.py +186 -0
  18. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_validate_policy.py +3 -3
  19. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_first_media_stall_report.py +43 -0
  20. python_aidot_cameras-1.0.0rc45/tests/test_sdes_idle_release.py +351 -0
  21. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ts_fanout.py +50 -2
  22. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ts_tee.py +137 -1
  23. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_presence.py +14 -0
  24. python_aidot_cameras-1.0.0rc44/tests/test_idle_release_both_loops.py +0 -49
  25. python_aidot_cameras-1.0.0rc44/tests/test_sdes_idle_release.py +0 -157
  26. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/LICENSE +0 -0
  27. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/MANIFEST.in +0 -0
  28. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/__init__.py +0 -0
  29. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/__main__.py +0 -0
  30. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_log_redaction.py +0 -0
  31. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_upstream.py +0 -0
  32. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/__init__.py +0 -0
  33. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  34. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  35. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  36. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  37. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  38. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  39. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  40. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  41. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  42. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  43. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  44. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  45. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  46. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  47. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  48. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  49. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  50. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  51. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  52. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  53. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  54. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  55. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  56. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  57. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  58. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  59. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  60. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  61. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  62. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  63. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  64. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  65. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  66. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/__init__.py +0 -0
  67. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/aac_track.py +0 -0
  68. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/constants.py +0 -0
  69. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/controls.py +0 -0
  70. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/go2rtc.py +0 -0
  71. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/h264_sps.py +0 -0
  72. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/hls_ts.py +0 -0
  73. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/hwaccel.py +0 -0
  74. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/lan_control.py +0 -0
  75. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/models.py +0 -0
  76. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/playback.py +0 -0
  77. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/recording.py +0 -0
  78. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/rtp_h264.py +0 -0
  79. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_events.py +0 -0
  80. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_listing.py +0 -0
  81. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sd_playback.py +0 -0
  82. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/sdes.py +0 -0
  83. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/tutk.py +0 -0
  84. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/camera/webrtc.py +0 -0
  85. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/client.py +0 -0
  86. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/cloud_auth.py +0 -0
  87. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/const.py +0 -0
  88. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/credentials.py +0 -0
  89. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/crypto.py +0 -0
  90. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/device_client.py +0 -0
  91. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/discover.py +0 -0
  92. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/exceptions.py +0 -0
  93. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/g711.py +0 -0
  94. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/aidot_cameras/py.typed +0 -0
  95. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  96. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  97. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/requires.txt +0 -0
  98. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  99. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/setup.cfg +0 -0
  100. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_aac_track.py +0 -0
  101. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_abandoned_attempt_skips_the_doomed_serve.py +0 -0
  102. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_abandoned_wait_still_maps_audio.py +0 -0
  103. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_aioice_compat.py +0 -0
  104. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_alarm_event.py +0 -0
  105. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_pt_kinds.py +0 -0
  106. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_sdp_video_pt.py +0 -0
  107. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_answer_section_selection.py +0 -0
  108. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_arrival_timestamps.py +0 -0
  109. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_audio_mux_offline.py +0 -0
  110. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auth_error_routing.py +0 -0
  111. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auto_alarm.py +0 -0
  112. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_auto_light_settings.py +0 -0
  113. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  114. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_request.py +0 -0
  115. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_response_parse.py +0 -0
  116. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_avio_response_router.py +0 -0
  117. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_backoff.py +0 -0
  118. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_detection.py +0 -0
  119. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_evidence_corroboration.py +0 -0
  120. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_stale_offer_is_abandoned_early.py +0 -0
  121. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_battery_wake_settles_before_the_command.py +0 -0
  122. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ble_gateway_is_gone.py +0 -0
  123. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_bridge_state_sentinels.py +0 -0
  124. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_busy_backoff.py +0 -0
  125. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_camera_properties.py +0 -0
  126. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_canary_log_names_the_camera.py +0 -0
  127. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_count.py +0 -0
  128. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_error_log_names_the_exception.py +0 -0
  129. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_error_severity.py +0 -0
  130. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_cloud_plan.py +0 -0
  131. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_constants_imports_resolve.py +0 -0
  132. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_control_verdicts.py +0 -0
  133. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_declared_properties.py +0 -0
  134. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_deferred_hardening.py +0 -0
  135. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_detection_types.py +0 -0
  136. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_cmd_seq_match.py +0 -0
  137. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_login_guard.py +0 -0
  138. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_device_user_info_cache.py +0 -0
  139. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_publish_attaches_negotiated_audio.py +0 -0
  140. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_keyframe_signal.py +0 -0
  141. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_serve.py +0 -0
  142. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_direct_ts_serve_only_for_http.py +0 -0
  143. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dormant_camera_cleanup.py +0 -0
  144. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_drain_is_cancellable.py +0 -0
  145. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_mux_terminal_write.py +0 -0
  146. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_not_ready_burst.py +0 -0
  147. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_pinning.py +0 -0
  148. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_recorder_wiring.py +0 -0
  149. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_resolution_ack.py +0 -0
  150. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_decode_skip.py +0 -0
  151. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_ice_wait.py +0 -0
  152. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_serve_open_timeout.py +0 -0
  153. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_skip_signaling_wait.py +0 -0
  154. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_slow_probe.py +0 -0
  155. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_stdout_producer.py +0 -0
  156. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_tap_states_no_reorder.py +0 -0
  157. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_ts_copy_recording.py +0 -0
  158. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_dtls_video_presence.py +0 -0
  159. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_egress_guard.py +0 -0
  160. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_env_seams.py +0 -0
  161. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  162. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  163. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_futile_keepalive.py +0 -0
  164. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  165. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_go2rtc.py +0 -0
  166. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_go2rtc_cli.py +0 -0
  167. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_h264_decode_rate_limit.py +0 -0
  168. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_h264_sps.py +0 -0
  169. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_highport_nomination.py +0 -0
  170. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hls_ts_end_to_end.py +0 -0
  171. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hls_ts_wiring.py +0 -0
  172. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_hwaccel_probe.py +0 -0
  173. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_config_cache.py +0 -0
  174. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_credentials_match_the_app.py +0 -0
  175. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ice_peer_reflexive.py +0 -0
  176. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_keepalive_renew.py +0 -0
  177. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_key_restart_sdp.py +0 -0
  178. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_keyframe_prompter.py +0 -0
  179. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_control.py +0 -0
  180. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  181. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_lan_sweep_only_with_lights.py +0 -0
  182. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_late_repeats_are_dropped.py +0 -0
  183. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_light_active_color_mode.py +0 -0
  184. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_session_carries_talk.py +0 -0
  185. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_stream_param.py +0 -0
  186. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_validate_token_refresh.py +0 -0
  187. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_live_video_canary.py +0 -0
  188. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_liveplay_echo_timeout.py +0 -0
  189. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_local_only_controls.py +0 -0
  190. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_log_hygiene.py +0 -0
  191. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_logging_caps.py +0 -0
  192. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_login_connect_deadline.py +0 -0
  193. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_login_reconnect_backoff.py +0 -0
  194. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_counted_only_when_plaintext.py +0 -0
  195. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_path_is_reported.py +0 -0
  196. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_socket_rcvbuf.py +0 -0
  197. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_media_wait_floor.py +0 -0
  198. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_motion_poll.py +0 -0
  199. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_client_id_is_exact.py +0 -0
  200. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_credential_refetch.py +0 -0
  201. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_credential_self_heal.py +0 -0
  202. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_password_not_persisted.py +0 -0
  203. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_session_reconnect.py +0 -0
  204. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  205. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nack.py +0 -0
  206. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  207. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_narrow_pc_ice.py +0 -0
  208. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_key_material_in_logs.py +0 -0
  209. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_private_notes_on_the_public_surface.py +0 -0
  210. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_self_referential_go2rtc.py +0 -0
  211. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_unbounded_hex_dumps.py +0 -0
  212. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_no_undefined_names.py +0 -0
  213. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_nominates_as_soon_as_the_answer_lands.py +0 -0
  214. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_off_subnet_candidate.py +0 -0
  215. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  216. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_offline_keepalive_pause.py +0 -0
  217. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_on_frame_is_an_av_frame.py +0 -0
  218. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_open_fail_logger.py +0 -0
  219. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_open_gate_delay.py +0 -0
  220. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peer_id_class_env.py +0 -0
  221. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peer_id_client_type.py +0 -0
  222. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_peerid_reuse.py +0 -0
  223. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_persistent_mqtt.py +0 -0
  224. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_persistent_mqtt_robustness.py +0 -0
  225. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_playback_tls.py +0 -0
  226. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_post_merge_hardening.py +0 -0
  227. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  228. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_public_signature_matches_impl.py +0 -0
  229. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_publish_target_readiness.py +0 -0
  230. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_quality_campaign_in_session.py +0 -0
  231. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_reboot_action.py +0 -0
  232. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_recent_recordings.py +0 -0
  233. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_remb.py +0 -0
  234. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  235. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_removed_keepalive_arguments_are_tolerated.py +0 -0
  236. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_resent_frames_are_dropped.py +0 -0
  237. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_resolution_persists.py +0 -0
  238. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_restart_cycle.py +0 -0
  239. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_retry_policy.py +0 -0
  240. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtp_h264.py +0 -0
  241. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtsp_publish.py +0 -0
  242. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_rtsp_publish_go2rtc.py +0 -0
  243. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_scripts_importable.py +0 -0
  244. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_init_ack_tsn.py +0 -0
  245. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_reassembly.py +0 -0
  246. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sctp_sack.py +0 -0
  247. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_card_presence.py +0 -0
  248. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_event_decode.py +0 -0
  249. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_event_probe_payloads.py +0 -0
  250. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_listing.py +0 -0
  251. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sd_playback.py +0 -0
  252. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_avio_dispatch.py +0 -0
  253. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_bridge_break_gate.py +0 -0
  254. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_connection_mode.py +0 -0
  255. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_echo_wait_is_not_dead_time.py +0 -0
  256. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_fast_liveplay.py +0 -0
  257. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  258. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_ice_teardown.py +0 -0
  259. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_nack_bridge.py +0 -0
  260. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_offer_video_pin.py +0 -0
  261. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_open_cleanup.py +0 -0
  262. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_pinned_codec_retry.py +0 -0
  263. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_sctp_abort.py +0 -0
  264. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_audio.py +0 -0
  265. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_cmd.py +0 -0
  266. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_serve_stderr_drain.py +0 -0
  267. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_session_cap.py +0 -0
  268. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_sprop.py +0 -0
  269. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_talk.py +0 -0
  270. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_teardown_exit_log.py +0 -0
  271. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_terminal_ack.py +0 -0
  272. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_terminal_ack_latency.py +0 -0
  273. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdes_watchdog.py +0 -0
  274. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdp_narrowing_properties.py +0 -0
  275. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sdp_payload_type_narrowing.py +0 -0
  276. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_security_hardening.py +0 -0
  277. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  278. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serializable_login_info.py +0 -0
  279. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_bound_survives_arrival_stamping.py +0 -0
  280. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_input_timeout.py +0 -0
  281. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_loss_tolerance.py +0 -0
  282. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_pt_respects_the_pin.py +0 -0
  283. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_relay.py +0 -0
  284. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  285. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_session_stats.py +0 -0
  286. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_session_stop_twice.py +0 -0
  287. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_shared_house_seam.py +0 -0
  288. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_signalling_is_attributable_to_a_camera.py +0 -0
  289. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_skip_guard.py +0 -0
  290. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_snapshot_cancellation.py +0 -0
  291. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_speak.py +0 -0
  292. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_capture_gate.py +0 -0
  293. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_staleness.py +0 -0
  294. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_unstable_public.py +0 -0
  295. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  296. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_srtp_rx_rekey.py +0 -0
  297. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_srtp_tx_key_note.py +0 -0
  298. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stable_terminal_id.py +0 -0
  299. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_nudge_policy.py +0 -0
  300. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  301. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  302. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  303. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  304. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  305. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_status_and_trace_channels.py +0 -0
  306. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_cap.py +0 -0
  307. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_drain_ownership.py +0 -0
  308. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_idle.py +0 -0
  309. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_slot_cap.py +0 -0
  310. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_stream_teardown.py +0 -0
  311. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_talk.py +0 -0
  312. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  313. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tap_to_mux_end_to_end.py +0 -0
  314. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_terminal_ack.py +0 -0
  315. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tmmbr.py +0 -0
  316. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_token_file_robustness.py +0 -0
  317. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_token_refresh.py +0 -0
  318. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_transport_failure_reporting.py +0 -0
  319. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_turn_create_permission.py +0 -0
  320. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  321. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_unbuildable_summary_repeats_at_debug.py +0 -0
  322. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_unreachable_nominee_is_abandoned_early.py +0 -0
  323. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_compat.py +0 -0
  324. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_seam_adapter.py +0 -0
  325. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_upstream_secrets_redacted.py +0 -0
  326. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_validator_shares_one_token.py +0 -0
  327. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_vendored_aiortc_integrity.py +0 -0
  328. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_presence_needs_a_keyframe.py +0 -0
  329. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_profile_log.py +0 -0
  330. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_pts_dts.py +0 -0
  331. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_video_ts_unwrap.py +0 -0
  332. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_check_throttle.py +0 -0
  333. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_viewer_present_push_mode.py +0 -0
  334. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wait_or_event.py +0 -0
  335. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wake_gate_camera_evidence.py +0 -0
  336. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/tests/test_wake_readiness_retry.py +0 -0
  337. {python_aidot_cameras-1.0.0rc44 → python_aidot_cameras-1.0.0rc45}/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.0rc44
3
+ Version: 1.0.0rc45
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
@@ -100,11 +100,11 @@ not going to change in this release. Neither is a fault to report.
100
100
  records to the cloud or to its own SD card is per-camera, not per-model, and the
101
101
  device reports it as `IsSupportPlayback`. This library lists cloud recordings
102
102
  (`async_get_cloud_recordings`) and resolves a playable HLS URL for one
103
- (`async_get_event_video_media`); it has no SD-card equivalent, so a camera
104
- storing to a card records normally and
105
- nothing here can play it back. On the reference fleet that is four of seven
106
- cameras. The vendor's own commands for it are identified but their responses
107
- have never been decoded; see `docs/ROAD-TO-1.0.md` item 6.
103
+ (`async_get_event_video_media`); for a camera storing to a card it lists the
104
+ on-device recordings (`async_get_sd_recordings`) but cannot play one back, so
105
+ such a camera records normally and nothing here can show you the video. On the
106
+ reference fleet that is four of seven cameras. Playback from the card is closed
107
+ as not feasible; see `docs/CAMERAS.md`, "On-device (SD card) recordings".
108
108
 
109
109
  **The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
110
110
  app.** Measured from a capture of the app on the same camera: it takes about
@@ -268,9 +268,9 @@ decides it:
268
268
 
269
269
  | your camera | use | why |
270
270
  | --- | --- | --- |
271
- | **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 |
271
+ | **DTLS** (mains, e.g. A000088) | `'{output}'` or `-` | `{output}` is published by the library itself (direct publish, on by default): the camera's 8 kHz A-law plus an in-process AAC track, no ffmpeg. `-` writes the muxed MPEG-TS with its 48 kHz AAC to stdout instead |
272
272
  | **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
273
- | either, with `AIDOT_DIRECT_PUBLISH=1` | `'{output}'` | published by the library with no ffmpeg; audio stays 8 kHz A-law. An H.265 session falls back to the ffmpeg push |
273
+ | either, with `AIDOT_DIRECT_PUBLISH=0` | `'{output}'` | the push goes through ffmpeg instead, which has to transcode audio down to 8 kHz G.711. A session that negotiates H.265 takes this path whatever the setting |
274
274
  | either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
275
275
 
276
276
  `'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
@@ -301,12 +301,13 @@ go2rtc itself gets its environment: export them in the shell before starting it,
301
301
  or set them in its service unit or container definition. If a stream fails
302
302
  immediately with an authentication error, that is the thing to check first.
303
303
 
304
- **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
305
- stream entirely through an ffmpeg subprocess, and pip cannot install a system
306
- binary. DTLS cameras no longer serve through it: the muxed MPEG-TS goes straight
307
- to the consumer, because that hop was the only component in the chain that lost
308
- timestamps. ffmpeg is still required, both
309
- for the SDES path and as the DTLS fallback when the serve port cannot be bound.
304
+ **ffmpeg must be on PATH.** pip cannot install a system binary, and the
305
+ library still needs one: recordings, snapshots, the `-` stdout form, `http://`
306
+ pull serves and any session that negotiates H.265 run through an ffmpeg
307
+ subprocess, and it is the DTLS fallback when a serve port cannot be bound. A
308
+ live `{output}` push does not use it (next paragraph), and a DTLS `-` serve
309
+ sends the muxed MPEG-TS straight to the consumer, because that hop was the only
310
+ component in the chain that lost timestamps.
310
311
 
311
312
  **Live pushes skip ffmpeg: direct publish is on by default** (since 1.0.0rc44;
312
313
  `AIDOT_DIRECT_PUBLISH=0` restores the ffmpeg path). An `rtsp://` / `{output}`
@@ -316,8 +317,8 @@ camera can use `{output}` too. It takes H.264 sessions only; a session that
316
317
  negotiates H.265 keeps the ffmpeg serve. Audio goes out as the camera's own
317
318
  G.711 A-law plus an in-process AAC track (see "Audio" under
318
319
  [Getting an RTSP URL](#getting-an-rtsp-url)).
319
- ffmpeg is still used for recordings and snapshots. Off by default while it is
320
- being validated; see [`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
320
+ ffmpeg is still used for recordings and snapshots; see
321
+ [`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
321
322
 
322
323
  ```bash
323
324
  sudo apt install ffmpeg # Debian/Ubuntu
@@ -503,14 +504,16 @@ the last one leaves - nothing talks to your cameras while nobody is watching:
503
504
 
504
505
  ```yaml
505
506
  streams:
506
- driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
507
+ driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera (stdout form; {output} works too)
507
508
  frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
508
509
  ```
509
510
 
510
- Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
511
- difference, and `--list` told you which is which. (`{output}` is a placeholder
512
- go2rtc fills in with its own publish URL. It is not something you type at a
513
- shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
511
+ `{output}` works for both transports: the library publishes the stream into
512
+ go2rtc itself (direct publish, on by default since 1.0.0rc44). A DTLS camera
513
+ can also take `-`, which hands go2rtc the muxed MPEG-TS on stdout; an SDES
514
+ camera cannot, and `--list` told you which is which. (`{output}` is a
515
+ placeholder go2rtc fills in with its own publish URL. It is not something you
516
+ type at a shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
514
517
  variables the CLI does, so set them wherever go2rtc starts.
515
518
 
516
519
  **3. Use the URL.** go2rtc serves every stream you named at:
@@ -532,20 +535,20 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
532
535
  - **Which camera is which.** `--list` reports the transport per device. DTLS is
533
536
  the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
534
537
  form is not silent - the CLI tells you which one to use and exits.
535
- - **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
536
- 8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
537
- on the MSE path have no mapping for G.711 at all. Audio is on by default; see
538
- `AIDOT_SDES_SERVE_AUDIO` to turn it off. With `AIDOT_DIRECT_PUBLISH=1` the
539
- publish carries the camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC
540
- viewers play it natively and nothing is transcoded. With
541
- `AIDOT_PUBLISH_AAC=1` it also carries an **AAC-LC 48 kHz mono** track
542
- encoded in-process; an AAC-only consumer (for example Home Assistant's HLS
543
- player) selects it with `?audio=aac`. No ffmpeg transcode
544
- source is needed for this - see
538
+ - **Audio.** A `{output}` push (direct publish, the default) carries the
539
+ camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC viewers play it
540
+ natively and nothing is transcoded - and, unless `AIDOT_PUBLISH_AAC=0`, an
541
+ **AAC-LC 48 kHz mono** track encoded in-process; an AAC-only consumer (for
542
+ example Home Assistant's HLS player) selects it with `?audio=aac`. The `-`
543
+ form carries AAC at 48 kHz mono, resampled from the camera's 8 kHz A-law,
544
+ because 8 kHz AAC plays silent in a lot of browsers and browsers on the MSE
545
+ path have no mapping for G.711 at all. Audio is on by default; see
546
+ `AIDOT_SDES_SERVE_AUDIO` to turn it off. No ffmpeg transcode
547
+ source is needed for the AAC - see
545
548
  [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#an-aac-transcoding-source-makes-every-player-crawl)
546
549
  for why the old `ffmpeg:<stream>#audio=aac` advice made playback crawl.
547
- The AAC track is off by default. A camera whose audio is mu-law (PCMU)
548
- gets no AAC track either way.
550
+ The AAC track is on by default since 1.0.0rc44. A camera whose audio is
551
+ mu-law (PCMU) gets no AAC track either way.
549
552
  - **A stream that starts with no video** and picks it up a few seconds later is
550
553
  normal: the mux waits for a keyframe so the first GOP is decodable. A stream
551
554
  that stays audio-only is a camera that never sent one - retry the view.
@@ -554,9 +557,11 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
554
557
  spawns. That process has to stay running to keep the port bound, so it holds
555
558
  a camera session whether or not anyone is watching. Prefer the `exec:` forms.
556
559
  - **Pushing to your own RTSP server** rather than go2rtc: `rtsp_push_url=`
557
- from Python, or an `rtsp://` output on the CLI. Both transports support it.
558
- A DTLS push transcodes audio to G.711 A-law, matching what SDES has always
559
- sent - ffmpeg's RTSP muxer will not accept the AAC that MPEG-TS carries.
560
+ from Python, or an `rtsp://` output on the CLI. Both transports support it,
561
+ and by default the library publishes it directly, with the audio described
562
+ above. Only on the `AIDOT_DIRECT_PUBLISH=0` ffmpeg path does a DTLS push
563
+ transcode audio to G.711 A-law - ffmpeg's RTSP muxer will not accept the AAC
564
+ that MPEG-TS carries.
560
565
  `output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
561
566
  address above, and `ensure_stream(name, source)` registers a stream with a
562
567
  running go2rtc if you would rather not edit YAML.
@@ -583,7 +588,7 @@ The library reads the following environment variables.
583
588
 
584
589
  ### Credentials
585
590
 
586
- Used by the credential helper (`aidot.credentials`); they take priority over any
591
+ Used by the credential helper (`aidot_cameras.credentials`); they take priority over any
587
592
  stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
588
593
 
589
594
  | Variable | Purpose | Default |
@@ -604,13 +609,13 @@ knob is in
604
609
  | `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
605
610
  | `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
606
611
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
607
- | `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`) |
612
+ | `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, though 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`) |
608
613
  | `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. | `96` (H.264; `none` leaves the camera its choice) |
609
614
  | `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
610
- | `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve (see `AIDOT_DIRECT_PUBLISH_H265`). Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
611
- | `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. Off unless set: `0`/`false`/`no`/`off` (or unset) leaves it off; any other value turns it on. | `1` (on) |
615
+ | `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve. Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
616
+ | `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. **On by default** (since 1.0.0rc44); set `0`/`false`/`no`/`off` to turn it off. | `1` (on) |
612
617
  | `AIDOT_HLS_DIRECT_TS` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Give each DTLS camera a library-muxed MPEG-TS on a loopback listener (`CameraMixin.hls_ts_url()`), for Home Assistant's HLS view and recordings to read instead of go2rtc's RTSP. go2rtc re-bases each track for each consumer, so a reader that joins a running stream had its sound 0.1-0.75 s late, differently each time; this TS carries the publisher's video and AAC on one clock, so every reader gets them in the same step, whenever it joins. Needs `AIDOT_DIRECT_PUBLISH` and `AIDOT_PUBLISH_AAC`. An SDES camera gets a URL only on a model whose media the bridge decrypts (A001064, A001513) with its offer pinned to H.264 (`AIDOT_SDES_VIDEO_PT=96`), so that every session feeds it; pulled (non-publishing) sessions get none. When a camera's SPS changes (the A001064 changes it between sessions), the TS's readers are disconnected so they rebuild their decoder. Each camera's URL carries a secret made per listener, as `?auth=<secret>` (Home Assistant masks that query parameter when it logs a stream's URL); a request without it gets 404. For the owner's own tools, the listener's URL for any camera (`http://127.0.0.1:<port>/{name}.ts?auth=<secret>`, where `{name}` is `aidot_<first 12 of the device id>`) is left in `hls-ts-url` in the library's state directory (`AIDOT_SPROP_DIR`), owner-readable only, while the listener runs. A reader of the TS counts as a viewer. Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
613
- | `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone (it is retried again the next time something asks for it). Applies to every device, camera or not - the devices that hit this are lights. | `6` |
618
+ | `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone: discovery and the library's own retry loop stop kicking it, and when something else still asks for a login (Home Assistant's light poll does, every five minutes) the attempt is made quietly - upstream's per-attempt warning is dropped - until the device's address changes or a login gets through. One warning marks the moment. Applies to every device, camera or not - the devices that hit this are lights on another subnet. | `6` |
614
619
 
615
620
  ### Security hardening
616
621
 
@@ -623,3 +628,4 @@ the permissive default.
623
628
  | --- | --- | --- |
624
629
  | `AIDOT_ALLOW_LAN_SERVE` | Silences the warning emitted when decrypted media is served on a non-loopback bind (e.g. `0.0.0.0`), where any host on the LAN can read the unencrypted stream. Set when an exposed bind is intentional. | unset (warn on non-loopback) |
625
630
  | `AIDOT_CRED_KEY_FILE` | Path to the Fernet key file for stored credentials. Point it outside the config dir (ideally a separate secret store) so the key isn't co-located with the ciphertext. Applies to the default credentials path only (ignored when an explicit `creds_path` is passed). | `$XDG_CONFIG_HOME/aidot/.key` (falls back to `~/.config/aidot/.key`) |
631
+ | `AIDOT_PLAYBACK_TLS_VERIFY` | Set to `1` to require full certificate and hostname verification on the TLS connection to a camera's playback server. The camera presents a self-signed certificate, so this needs a trust anchor it chains to; unset, verification is skipped and a one-time warning is logged. | unset (skip verification, warn once) |
@@ -60,11 +60,11 @@ not going to change in this release. Neither is a fault to report.
60
60
  records to the cloud or to its own SD card is per-camera, not per-model, and the
61
61
  device reports it as `IsSupportPlayback`. This library lists cloud recordings
62
62
  (`async_get_cloud_recordings`) and resolves a playable HLS URL for one
63
- (`async_get_event_video_media`); it has no SD-card equivalent, so a camera
64
- storing to a card records normally and
65
- nothing here can play it back. On the reference fleet that is four of seven
66
- cameras. The vendor's own commands for it are identified but their responses
67
- have never been decoded; see `docs/ROAD-TO-1.0.md` item 6.
63
+ (`async_get_event_video_media`); for a camera storing to a card it lists the
64
+ on-device recordings (`async_get_sd_recordings`) but cannot play one back, so
65
+ such a camera records normally and nothing here can show you the video. On the
66
+ reference fleet that is four of seven cameras. Playback from the card is closed
67
+ as not feasible; see `docs/CAMERAS.md`, "On-device (SD card) recordings".
68
68
 
69
69
  **The A001064 (PTZ) streams at roughly 2.2 Mbps, about 24% above the vendor
70
70
  app.** Measured from a capture of the app on the same camera: it takes about
@@ -228,9 +228,9 @@ decides it:
228
228
 
229
229
  | your camera | use | why |
230
230
  | --- | --- | --- |
231
- | **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 |
231
+ | **DTLS** (mains, e.g. A000088) | `'{output}'` or `-` | `{output}` is published by the library itself (direct publish, on by default): the camera's 8 kHz A-law plus an in-process AAC track, no ffmpeg. `-` writes the muxed MPEG-TS with its 48 kHz AAC to stdout instead |
232
232
  | **SDES** (the A001513 and A001064 families) | `'{output}'` | these stream by pushing RTSP; there is nothing to read from stdout |
233
- | either, with `AIDOT_DIRECT_PUBLISH=1` | `'{output}'` | published by the library with no ffmpeg; audio stays 8 kHz A-law. An H.265 session falls back to the ffmpeg push |
233
+ | either, with `AIDOT_DIRECT_PUBLISH=0` | `'{output}'` | the push goes through ffmpeg instead, which has to transcode audio down to 8 kHz G.711. A session that negotiates H.265 takes this path whatever the setting |
234
234
  | either, if you want to pull | an `http://` URL | the process serves there and waits for a consumer to connect |
235
235
 
236
236
  `'{output}'` is a placeholder that go2rtc substitutes with the stream's own push
@@ -261,12 +261,13 @@ go2rtc itself gets its environment: export them in the shell before starting it,
261
261
  or set them in its service unit or container definition. If a stream fails
262
262
  immediately with an authentication error, that is the thing to check first.
263
263
 
264
- **ffmpeg must be on PATH.** SDES cameras - two of the three validated models -
265
- stream entirely through an ffmpeg subprocess, and pip cannot install a system
266
- binary. DTLS cameras no longer serve through it: the muxed MPEG-TS goes straight
267
- to the consumer, because that hop was the only component in the chain that lost
268
- timestamps. ffmpeg is still required, both
269
- for the SDES path and as the DTLS fallback when the serve port cannot be bound.
264
+ **ffmpeg must be on PATH.** pip cannot install a system binary, and the
265
+ library still needs one: recordings, snapshots, the `-` stdout form, `http://`
266
+ pull serves and any session that negotiates H.265 run through an ffmpeg
267
+ subprocess, and it is the DTLS fallback when a serve port cannot be bound. A
268
+ live `{output}` push does not use it (next paragraph), and a DTLS `-` serve
269
+ sends the muxed MPEG-TS straight to the consumer, because that hop was the only
270
+ component in the chain that lost timestamps.
270
271
 
271
272
  **Live pushes skip ffmpeg: direct publish is on by default** (since 1.0.0rc44;
272
273
  `AIDOT_DIRECT_PUBLISH=0` restores the ffmpeg path). An `rtsp://` / `{output}`
@@ -276,8 +277,8 @@ camera can use `{output}` too. It takes H.264 sessions only; a session that
276
277
  negotiates H.265 keeps the ffmpeg serve. Audio goes out as the camera's own
277
278
  G.711 A-law plus an in-process AAC track (see "Audio" under
278
279
  [Getting an RTSP URL](#getting-an-rtsp-url)).
279
- ffmpeg is still used for recordings and snapshots. Off by default while it is
280
- being validated; see [`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
280
+ ffmpeg is still used for recordings and snapshots; see
281
+ [`docs/DESIGN-direct-publish.md`](docs/DESIGN-direct-publish.md).
281
282
 
282
283
  ```bash
283
284
  sudo apt install ffmpeg # Debian/Ubuntu
@@ -463,14 +464,16 @@ the last one leaves - nothing talks to your cameras while nobody is watching:
463
464
 
464
465
  ```yaml
465
466
  streams:
466
- driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera
467
+ driveway: exec:aidot-go2rtc 1a2b3c4d... - # DTLS camera (stdout form; {output} works too)
467
468
  frontdoor: exec:aidot-go2rtc 5e6f7a8b... {output} # SDES camera
468
469
  ```
469
470
 
470
- Use `-` for a DTLS camera and `{output}` for an SDES one - that is the whole
471
- difference, and `--list` told you which is which. (`{output}` is a placeholder
472
- go2rtc fills in with its own publish URL. It is not something you type at a
473
- shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
471
+ `{output}` works for both transports: the library publishes the stream into
472
+ go2rtc itself (direct publish, on by default since 1.0.0rc44). A DTLS camera
473
+ can also take `-`, which hands go2rtc the muxed MPEG-TS on stdout; an SDES
474
+ camera cannot, and `--list` told you which is which. (`{output}` is a
475
+ placeholder go2rtc fills in with its own publish URL. It is not something you
476
+ type at a shell; `-` is the form for that.) go2rtc needs the same `AIDOT_*` environment
474
477
  variables the CLI does, so set them wherever go2rtc starts.
475
478
 
476
479
  **3. Use the URL.** go2rtc serves every stream you named at:
@@ -492,20 +495,20 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
492
495
  - **Which camera is which.** `--list` reports the transport per device. DTLS is
493
496
  the mains A000088; SDES is the A001064 / A001513 family. Passing the wrong
494
497
  form is not silent - the CLI tells you which one to use and exits.
495
- - **Audio.** Both forms carry AAC at 48 kHz mono, resampled from the camera's
496
- 8 kHz A-law, because 8 kHz AAC plays silent in a lot of browsers and browsers
497
- on the MSE path have no mapping for G.711 at all. Audio is on by default; see
498
- `AIDOT_SDES_SERVE_AUDIO` to turn it off. With `AIDOT_DIRECT_PUBLISH=1` the
499
- publish carries the camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC
500
- viewers play it natively and nothing is transcoded. With
501
- `AIDOT_PUBLISH_AAC=1` it also carries an **AAC-LC 48 kHz mono** track
502
- encoded in-process; an AAC-only consumer (for example Home Assistant's HLS
503
- player) selects it with `?audio=aac`. No ffmpeg transcode
504
- source is needed for this - see
498
+ - **Audio.** A `{output}` push (direct publish, the default) carries the
499
+ camera's own **PCMA (G.711 A-law, 8 kHz)** first - WebRTC viewers play it
500
+ natively and nothing is transcoded - and, unless `AIDOT_PUBLISH_AAC=0`, an
501
+ **AAC-LC 48 kHz mono** track encoded in-process; an AAC-only consumer (for
502
+ example Home Assistant's HLS player) selects it with `?audio=aac`. The `-`
503
+ form carries AAC at 48 kHz mono, resampled from the camera's 8 kHz A-law,
504
+ because 8 kHz AAC plays silent in a lot of browsers and browsers on the MSE
505
+ path have no mapping for G.711 at all. Audio is on by default; see
506
+ `AIDOT_SDES_SERVE_AUDIO` to turn it off. No ffmpeg transcode
507
+ source is needed for the AAC - see
505
508
  [`docs/CAMERAS.md`](https://github.com/cbrightly/python-aidot-cameras/blob/main/docs/CAMERAS.md#an-aac-transcoding-source-makes-every-player-crawl)
506
509
  for why the old `ffmpeg:<stream>#audio=aac` advice made playback crawl.
507
- The AAC track is off by default. A camera whose audio is mu-law (PCMU)
508
- gets no AAC track either way.
510
+ The AAC track is on by default since 1.0.0rc44. A camera whose audio is
511
+ mu-law (PCMU) gets no AAC track either way.
509
512
  - **A stream that starts with no video** and picks it up a few seconds later is
510
513
  normal: the mux waits for a keyframe so the first GOP is decodable. A stream
511
514
  that stays audio-only is a camera that never sent one - retry the view.
@@ -514,9 +517,11 @@ Stream #0:1: Audio: aac (LC), 48000 Hz, mono
514
517
  spawns. That process has to stay running to keep the port bound, so it holds
515
518
  a camera session whether or not anyone is watching. Prefer the `exec:` forms.
516
519
  - **Pushing to your own RTSP server** rather than go2rtc: `rtsp_push_url=`
517
- from Python, or an `rtsp://` output on the CLI. Both transports support it.
518
- A DTLS push transcodes audio to G.711 A-law, matching what SDES has always
519
- sent - ffmpeg's RTSP muxer will not accept the AAC that MPEG-TS carries.
520
+ from Python, or an `rtsp://` output on the CLI. Both transports support it,
521
+ and by default the library publishes it directly, with the audio described
522
+ above. Only on the `AIDOT_DIRECT_PUBLISH=0` ffmpeg path does a DTLS push
523
+ transcode audio to G.711 A-law - ffmpeg's RTSP muxer will not accept the AAC
524
+ that MPEG-TS carries.
520
525
  `output_path=` records to a file. `Go2RtcClient.rtsp_url(name)` builds the
521
526
  address above, and `ensure_stream(name, source)` registers a stream with a
522
527
  running go2rtc if you would rather not edit YAML.
@@ -543,7 +548,7 @@ The library reads the following environment variables.
543
548
 
544
549
  ### Credentials
545
550
 
546
- Used by the credential helper (`aidot.credentials`); they take priority over any
551
+ Used by the credential helper (`aidot_cameras.credentials`); they take priority over any
547
552
  stored credentials file. See [`aidot_cameras/credentials.py`](https://github.com/cbrightly/python-aidot-cameras/blob/main/aidot_cameras/credentials.py).
548
553
 
549
554
  | Variable | Purpose | Default |
@@ -564,13 +569,13 @@ knob is in
564
569
  | `AIDOT_VIDEO_DECODER` | Force a video decoder instead of measuring one: a decoder name (`h264_v4l2m2m`), or an acceleration method prefixed with `hwaccel:` (`hwaccel:videotoolbox`). The prefix is required because the two are not interchangeable - VideoToolbox and VAAPI have no decoder to name. | (measured) |
565
570
  | `AIDOT_DISABLE_HWACCEL` | Keep to software decoding and skip the measurement entirely. | (unset) |
566
571
  | `AIDOT_MAX_CONCURRENT_STREAMS` | Caps how many cameras stream at once. | `3` |
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`) |
572
+ | `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, though 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`) |
568
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. | `96` (H.264; `none` leaves the camera its choice) |
569
574
  | `AIDOT_PERSISTENT_MQTT` | Reuse ONE account-level persistent MQTT connection for commands, attribute fetches, and stream-open signaling (matching the official app) instead of connecting per operation. **On by default** (live soaks cut SDES NO_MEDIA from ~57% to ~11-19%); set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
570
- | `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve (see `AIDOT_DIRECT_PUBLISH_H265`). Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
571
- | `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. Off unless set: `0`/`false`/`no`/`off` (or unset) leaves it off; any other value turns it on. | `1` (on) |
575
+ | `AIDOT_DIRECT_PUBLISH` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Publish a live `rtsp://` push into go2rtc from the library itself instead of through ffmpeg, for both transports: SDES hands the bridge's plain RTP straight to an RTSP publisher, DTLS packetizes the tapped H.264 and A-law directly. No ffmpeg process per stream, audio is PCMA plus an in-process AAC track, packets put back in order before publishing, and audio attached from the camera's negotiated answer rather than waiting to observe a packet. Recordings, snapshots and `-`/`http://` serves are unchanged. Takes H.264 sessions only - an H.265 session keeps the ffmpeg serve. Live-validated with H.264 on A000088, A001064 and A001513 (see `docs/DESIGN-direct-publish.md`). Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
576
+ | `AIDOT_PUBLISH_AAC` | Whether a direct publish adds an AAC-LC 48 kHz mono track after the A-law one, encoded in-process, so an AAC-only consumer (Home Assistant's HLS player, `camera.record`) has sound; select it with go2rtc's `?audio=aac`. Costs one AAC encode per streaming camera for as long as its direct publish runs. A camera whose audio is mu-law (PCMU) gets no AAC track regardless. **On by default** (since 1.0.0rc44); set `0`/`false`/`no`/`off` to turn it off. | `1` (on) |
572
577
  | `AIDOT_HLS_DIRECT_TS` | **On by default** (since 1.0.0rc44; set `0` to turn it off). Give each DTLS camera a library-muxed MPEG-TS on a loopback listener (`CameraMixin.hls_ts_url()`), for Home Assistant's HLS view and recordings to read instead of go2rtc's RTSP. go2rtc re-bases each track for each consumer, so a reader that joins a running stream had its sound 0.1-0.75 s late, differently each time; this TS carries the publisher's video and AAC on one clock, so every reader gets them in the same step, whenever it joins. Needs `AIDOT_DIRECT_PUBLISH` and `AIDOT_PUBLISH_AAC`. An SDES camera gets a URL only on a model whose media the bridge decrypts (A001064, A001513) with its offer pinned to H.264 (`AIDOT_SDES_VIDEO_PT=96`), so that every session feeds it; pulled (non-publishing) sessions get none. When a camera's SPS changes (the A001064 changes it between sessions), the TS's readers are disconnected so they rebuild their decoder. Each camera's URL carries a secret made per listener, as `?auth=<secret>` (Home Assistant masks that query parameter when it logs a stream's URL); a request without it gets 404. For the owner's own tools, the listener's URL for any camera (`http://127.0.0.1:<port>/{name}.ts?auth=<secret>`, where `{name}` is `aidot_<first 12 of the device id>`) is left in `hls-ts-url` in the library's state directory (`AIDOT_SPROP_DIR`), owner-readable only, while the listener runs. A reader of the TS counts as a viewer. Truthy (`1`/`true`/`yes`/`on`) enables. | `1` (on) |
573
- | `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone (it is retried again the next time something asks for it). Applies to every device, camera or not - the devices that hit this are lights. | `6` |
578
+ | `AIDOT_LOGIN_RETRY_LIMIT` | Consecutive failed LAN logins after which a device is left alone: discovery and the library's own retry loop stop kicking it, and when something else still asks for a login (Home Assistant's light poll does, every five minutes) the attempt is made quietly - upstream's per-attempt warning is dropped - until the device's address changes or a login gets through. One warning marks the moment. Applies to every device, camera or not - the devices that hit this are lights on another subnet. | `6` |
574
579
 
575
580
  ### Security hardening
576
581
 
@@ -583,3 +588,4 @@ the permissive default.
583
588
  | --- | --- | --- |
584
589
  | `AIDOT_ALLOW_LAN_SERVE` | Silences the warning emitted when decrypted media is served on a non-loopback bind (e.g. `0.0.0.0`), where any host on the LAN can read the unencrypted stream. Set when an exposed bind is intentional. | unset (warn on non-loopback) |
585
590
  | `AIDOT_CRED_KEY_FILE` | Path to the Fernet key file for stored credentials. Point it outside the config dir (ideally a separate secret store) so the key isn't co-located with the ciphertext. Applies to the default credentials path only (ignored when an explicit `creds_path` is passed). | `$XDG_CONFIG_HOME/aidot/.key` (falls back to `~/.config/aidot/.key`) |
591
+ | `AIDOT_PLAYBACK_TLS_VERIFY` | Set to `1` to require full certificate and hostname verification on the TLS connection to a camera's playback server. The camera presents a self-signed certificate, so this needs a trust anchor it chains to; unset, verification is skipped and a one-time warning is logged. | unset (skip verification, warn once) |
@@ -97,6 +97,8 @@ from .protocol import ( # noqa: F401 - used here and/or by the webrtc_open mixi
97
97
  _grab_free_port,
98
98
  _tcp_table_has_established_on_port,
99
99
  _idle_release_due,
100
+ _battery_unknown_release_s,
101
+ _stream_startup_grace_s,
100
102
  _DirectTsServer,
101
103
  _serve_port,
102
104
  _dtls_av_mux_run,
@@ -4549,8 +4551,10 @@ class CameraMixin(
4549
4551
  is (the serve port is go2rtc's shared RTSP port, where every camera's own
4550
4552
  publisher is connected, so a socket check would report a viewer for every
4551
4553
  camera forever). With no signal ``_viewer_present`` answers "unknown",
4552
- ``_idle_release_due`` correctly never releases on unknown, and a battery
4553
- camera's keepalive renews forever against a camera nobody is watching.
4554
+ and ``_idle_release_due`` never releases a mains camera on unknown; a
4555
+ battery camera is released only after
4556
+ ``AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S`` (300 s) without a known
4557
+ viewer, so every view of it ends at that cap.
4554
4558
 
4555
4559
  ``stream_idle_s`` sets the no-viewer idle-release window in seconds,
4556
4560
  overriding the ``AIDOT_STREAM_IDLE_S`` env default (120). ``0`` (or
@@ -4619,10 +4623,10 @@ class CameraMixin(
4619
4623
  # mid-flight, which was observed leaving DTLS cameras producing nothing
4620
4624
  # at all. Such a consumer previously had to withhold go2rtc_url
4621
4625
  # entirely, which silently disabled viewer detection - and in SDES push
4622
- # mode that means a battery camera can never idle-release, because
4623
- # ``_viewer_present`` answers "unknown" and ``_idle_release_due``
4624
- # (correctly) never releases on unknown. The keepalive then renews
4625
- # forever against a camera nobody is watching.
4626
+ # mode that means ``_viewer_present`` answers "unknown", on which
4627
+ # ``_idle_release_due`` never releases a mains camera and releases a
4628
+ # battery camera only after AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S
4629
+ # without a known viewer - cutting every view of it short at that cap.
4626
4630
  if go2rtc_url and go2rtc_register:
4627
4631
  self._go2rtc_task = asyncio.ensure_future(self._register_with_go2rtc())
4628
4632
 
@@ -4963,7 +4967,8 @@ class CameraMixin(
4963
4967
  against a 5 minute idle window.
4964
4968
 
4965
4969
  Falls back to the TCP-table check when go2rtc is not in use, and returns
4966
- None (never release) if neither can answer.
4970
+ None (unknown: never releases a mains camera; a battery camera after
4971
+ AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S) if neither can answer.
4967
4972
  """
4968
4973
  # A consumer of the camera's in-sync HLS TS is a viewer that go2rtc
4969
4974
  # never sees (it reads from the library, not from go2rtc). Local and
@@ -5002,7 +5007,8 @@ class CameraMixin(
5002
5007
  # push mode, which is why a consumer that registers its own stream
5003
5008
  # should still pass go2rtc_url with go2rtc_register=False rather than
5004
5009
  # withholding it (withholding it silently pins push-mode cameras to
5005
- # "unknown", and unknown never releases).
5010
+ # "unknown", and unknown never releases a mains camera and cuts a
5011
+ # battery camera's views short at AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S).
5006
5012
  if base and name:
5007
5013
  try:
5008
5014
  import aiohttp
@@ -5262,7 +5268,9 @@ class CameraMixin(
5262
5268
  # pipe to watch, so detect "nobody is pulling" via an ESTABLISHED TCP
5263
5269
  # connection on the -listen serve port and release after the same idle
5264
5270
  # window as DTLS; the next view re-runs camera.stream_source(). Fail
5265
- # safe: unknown (non-Linux /proc) never releases. Escape hatch:
5271
+ # safe: unknown (non-Linux /proc) never releases a mains camera; a
5272
+ # battery camera releases after AIDOT_BATTERY_UNKNOWN_VIEWER_RELEASE_S
5273
+ # without a known viewer. Escape hatch:
5266
5274
  # AIDOT_SDES_IDLE_RELEASE=0.
5267
5275
  # stream_idle_s / AIDOT_STREAM_IDLE_S override; <= 0 = never release.
5268
5276
  _idle_secs = self._resolve_idle_secs()
@@ -5271,6 +5279,17 @@ class CameraMixin(
5271
5279
  )
5272
5280
  _serve_port = _sdes_serve_port(self._keepalive_rtsp_url)
5273
5281
  _last_consumer = _started_at # grace: count idle from session open
5282
+ # Battery exception to "unknown never releases": a battery camera
5283
+ # whose viewers cannot be counted is released this long after the
5284
+ # last KNOWN viewer, so it cannot stream (and drain) until restart.
5285
+ _battery = bool(getattr(self, "is_battery_camera", False))
5286
+ _unknown_cap = _battery_unknown_release_s()
5287
+ # Start-up grace: until a viewer has been seen, the idle window is
5288
+ # at least AIDOT_STREAM_STARTUP_GRACE_S from the open, so a short
5289
+ # window cannot end a view whose first viewer is still connecting
5290
+ # (a stream worker or recording that retries late on a slow wake).
5291
+ _viewer_seen = False
5292
+ _startup_grace = _stream_startup_grace_s()
5274
5293
  try:
5275
5294
  while True:
5276
5295
  _fin, _ = await asyncio.wait({_done}, timeout=5.0)
@@ -5309,12 +5328,28 @@ class CameraMixin(
5309
5328
  _present = await self._viewer_present(_serve_port)
5310
5329
  if _present: # True -> a viewer is pulling; stay alive
5311
5330
  _last_consumer = time.monotonic()
5331
+ _viewer_seen = True
5312
5332
  elif _idle_release_due(
5313
- _present, _last_consumer, time.monotonic(), _idle_secs
5333
+ _present,
5334
+ _last_consumer,
5335
+ time.monotonic(),
5336
+ _idle_secs,
5337
+ battery=_battery,
5338
+ unknown_cap_s=_unknown_cap,
5339
+ viewer_seen=_viewer_seen,
5340
+ startup_grace_s=_startup_grace,
5314
5341
  ):
5342
+ if _present is None:
5343
+ _LOGGER.info(
5344
+ "SDES serve: %s released - viewer state "
5345
+ "unknown for %.0f s on a battery camera",
5346
+ self.device_id,
5347
+ time.monotonic() - _last_consumer,
5348
+ )
5315
5349
  _idle_release = True
5316
5350
  break
5317
- # _present is None (unreadable table) -> don't release
5351
+ # _present is None (unknown) -> a mains camera never
5352
+ # releases; a battery camera releases after the cap.
5318
5353
  except asyncio.CancelledError:
5319
5354
  _done.cancel()
5320
5355
  self._stream_session = None
@@ -6010,8 +6045,8 @@ class CameraMixin(
6010
6045
  if getattr(self, "is_battery_camera", False):
6011
6046
  return False
6012
6047
  # The connection mode composes here rather than in a parallel path, so
6013
- # everything hanging off this resolver (instrumentation, the adaptive
6014
- # interplay) sees one answer. relay force-keeps the pre-allocation -
6048
+ # everything hanging off this resolver (instrumentation included) sees
6049
+ # one answer. relay force-keeps the pre-allocation -
6015
6050
  # forcing the relay while skipping its allocation would offer nothing.
6016
6051
  return self._resolve_sdes_connection_mode() == "lan"
6017
6052
 
@@ -6437,6 +6472,13 @@ class CameraMixin(
6437
6472
  proc = wfile = stop_flag = mux_thread = None
6438
6473
  cancelled = idle_release = False
6439
6474
  _pub_fail_n = 0
6475
+ # Start-up grace (see the SDES loop): before the first viewer of
6476
+ # this session, every release comparison in the serve cycle uses at
6477
+ # least AIDOT_STREAM_STARTUP_GRACE_S instead of the idle window.
6478
+ # Once per PC session, not per serve cycle: a cycle restarted after
6479
+ # a viewer left must not get the full grace again.
6480
+ _viewer_seen_dtls = False
6481
+ _grace_dtls = _stream_startup_grace_s()
6440
6482
  try:
6441
6483
  # (Re)start the ffmpeg serve whenever go2rtc (re)connects, while
6442
6484
  # the warm WebRTC session keeps delivering encoded frames.
@@ -6459,7 +6501,8 @@ class CameraMixin(
6459
6501
  else:
6460
6502
  _ff_port = None
6461
6503
  _ff_url = serve_url
6462
- # Opt-in: serve the muxed TS straight to the consumer.
6504
+ # Serve the muxed TS straight to the consumer on any http://
6505
+ # serve URL that direct publish does not take.
6463
6506
  # The `-c copy` ffmpeg hop re-packetizes MPEG-TS this mux
6464
6507
  # already wrote in the form go2rtc wants, and measured
6465
6508
  # 2026-08-20 it is the one component that LOSES timestamps
@@ -6579,6 +6622,8 @@ class CameraMixin(
6579
6622
  _stall_pli_armed = True
6580
6623
  _last_viewer_dtls = loop.time()
6581
6624
  _serve_port_dtls = _sdes_serve_port(serve_url) or 0
6625
+ _battery_dtls = bool(getattr(self, "is_battery_camera", False))
6626
+ _unknown_cap_dtls = _battery_unknown_release_s()
6582
6627
  # Video-presence check. The only other liveness test here is
6583
6628
  # _pc_dead(), which reads the ICE/PC state - and a session
6584
6629
  # receiving audio and no video passes it forever (measured
@@ -6660,15 +6705,42 @@ class CameraMixin(
6660
6705
  # cannot answer.
6661
6706
  if idle_secs > 0:
6662
6707
  _watching = await self._viewer_present(_serve_port_dtls)
6708
+ _window_dtls = (
6709
+ idle_secs
6710
+ if _viewer_seen_dtls
6711
+ else max(idle_secs, _grace_dtls)
6712
+ )
6663
6713
  if _watching:
6664
6714
  _last_viewer_dtls = _now
6715
+ _viewer_seen_dtls = True
6665
6716
  elif _watching is False:
6666
- if _now - _last_viewer_dtls > idle_secs:
6717
+ if _now - _last_viewer_dtls > _window_dtls:
6667
6718
  idle_release = True
6668
- elif _now - progress[0] > idle_secs:
6719
+ elif _now - progress[0] > _window_dtls:
6669
6720
  # Unknown: keep the old staleness heuristic rather
6670
6721
  # than holding the stream open forever.
6671
6722
  idle_release = True
6723
+ elif _battery_dtls and _idle_release_due(
6724
+ None,
6725
+ _last_viewer_dtls,
6726
+ _now,
6727
+ idle_secs,
6728
+ battery=True,
6729
+ unknown_cap_s=_unknown_cap_dtls,
6730
+ viewer_seen=_viewer_seen_dtls,
6731
+ startup_grace_s=_grace_dtls,
6732
+ ):
6733
+ # Unknown on a battery camera, and go2rtc keeps
6734
+ # draining the pipe so staleness never fires:
6735
+ # release after the cap since the last known
6736
+ # viewer rather than streaming until restart.
6737
+ _LOGGER.info(
6738
+ "DTLS serve: %s released - viewer state "
6739
+ "unknown for %.0f s on a battery camera",
6740
+ self.device_id,
6741
+ _now - _last_viewer_dtls,
6742
+ )
6743
+ idle_release = True
6672
6744
  # Only an actual idle release ends this serve cycle.
6673
6745
  # (This break was unconditional - any idle_secs > 0
6674
6746
  # tore down a HEALTHY ffmpeg after one 0.5s tick,