python-aidot-cameras 1.0.0b28__tar.gz → 1.0.0b30__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 (265) hide show
  1. {python_aidot_cameras-1.0.0b28/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b30}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/__main__.py +10 -4
  3. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/protocol.py +7 -7
  4. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sdes_open.py +108 -78
  5. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/cloud_auth.py +45 -8
  6. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/pyproject.toml +1 -1
  7. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  8. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offer_bandwidth_line.py +5 -2
  9. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_remb_goes_out_the_relay_aware_sender.py +18 -9
  10. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_tmmbr_bridge.py +16 -7
  11. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_self_peer_is_an_address_not_an_ip.py +1 -1
  12. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_validator_shares_one_token.py +59 -0
  13. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/LICENSE +0 -0
  14. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/MANIFEST.in +0 -0
  15. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/README.md +0 -0
  16. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_upstream.py +0 -0
  18. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  20. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  22. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  23. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  24. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  25. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  26. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  27. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  28. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  29. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  30. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  31. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  32. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  33. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  34. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  35. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  36. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  37. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  38. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  39. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  40. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  41. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  42. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  43. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  44. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  45. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  46. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  47. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  48. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  49. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  50. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  51. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  52. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/ble_gateway.py +0 -0
  53. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/__init__.py +0 -0
  54. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/client.py +0 -0
  55. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/constants.py +0 -0
  56. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/controls.py +0 -0
  57. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/go2rtc.py +0 -0
  58. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/hwaccel.py +0 -0
  59. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/lan_control.py +0 -0
  60. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/models.py +0 -0
  61. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/playback.py +0 -0
  62. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_events.py +0 -0
  63. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_listing.py +0 -0
  64. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sd_playback.py +0 -0
  65. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/sdes.py +0 -0
  66. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/tutk.py +0 -0
  67. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/webrtc.py +0 -0
  68. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/camera/webrtc_open.py +0 -0
  69. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/client.py +0 -0
  70. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/const.py +0 -0
  71. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/credentials.py +0 -0
  72. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/crypto.py +0 -0
  73. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/device_client.py +0 -0
  74. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/discover.py +0 -0
  75. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/exceptions.py +0 -0
  76. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/g711.py +0 -0
  77. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/lan_retry.py +0 -0
  78. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/aidot_cameras/py.typed +0 -0
  79. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  80. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  81. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  82. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/requires.txt +0 -0
  83. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  84. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/setup.cfg +0 -0
  85. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_aioice_compat.py +0 -0
  86. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_alarm_event.py +0 -0
  87. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_answer_sdp_video_pt.py +0 -0
  88. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_answer_section_selection.py +0 -0
  89. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_audio_mux_offline.py +0 -0
  90. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  91. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_request.py +0 -0
  92. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_response_parse.py +0 -0
  93. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_avio_response_router.py +0 -0
  94. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_backoff.py +0 -0
  95. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_detection.py +0 -0
  96. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_evidence_corroboration.py +0 -0
  97. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_battery_skip_turn.py +0 -0
  98. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_bitrate_campaign_arms.py +0 -0
  99. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ble_gateway.py +0 -0
  100. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_bridge_state_sentinels.py +0 -0
  101. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_busy_backoff.py +0 -0
  102. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_canary_log_names_the_camera.py +0 -0
  103. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_cloud_count.py +0 -0
  104. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_cloud_plan.py +0 -0
  105. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_constants_imports_resolve.py +0 -0
  106. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_control_verdicts.py +0 -0
  107. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_deferred_hardening.py +0 -0
  108. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_cmd_seq_match.py +0 -0
  109. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_login_guard.py +0 -0
  110. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_device_user_info_cache.py +0 -0
  111. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_direct_ts_serve.py +0 -0
  112. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dormant_camera_cleanup.py +0 -0
  113. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_drain_is_cancellable.py +0 -0
  114. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_mux_terminal_write.py +0 -0
  115. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_not_ready_burst.py +0 -0
  116. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_pinning.py +0 -0
  117. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_resolution_ack.py +0 -0
  118. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_decode_skip.py +0 -0
  119. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_ice_wait.py +0 -0
  120. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_serve_open_timeout.py +0 -0
  121. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_skip_signaling_wait.py +0 -0
  122. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_slow_probe.py +0 -0
  123. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_stdout_producer.py +0 -0
  124. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_dtls_video_presence.py +0 -0
  125. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_egress_guard.py +0 -0
  126. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_env_seams.py +0 -0
  127. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  128. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  129. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_futile_keepalive.py +0 -0
  130. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  131. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_go2rtc.py +0 -0
  132. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_go2rtc_cli.py +0 -0
  133. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_h264_decode_rate_limit.py +0 -0
  134. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_highport_nomination.py +0 -0
  135. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_hwaccel_probe.py +0 -0
  136. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ice_config_cache.py +0 -0
  137. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ice_peer_reflexive.py +0 -0
  138. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_idle_release_both_loops.py +0 -0
  139. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_keepalive_renew.py +0 -0
  140. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_key_restart_sdp.py +0 -0
  141. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_keyframe_prompter.py +0 -0
  142. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_lan_control.py +0 -0
  143. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  144. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_late_repeats_are_dropped.py +0 -0
  145. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_light_active_color_mode.py +0 -0
  146. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_session_carries_talk.py +0 -0
  147. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_stream_param.py +0 -0
  148. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_validate_policy.py +0 -0
  149. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_live_video_canary.py +0 -0
  150. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_local_only_controls.py +0 -0
  151. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_logging_caps.py +0 -0
  152. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_login_connect_deadline.py +0 -0
  153. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_login_reconnect_backoff.py +0 -0
  154. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_counted_only_when_plaintext.py +0 -0
  155. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_socket_rcvbuf.py +0 -0
  156. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_media_wait_floor.py +0 -0
  157. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_motion_poll.py +0 -0
  158. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_client_id_is_exact.py +0 -0
  159. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_credential_refetch.py +0 -0
  160. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_credential_self_heal.py +0 -0
  161. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_password_not_persisted.py +0 -0
  162. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_session_reconnect.py +0 -0
  163. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  164. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_nack.py +0 -0
  165. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  166. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_narrow_pc_ice.py +0 -0
  167. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_key_material_in_logs.py +0 -0
  168. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_self_referential_go2rtc.py +0 -0
  169. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_no_undefined_names.py +0 -0
  170. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  171. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_offline_keepalive_pause.py +0 -0
  172. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_on_frame_is_an_av_frame.py +0 -0
  173. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_open_fail_logger.py +0 -0
  174. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_open_gate_delay.py +0 -0
  175. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_peerid_reuse.py +0 -0
  176. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_persistent_mqtt.py +0 -0
  177. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_persistent_mqtt_robustness.py +0 -0
  178. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_playback_tls.py +0 -0
  179. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_post_merge_hardening.py +0 -0
  180. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  181. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_public_signature_matches_impl.py +0 -0
  182. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_quality_campaign_in_session.py +0 -0
  183. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_recent_recordings.py +0 -0
  184. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_remb.py +0 -0
  185. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_resent_frames_are_dropped.py +0 -0
  186. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_resolution_persists.py +0 -0
  187. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_restart_cycle.py +0 -0
  188. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_retry_policy.py +0 -0
  189. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_scripts_importable.py +0 -0
  190. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sctp_init_ack_tsn.py +0 -0
  191. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_card_presence.py +0 -0
  192. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_event_decode.py +0 -0
  193. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_event_probe_payloads.py +0 -0
  194. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_listing.py +0 -0
  195. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sd_playback.py +0 -0
  196. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_adaptive.py +0 -0
  197. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_avio_dispatch.py +0 -0
  198. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_bridge_break_gate.py +0 -0
  199. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_echo_wait_timeout.py +0 -0
  200. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_fast_liveplay.py +0 -0
  201. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  202. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_first_media_stall_report.py +0 -0
  203. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_idle_release.py +0 -0
  204. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_nack_bridge.py +0 -0
  205. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_offer_video_order.py +0 -0
  206. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_offer_video_pin.py +0 -0
  207. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_open_cleanup.py +0 -0
  208. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_sctp_abort.py +0 -0
  209. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_audio.py +0 -0
  210. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_cmd.py +0 -0
  211. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_serve_stderr_drain.py +0 -0
  212. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_sprop.py +0 -0
  213. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_talk.py +0 -0
  214. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_teardown_exit_log.py +0 -0
  215. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_terminal_ack.py +0 -0
  216. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_terminal_ack_latency.py +0 -0
  217. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdes_watchdog.py +0 -0
  218. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdp_narrowing_properties.py +0 -0
  219. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sdp_payload_type_narrowing.py +0 -0
  220. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_security_hardening.py +0 -0
  221. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serializable_login_info.py +0 -0
  222. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_loss_tolerance.py +0 -0
  223. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_pt_respects_the_pin.py +0 -0
  224. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_relay.py +0 -0
  225. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  226. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_session_stats.py +0 -0
  227. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_shared_house_seam.py +0 -0
  228. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_snapshot_cancellation.py +0 -0
  229. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_speak.py +0 -0
  230. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_capture_gate.py +0 -0
  231. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_staleness.py +0 -0
  232. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_unstable_public.py +0 -0
  233. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  234. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_srtp_rx_rekey.py +0 -0
  235. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_srtp_tx_key_note.py +0 -0
  236. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  237. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  238. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  239. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  240. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  241. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_cap.py +0 -0
  242. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_drain_ownership.py +0 -0
  243. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_idle.py +0 -0
  244. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_slot_cap.py +0 -0
  245. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_stream_teardown.py +0 -0
  246. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_talk.py +0 -0
  247. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  248. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tap_to_mux_end_to_end.py +0 -0
  249. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_terminal_ack.py +0 -0
  250. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tmmbr.py +0 -0
  251. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_token_file_robustness.py +0 -0
  252. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_token_refresh.py +0 -0
  253. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_turn_create_permission.py +0 -0
  254. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  255. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_upstream_compat.py +0 -0
  256. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_profile_log.py +0 -0
  257. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_pts_dts.py +0 -0
  258. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_video_ts_unwrap.py +0 -0
  259. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_check_throttle.py +0 -0
  260. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_presence.py +0 -0
  261. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_viewer_present_push_mode.py +0 -0
  262. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wait_or_event.py +0 -0
  263. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wake_gate_camera_evidence.py +0 -0
  264. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/tests/test_wake_readiness_retry.py +0 -0
  265. {python_aidot_cameras-1.0.0b28 → python_aidot_cameras-1.0.0b30}/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.0b28
3
+ Version: 1.0.0b30
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
@@ -77,6 +77,7 @@ import aiohttp
77
77
 
78
78
  from .client import AidotClient
79
79
  from .cloud_auth import ( # noqa: F401 (re-exported for callers and tests)
80
+ CloudAuthUnavailable,
80
81
  _install_token_cache,
81
82
  _make_client,
82
83
  _read_token_file,
@@ -86,8 +87,6 @@ from .const import CONF_DEVICE_LIST, CONF_ID, CONF_MODEL_ID, CONF_NAME
86
87
 
87
88
  _LOGGER = logging.getLogger("aidot.go2rtc")
88
89
 
89
- DEFAULT_COUNTRY = os.environ.get("AIDOT_COUNTRY", "US")
90
-
91
90
 
92
91
  def _env_bool(name: str) -> bool | None:
93
92
  """Tri-state env flag: '1' -> True, '0' -> False, unset -> None (library default)."""
@@ -131,7 +130,11 @@ def _go2rtc_source(dev_id: str, is_sdes: bool) -> str:
131
130
  async def cmd_list() -> int:
132
131
  """Print device id + transport for every camera, plus paste-ready go2rtc YAML."""
133
132
  async with aiohttp.ClientSession() as session:
134
- client = await _make_client(session)
133
+ # CloudAuthUnavailable is the library-side signal; a CLI exits on it.
134
+ try:
135
+ client = await _make_client(session)
136
+ except CloudAuthUnavailable as exc:
137
+ sys.exit(str(exc))
135
138
  cams = await _cameras(client)
136
139
  if not cams:
137
140
  print("No IPC cameras found on this account.")
@@ -158,7 +161,10 @@ async def cmd_list() -> int:
158
161
  async def cmd_stream(dev_id: str, output_url: str) -> int:
159
162
  """Open one camera's keepalive and push/serve it to ``output_url``."""
160
163
  async with aiohttp.ClientSession() as session:
161
- client = await _make_client(session)
164
+ try:
165
+ client = await _make_client(session)
166
+ except CloudAuthUnavailable as exc:
167
+ sys.exit(str(exc))
162
168
  cams = await _cameras(client)
163
169
  device = next((d for d in cams if d[CONF_ID] == dev_id), None)
164
170
  if device is None:
@@ -2950,6 +2950,12 @@ def decode_remb_bitrate(pkt: bytes) -> int:
2950
2950
  return mantissa << exp
2951
2951
 
2952
2952
 
2953
+ #: RTPFB feedback message types (RFC 4585 s6.1, RFC 5104 s4.2).
2954
+ _RTPFB_PT = 205
2955
+ _FMT_NACK = 1
2956
+ _FMT_TMMBR = 3
2957
+
2958
+
2953
2959
  def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
2954
2960
  """An RTCP Generic NACK (RFC 4585 s6.2.1) asking for ``lost_seqs`` again.
2955
2961
 
@@ -2985,7 +2991,7 @@ def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
2985
2991
  i += 1
2986
2992
  fci.append(struct.pack("!HH", pid, blp))
2987
2993
 
2988
- pkt = (struct.pack("!BBH", 0x80 | 1, 205, 0)
2994
+ pkt = (struct.pack("!BBH", 0x80 | _FMT_NACK, _RTPFB_PT, 0)
2989
2995
  + struct.pack("!II", sender_ssrc & 0xFFFFFFFF,
2990
2996
  media_ssrc & 0xFFFFFFFF)
2991
2997
  + b"".join(fci))
@@ -2993,12 +2999,6 @@ def build_nack(sender_ssrc: int, media_ssrc: int, lost_seqs: "list") -> bytes:
2993
2999
  return pkt[:2] + struct.pack("!H", len(pkt) // 4 - 1) + pkt[4:]
2994
3000
 
2995
3001
 
2996
- #: RTPFB feedback message types (RFC 4585 s6.1, RFC 5104 s4.2).
2997
- _RTPFB_PT = 205
2998
- _FMT_NACK = 1
2999
- _FMT_TMMBR = 3
3000
-
3001
-
3002
3002
  def build_tmmbr(sender_ssrc: int, media_ssrc: int, bitrate_bps: int,
3003
3003
  overhead: int = 60) -> bytes:
3004
3004
  """A TMMBR (RFC 5104 s4.2.1) telling ``media_ssrc`` not to exceed a rate.
@@ -241,19 +241,28 @@ def _widen_media_rcvbuf(sock, kind: str, device_id: str = "?") -> int:
241
241
  return got
242
242
 
243
243
 
244
- def _sdes_offer_bandwidth_kbps():
245
- """A receive-bandwidth ceiling for the offer, in kbps, or None for none.
246
-
247
- Off unless ``AIDOT_SDES_OFFER_BANDWIDTH_KBPS`` names a positive integer.
248
- An unparseable value is off rather than an error: this is read while an
249
- offer is being built, and a typo must not take a camera off the air.
244
+ def _env_positive(name: str, cast=int, off=None):
245
+ """Parse ``name`` as a positive number; anything else means ``off``.
246
+
247
+ One body for the experiment knobs below - the third copy of this parser
248
+ was the rule-of-three moment. ``off`` is the knob's disabled value (None
249
+ for the ints, 0.0 for the delay), and unparseable input is disabled rather
250
+ than an error: these are read while sessions are being built, and a typo
251
+ must not take a camera off the air.
250
252
  """
251
- raw = os.environ.get("AIDOT_SDES_OFFER_BANDWIDTH_KBPS")
253
+ raw = os.environ.get(name)
254
+ if raw is None:
255
+ return off
252
256
  try:
253
- kbps = int(str(raw).strip())
254
- except (TypeError, ValueError, AttributeError):
255
- return None
256
- return kbps if kbps > 0 else None
257
+ value = cast(raw.strip())
258
+ except ValueError:
259
+ return off
260
+ return value if value > 0 else off
261
+
262
+
263
+ def _sdes_offer_bandwidth_kbps():
264
+ """A receive-bandwidth ceiling for the offer, in kbps, or None for none."""
265
+ return _env_positive("AIDOT_SDES_OFFER_BANDWIDTH_KBPS")
257
266
 
258
267
 
259
268
  def _offer_bandwidth_line(kbps) -> str:
@@ -381,13 +390,8 @@ def _send_video_nack(send, srtcp_sess, sender_ssrc: int,
381
390
  """
382
391
  if not lost_seqs:
383
392
  return False
384
- try:
385
- raw = build_nack(sender_ssrc, media_ssrc, lost_seqs)
386
- send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
387
- return True
388
- except Exception:
389
- _LOGGER.debug("NACK send failed", exc_info=True)
390
- return False
393
+ return _send_rtcp_fb(
394
+ send, srtcp_sess, build_nack(sender_ssrc, media_ssrc, lost_seqs))
391
395
 
392
396
 
393
397
  def _sdes_tmmbr_bps():
@@ -396,11 +400,7 @@ def _sdes_tmmbr_bps():
396
400
  Off unless ``AIDOT_SDES_TMMBR_BPS`` names a positive integer. Unparseable
397
401
  is off rather than an error: this is read while a session is running.
398
402
  """
399
- try:
400
- bps = int(str(os.environ.get("AIDOT_SDES_TMMBR_BPS")).strip())
401
- except (TypeError, ValueError, AttributeError):
402
- return None
403
- return bps if bps > 0 else None
403
+ return _env_positive("AIDOT_SDES_TMMBR_BPS")
404
404
 
405
405
 
406
406
  def _sdes_tmmbr_after_s() -> float:
@@ -413,11 +413,7 @@ def _sdes_tmmbr_after_s() -> float:
413
413
  working SD control. Both windows of one session share the codec and the
414
414
  scene, so neither can confound it.
415
415
  """
416
- try:
417
- after = float(str(os.environ.get("AIDOT_SDES_TMMBR_AFTER_S")).strip())
418
- except (TypeError, ValueError, AttributeError):
419
- return 0.0
420
- return after if after > 0 else 0.0
416
+ return _env_positive("AIDOT_SDES_TMMBR_AFTER_S", cast=float, off=0.0)
421
417
 
422
418
 
423
419
  def _tmmbr_ready(first_video_ts, now: float, after_s: float) -> bool:
@@ -431,6 +427,38 @@ def _tmmbr_ready(first_video_ts, now: float, after_s: float) -> bool:
431
427
  return (now - first_video_ts) >= after_s
432
428
 
433
429
 
430
+ def _send_rtcp_fb(send, srtcp_sess, raw: bytes) -> bool:
431
+ """Protect-and-send tail shared by the NACK, TMMBR and REMB helpers.
432
+
433
+ One body for the ``protect_rtcp``-if-session-else-raw dance and the
434
+ swallow-and-debug-log error policy. It was written three times before
435
+ this, and the copy that drifted (REMB's, still inline in the loop body)
436
+ is the one where the raw-socket relay bug hid. Never raises: these run
437
+ inside the bridge's packet loop.
438
+ """
439
+ try:
440
+ send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
441
+ return True
442
+ except Exception:
443
+ _LOGGER.debug("RTCP feedback send failed", exc_info=True)
444
+ return False
445
+
446
+
447
+ def _send_video_remb(send, srtcp_sess, sender_ssrc: int,
448
+ media_ssrc: int, target_bps) -> bool:
449
+ """Put one REMB on the camera's RTCP path. True if it went out.
450
+
451
+ Same shape and same relay-aware ``send`` contract as
452
+ :func:`_send_video_nack` - the REMB logic previously lived inline in the
453
+ loop body, and being the only feedback path without a helper is exactly
454
+ how it kept the raw-socket write long after the NACK path was fixed.
455
+ """
456
+ if not target_bps or target_bps <= 0:
457
+ return False
458
+ return _send_rtcp_fb(
459
+ send, srtcp_sess, build_remb(sender_ssrc, [media_ssrc], target_bps))
460
+
461
+
434
462
  def _send_video_tmmbr(send, srtcp_sess, sender_ssrc: int,
435
463
  media_ssrc: int, bitrate_bps) -> bool:
436
464
  """Put one TMMBR on the camera's RTCP path. True if it went out.
@@ -446,13 +474,8 @@ def _send_video_tmmbr(send, srtcp_sess, sender_ssrc: int,
446
474
  """
447
475
  if not bitrate_bps or bitrate_bps <= 0:
448
476
  return False
449
- try:
450
- raw = build_tmmbr(sender_ssrc, media_ssrc, bitrate_bps)
451
- send(srtcp_sess.protect_rtcp(raw) if srtcp_sess is not None else raw)
452
- return True
453
- except Exception:
454
- _LOGGER.debug("TMMBR send failed", exc_info=True)
455
- return False
477
+ return _send_rtcp_fb(
478
+ send, srtcp_sess, build_tmmbr(sender_ssrc, media_ssrc, bitrate_bps))
456
479
 
457
480
 
458
481
  #: Sender SSRC on every RTCP we send the camera. Load-bearing, not cosmetic:
@@ -2008,6 +2031,11 @@ class _SdesOpenMixin:
2008
2031
  # this replaced; AIDOT_SDES_VIDEO_PT_ORDER reorders it without ever
2009
2032
  # narrowing it.
2010
2033
  _video_pt_order = _resolve_sdes_video_pt_order()
2034
+ # Read ONCE and feed both the SDP line and the status receipt below:
2035
+ # two independent env reads let the receipt name a value that never
2036
+ # reached the wire, which is the false-receipt trap the receipt
2037
+ # exists to close.
2038
+ _bw_kbps = _sdes_offer_bandwidth_kbps()
2011
2039
  _video_pt_list, _video_codec_attrs = _sdes_offer_video_codec_lines(
2012
2040
  _video_pt_order)
2013
2041
  sdes_offer_sdp = (
@@ -2050,7 +2078,7 @@ class _SdesOpenMixin:
2050
2078
  # video m-section
2051
2079
  + f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
2052
2080
  f"c=IN IP4 {_offer_video_ip}\r\n"
2053
- + _offer_bandwidth_line(_sdes_offer_bandwidth_kbps())
2081
+ + _offer_bandwidth_line(_bw_kbps)
2054
2082
  + "a=recvonly\r\n"
2055
2083
  "a=mid:1\r\n"
2056
2084
  f"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:{srtp_key_video}\r\n"
@@ -2103,7 +2131,6 @@ class _SdesOpenMixin:
2103
2131
  # is not evidence the line reached the wire. Measured 2026-08-23: a
2104
2132
  # b=AS arm scored identically to its control and the only available
2105
2133
  # check for the knob was reading the env back out of the harness.
2106
- _bw_kbps = _sdes_offer_bandwidth_kbps()
2107
2134
  if _bw_kbps and _status:
2108
2135
  _status(f"SDES: offer receive-bandwidth ceiling b=AS:{_bw_kbps}")
2109
2136
 
@@ -4199,33 +4226,29 @@ class _SdesOpenMixin:
4199
4226
  # skipping a tick until the sender exists costs a second,
4200
4227
  # falling back costs correctness on the very sessions this
4201
4228
  # is for.
4202
- _remb_send = getattr(_bridge_fn, '_send_to_cam', None)
4229
+ # Guard order is cost order: the module constant first,
4230
+ # so the default REMB-off config pays nothing per packet
4231
+ # (the sender lookup used to sit above this line and ran
4232
+ # ~300x/s for a feature that ships disabled).
4203
4233
  if (REMB_TARGET_BPS > 0
4204
4234
  and hasattr(_bridge_fn, '_cam_video_ssrc')
4205
- and _remb_send is not None
4235
+ and (_remb_send := getattr(
4236
+ _bridge_fn, '_send_to_cam', None)) is not None
4206
4237
  and _time_br.time() - getattr(
4207
4238
  _bridge_fn, '_last_remb_ts', 0.0) >= 1.0):
4208
4239
  _bridge_fn._last_remb_ts = _time_br.time()
4209
- try:
4210
- _remb_raw = build_remb(
4240
+ if _send_video_remb(
4241
+ _remb_send,
4242
+ getattr(_bridge_fn, '_pli_tx_sess', None),
4211
4243
  _CAM_RTCP_SENDER_SSRC,
4212
- [_bridge_fn._cam_video_ssrc],
4213
- REMB_TARGET_BPS)
4214
- _remb_sess = getattr(_bridge_fn, '_pli_tx_sess', None)
4215
- _remb_send(
4216
- _remb_sess.protect_rtcp(_remb_raw)
4217
- if _remb_sess is not None else _remb_raw)
4218
- if not getattr(_bridge_fn, '_remb_logged', False):
4219
- _bridge_fn._remb_logged = True
4220
- _status(
4221
- f"SDES: sent REMB {REMB_TARGET_BPS // 1000} kbps"
4222
- f" for video SSRC"
4223
- f" 0x{_bridge_fn._cam_video_ssrc:08x}")
4224
- except Exception:
4225
- _LOGGER.debug(
4226
- "camera %s: swallowed exception in %s",
4227
- getattr(self, "device_id", "?"), '_remb',
4228
- exc_info=True)
4244
+ _bridge_fn._cam_video_ssrc,
4245
+ REMB_TARGET_BPS,
4246
+ ) and not getattr(_bridge_fn, '_remb_logged', False):
4247
+ _bridge_fn._remb_logged = True
4248
+ _status(
4249
+ f"SDES: sent REMB {REMB_TARGET_BPS // 1000} kbps"
4250
+ f" for video SSRC"
4251
+ f" 0x{_bridge_fn._cam_video_ssrc:08x}")
4229
4252
 
4230
4253
  # TMMBR: a BOUND, where REMB above is an ESTIMATE. Two
4231
4254
  # different RFC 5104 / 4585 messages, and this firmware can
@@ -4236,17 +4259,23 @@ class _SdesOpenMixin:
4236
4259
  # every bitrate control tried on this camera so far has
4237
4260
  # been acked and ignored, and an unmeasured one must not
4238
4261
  # reach the four cameras that stream fine today.
4262
+ # Same cost ordering: the per-session bps attr is the
4263
+ # one cheap read the off-default pays; the cadence gate
4264
+ # runs before _tmmbr_ready so the readiness math and the
4265
+ # sender lookup happen at most once a second, not per
4266
+ # packet, and one time.time() serves both checks.
4239
4267
  _tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)
4240
- _tmmbr_send = getattr(_bridge_fn, '_send_to_cam', None)
4241
- if (_tmmbr_bps and _tmmbr_send is not None
4268
+ if (_tmmbr_bps
4242
4269
  and hasattr(_bridge_fn, '_cam_video_ssrc')
4270
+ and (_tmmbr_now := _time_br.time()) - getattr(
4271
+ _bridge_fn, '_last_tmmbr_ts', 0.0) >= 1.0
4243
4272
  and _tmmbr_ready(
4244
4273
  getattr(_bridge_fn, '_first_video_ts', None),
4245
- _time_br.time(),
4274
+ _tmmbr_now,
4246
4275
  getattr(_bridge_fn, '_tmmbr_after_s', 0.0))
4247
- and _time_br.time() - getattr(
4248
- _bridge_fn, '_last_tmmbr_ts', 0.0) >= 1.0):
4249
- _bridge_fn._last_tmmbr_ts = _time_br.time()
4276
+ and (_tmmbr_send := getattr(
4277
+ _bridge_fn, '_send_to_cam', None)) is not None):
4278
+ _bridge_fn._last_tmmbr_ts = _tmmbr_now
4250
4279
  if _send_video_tmmbr(
4251
4280
  _tmmbr_send,
4252
4281
  getattr(_bridge_fn, '_pli_tx_sess', None),
@@ -5477,23 +5506,24 @@ class _SdesOpenMixin:
5477
5506
  '!I', _bpkt, 8)[0]
5478
5507
  _bridge_fn._cam_srtp_src = _bsrc
5479
5508
  _bridge_fn._cam_srtp_sock = _bs
5480
- # Publish the relay-aware sender so the RTCP
5481
- # cadence (which runs outside this loop and has
5482
- # no socket/peer in scope) can reach the camera
5483
- # the same way the NACK does. Rebuilt only when
5484
- # the address changes: this runs ~300x/s per
5485
- # camera, which is why _nack_on is resolved once
5486
- # per session rather than read per packet.
5487
5509
  if getattr(_bridge_fn, '_first_video_ts',
5488
5510
  None) is None:
5489
5511
  _bridge_fn._first_video_ts = _time_br.time()
5490
- if getattr(_bridge_fn, '_send_to_cam_src',
5491
- None) != _bsrc:
5492
- _bridge_fn._send_to_cam_src = _bsrc
5493
- _bridge_fn._send_to_cam = (
5494
- lambda _d, _s=_bs, _a=_bsrc,
5495
- _p=_br_cam_peer: _br_send_to_cam(
5496
- _s, _d, _a, _p))
5512
+ # Publish the relay-aware sender so the RTCP
5513
+ # cadence blocks (which have no socket/peer in
5514
+ # scope) can reach the camera the way the NACK
5515
+ # does. This branch runs ONCE per session (it
5516
+ # sits inside the first-video gate), so the
5517
+ # sender is frozen at the first video packet's
5518
+ # source - the same adoption `_cam_srtp_src`
5519
+ # has always used. A 1 Hz cadence send to a
5520
+ # stale address after a mid-session address
5521
+ # change just drops; the NACK path keeps its
5522
+ # own per-event binding for exactly that case.
5523
+ _bridge_fn._send_to_cam = (
5524
+ lambda _d, _s=_bs, _a=_bsrc,
5525
+ _p=_br_cam_peer: _br_send_to_cam(
5526
+ _s, _d, _a, _p))
5497
5527
  # Schedule immediate PLI so IDR+SPS arrives in
5498
5528
  # the analyzeduration window.
5499
5529
  _bridge_fn._last_pli_ts = 0.0
@@ -18,7 +18,6 @@ import asyncio
18
18
  import json
19
19
  import logging
20
20
  import os
21
- import sys
22
21
  import tempfile
23
22
 
24
23
  import aiohttp
@@ -27,7 +26,25 @@ from .client import AidotClient
27
26
 
28
27
  _LOGGER = logging.getLogger("aidot.cloud_auth")
29
28
 
30
- DEFAULT_COUNTRY = os.environ.get("AIDOT_COUNTRY", "US")
29
+
30
+ def _default_country() -> str:
31
+ """The AIDOT_COUNTRY fallback, read at CALL time.
32
+
33
+ This used to be a module-level constant bound at import, which made a
34
+ caller's later os.environ tweak silently ineffective: a creds file with a
35
+ non-US country logged in as US on the password path. Reading it when the
36
+ client is built keeps environment and parameters equivalent.
37
+ """
38
+ return os.environ.get("AIDOT_COUNTRY", "US")
39
+
40
+
41
+ class CloudAuthUnavailable(RuntimeError):
42
+ """No usable credentials: no readable token file and no username/password.
43
+
44
+ Raised instead of ``sys.exit`` so library-side callers (the release
45
+ validator among them) keep their own error reporting; the go2rtc CLI
46
+ translates it to an exit in ``__main__``.
47
+ """
31
48
 
32
49
 
33
50
  def _read_token_file(path: str) -> dict:
@@ -85,19 +102,39 @@ def _install_token_cache(client: AidotClient, path: str) -> None:
85
102
  client.set_token_fresh_cb(_cb)
86
103
 
87
104
 
88
- async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
89
- """Authenticate to the AiDot cloud from the environment.
105
+ async def _make_client(
106
+ session: aiohttp.ClientSession,
107
+ *,
108
+ username: "str | None" = None,
109
+ password: "str | None" = None,
110
+ country: "str | None" = None,
111
+ ) -> AidotClient:
112
+ """Authenticate to the AiDot cloud, from parameters or the environment.
90
113
 
91
114
  Prefers a stored token (AIDOT_TOKEN_FILE) - the same login_info dict the HA
92
115
  integration persists - which carries access/refresh tokens so no password
93
116
  round-trip is needed. Falls back to username/password login otherwise. In
94
117
  both cases, if AIDOT_TOKEN_FILE is set we register a write-back cache so
95
118
  rotations persist.
119
+
120
+ Credentials a caller already holds are passed explicitly; each parameter
121
+ left as None falls back to its environment variable. The parameters exist
122
+ so a caller never has to publish secrets into ``os.environ`` (where every
123
+ spawned child inherits them) just to reach this function - the shim that
124
+ did exactly that also carried a dead country assignment, because the old
125
+ module-level default was bound at import time.
126
+
127
+ Raises :class:`CloudAuthUnavailable` when neither a readable token file
128
+ nor a username/password pair is available.
96
129
  """
97
130
  loop = asyncio.get_running_loop()
98
131
  token_file = os.environ.get("AIDOT_TOKEN_FILE")
99
- username = os.environ.get("AIDOT_USERNAME")
100
- password = os.environ.get("AIDOT_PASSWORD")
132
+ if username is None:
133
+ username = os.environ.get("AIDOT_USERNAME")
134
+ if password is None:
135
+ password = os.environ.get("AIDOT_PASSWORD")
136
+ if country is None:
137
+ country = _default_country()
101
138
 
102
139
  if token_file and os.path.exists(token_file):
103
140
  try:
@@ -113,14 +150,14 @@ async def _make_client(session: aiohttp.ClientSession) -> AidotClient:
113
150
  return client
114
151
 
115
152
  if not username or not password:
116
- sys.exit(
153
+ raise CloudAuthUnavailable(
117
154
  "Set AIDOT_TOKEN_FILE, or AIDOT_USERNAME and AIDOT_PASSWORD, in the "
118
155
  "environment."
119
156
  )
120
157
 
121
158
  client = AidotClient(
122
159
  session=session,
123
- country_code=DEFAULT_COUNTRY,
160
+ country_code=country,
124
161
  username=username,
125
162
  password=password,
126
163
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0b28"
7
+ version = "1.0.0b30"
8
8
  description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b28
3
+ Version: 1.0.0b30
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
@@ -132,8 +132,11 @@ def test_applying_a_ceiling_leaves_a_receipt_in_the_log():
132
132
 
133
133
  src = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
134
134
  / "camera" / "sdes_open.py").read_text()
135
- m = re.search(r"_bw_kbps\s*=\s*_sdes_offer_bandwidth_kbps\(\)\s*\n"
136
- r"\s*if _bw_kbps and _status:\s*\n"
135
+ m = re.search(r"if _bw_kbps and _status:\s*\n"
137
136
  r'\s*_status\(f"[^"]*b=AS:\{_bw_kbps\}"\)', src)
138
137
  assert m, ("applying a receive-bandwidth ceiling must emit a status "
139
138
  "receipt naming the value that reached the offer")
139
+ assert src.count("_sdes_offer_bandwidth_kbps()") == 2, (
140
+ "the knob must be read ONCE per offer (its definition plus one call "
141
+ "site); a second read lets the receipt disagree with the SDP actually "
142
+ "sent, which is the false-receipt trap the receipt exists to close")
@@ -50,12 +50,17 @@ _SRC = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
50
50
 
51
51
 
52
52
  def _remb_block() -> str:
53
- """The REMB send, from its guard to the end of its exception handler."""
53
+ """The REMB cadence block: its guard through to the TMMBR block's start.
54
+
55
+ Sliced anchor-to-structural-terminator rather than anchor+N characters -
56
+ a fixed-width window goes quietly vacuous when comments grow, and a
57
+ vacuous window makes the negative assertions below pass without guarding
58
+ anything.
59
+ """
54
60
  src = _SRC.read_text()
55
- # Anchor on the sender resolution, which sits just above the guard, so the
56
- # window covers how the sender is obtained as well as how it is used.
57
- start = src.index("_remb_send = getattr(")
58
- return src[start:start + 1400]
61
+ start = src.index("if (REMB_TARGET_BPS > 0")
62
+ end = src.index("_tmmbr_bps = getattr(", start)
63
+ return src[start:end]
59
64
 
60
65
 
61
66
  def test_remb_does_not_write_to_the_raw_socket():
@@ -77,15 +82,19 @@ def test_remb_does_not_fall_back_to_the_socket_when_the_sender_is_missing():
77
82
  second; falling back to `sendto` costs correctness on every relayed camera.
78
83
  """
79
84
  block = _remb_block()
80
- assert re.search(r"_remb_send\s*=\s*getattr\(\s*_bridge_fn,\s*'_send_to_cam',\s*None\s*\)",
81
- block), "the sender must be resolved with a None default"
82
- assert re.search(r"_remb_send is not None", block), (
83
- "the guard must require the relay-aware sender, not fall back")
85
+ assert re.search(
86
+ r"_remb_send\s*:=\s*getattr\(\s*\n?\s*_bridge_fn,\s*'_send_to_cam',"
87
+ r"\s*None\s*\)+\s*\)?\s*is not None", block), (
88
+ "the guard must resolve the relay-aware sender with a None default "
89
+ "and require it, not fall back to a socket")
84
90
 
85
91
 
86
92
  def test_remb_uses_the_keyed_sender_ssrc_constant():
87
93
  block = _remb_block()
88
94
  assert "_CAM_RTCP_SENDER_SSRC" in block
95
+ assert "_send_video_remb(" in block, (
96
+ "REMB must go through its helper like NACK and TMMBR do; the inline "
97
+ "copy is where the raw-socket bug hid")
89
98
  assert "0xAB12CD34" not in block, (
90
99
  "the SRTP TX policy is keyed on _CAM_RTCP_SENDER_SSRC; a literal here "
91
100
  "drifts and the camera drops the packet")
@@ -95,9 +95,12 @@ def test_the_call_site_uses_the_relay_aware_sender_and_the_keyed_ssrc():
95
95
  if src.startswith("_send_video_tmmbr(", i)
96
96
  and not src[:i].endswith("def ")]
97
97
  assert calls, "the bridge must actually send the TMMBR it can build"
98
- # Widen behind the call too: the sender is resolved into a local a few
99
- # lines above (_tmmbr_send = getattr(_bridge_fn, '_send_to_cam', None)).
100
- args = src[max(0, calls[0] - 900):calls[0] + 400]
98
+ # Window from the cadence block's start to the PLI block that follows,
99
+ # so it covers how the sender is resolved as well as how it is used -
100
+ # structural terminators, not a fixed character count that goes vacuous
101
+ # as comments grow.
102
+ start = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
103
+ args = src[start:src.index("_pli_done", start)]
101
104
  assert "_cam_srtp_sock.sendto" not in args, (
102
105
  "a raw socket write is silently inert on a TURN-relayed session -- "
103
106
  "use the bridge's relay-aware sender, as the NACK path does")
@@ -164,8 +167,8 @@ def test_the_bridge_gates_the_send_on_it():
164
167
 
165
168
  src = (pathlib.Path(__file__).resolve().parents[1] / "aidot_cameras"
166
169
  / "camera" / "sdes_open.py").read_text()
167
- i = src.index("_tmmbr_send = getattr(_bridge_fn, '_send_to_cam', None)")
168
- window = src[i:i + 900]
170
+ i = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
171
+ window = src[i:src.index("_pli_done", i)]
169
172
  assert "_tmmbr_ready(" in window, (
170
173
  "the TMMBR cadence must consult _tmmbr_ready, or AIDOT_SDES_TMMBR_AFTER_S "
171
174
  "does nothing and the measurement is between-session again")
@@ -197,8 +200,14 @@ def test_the_switches_are_resolved_once_per_session_not_per_packet():
197
200
  assert "_sdes_tmmbr_after_s()" in setup
198
201
 
199
202
  # And the cadence reads the resolved value rather than the environment.
200
- i = src.index("_tmmbr_send = getattr(_bridge_fn, '_send_to_cam', None)")
201
- cadence = src[i - 600:i + 900]
203
+ i = src.index("_tmmbr_bps = getattr(_bridge_fn, '_tmmbr_bps', None)")
204
+ cadence = src[i:src.index("_pli_done", i)]
202
205
  assert "_sdes_tmmbr_bps()" not in cadence, (
203
206
  "the cadence must not call os.environ; it runs on the packet loop")
204
207
  assert "_sdes_tmmbr_after_s()" not in cadence
208
+ # Cost ordering inside the guard: the once-a-second cadence gate must sit
209
+ # BEFORE the readiness math and the sender lookup, or both run per packet.
210
+ assert cadence.index("_last_tmmbr_ts") < cadence.index("_tmmbr_ready("), (
211
+ "the 1s cadence gate must run before _tmmbr_ready")
212
+ assert cadence.index("_tmmbr_ready(") < cadence.index("_send_to_cam"), (
213
+ "the sender lookup must be the last, rarest check")
@@ -34,7 +34,7 @@ from aidot_cameras.camera.sdes_open import (
34
34
  )
35
35
 
36
36
  LOCAL = "192.168.7.2"
37
- PUBLIC = "173.53.36.206"
37
+ PUBLIC = "203.0.113.7"
38
38
  OURS = (41000, 41002) # the ports our own srflx candidates advertise
39
39
 
40
40