python-aidot-cameras 0.17.3b2__tar.gz → 1.0.0b2__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 (214) hide show
  1. {python_aidot_cameras-0.17.3b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b2}/PKG-INFO +3 -1
  2. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/README.md +2 -0
  3. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/client.py +13 -4
  4. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/protocol.py +12 -4
  5. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes.py +28 -8
  6. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/sdes_open.py +371 -10
  7. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/pyproject.toml +1 -1
  8. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info}/PKG-INFO +3 -1
  9. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/SOURCES.txt +9 -0
  10. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_control_verdicts.py +12 -2
  11. python_aidot_cameras-1.0.0b2/tests/test_dtls_resolution_ack.py +238 -0
  12. python_aidot_cameras-1.0.0b2/tests/test_feature_probe_needs_a_live_session.py +104 -0
  13. python_aidot_cameras-1.0.0b2/tests/test_gate_requires_a_decoded_frame.py +101 -0
  14. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_live_validate_policy.py +182 -0
  15. python_aidot_cameras-1.0.0b2/tests/test_no_key_material_in_logs.py +269 -0
  16. python_aidot_cameras-1.0.0b2/tests/test_ptz_probe_reads_what_it_is_told.py +88 -0
  17. python_aidot_cameras-1.0.0b2/tests/test_sdes_first_media_stall_report.py +344 -0
  18. python_aidot_cameras-1.0.0b2/tests/test_sdes_offer_video_order.py +263 -0
  19. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_talk.py +16 -0
  20. python_aidot_cameras-1.0.0b2/tests/test_sprop_write_is_reported_truthfully.py +102 -0
  21. python_aidot_cameras-1.0.0b2/tests/test_talk_reports_a_speaker_that_never_opened.py +170 -0
  22. python_aidot_cameras-1.0.0b2/tests/test_tutk_path_is_out_of_reach.py +78 -0
  23. python_aidot_cameras-0.17.3b2/tests/test_no_key_material_in_logs.py +0 -140
  24. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/LICENSE +0 -0
  25. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/MANIFEST.in +0 -0
  26. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__init__.py +0 -0
  27. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/__main__.py +0 -0
  28. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_upstream.py +0 -0
  29. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/__init__.py +0 -0
  30. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  31. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  32. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  33. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  34. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  35. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  36. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  37. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  38. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  39. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  40. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  41. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  42. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  43. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  44. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  45. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  46. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  47. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  48. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  49. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  50. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  51. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  52. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  53. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  54. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  55. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  56. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  57. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  58. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  59. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  60. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  61. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  62. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/ble_gateway.py +0 -0
  63. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/__init__.py +0 -0
  64. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/constants.py +0 -0
  65. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/controls.py +0 -0
  66. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/go2rtc.py +0 -0
  67. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/hwaccel.py +0 -0
  68. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/lan_control.py +0 -0
  69. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/models.py +0 -0
  70. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/playback.py +0 -0
  71. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/tutk.py +0 -0
  72. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc.py +0 -0
  73. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/camera/webrtc_open.py +0 -0
  74. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/client.py +0 -0
  75. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/const.py +0 -0
  76. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/credentials.py +0 -0
  77. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/crypto.py +0 -0
  78. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/device_client.py +0 -0
  79. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/discover.py +0 -0
  80. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/exceptions.py +0 -0
  81. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/g711.py +0 -0
  82. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/lan_retry.py +0 -0
  83. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/aidot_cameras/py.typed +0 -0
  84. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  85. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  86. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/requires.txt +0 -0
  87. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  88. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/setup.cfg +0 -0
  89. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_aioice_compat.py +0 -0
  90. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_alarm_event.py +0 -0
  91. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_answer_sdp_video_pt.py +0 -0
  92. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_audio_mux_offline.py +0 -0
  93. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_request.py +0 -0
  94. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_parse.py +0 -0
  95. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_avio_response_router.py +0 -0
  96. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_backoff.py +0 -0
  97. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_detection.py +0 -0
  98. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_evidence_corroboration.py +0 -0
  99. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_battery_skip_turn.py +0 -0
  100. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ble_gateway.py +0 -0
  101. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_bridge_state_sentinels.py +0 -0
  102. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_busy_backoff.py +0 -0
  103. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_constants_imports_resolve.py +0 -0
  104. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_deferred_hardening.py +0 -0
  105. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_cmd_seq_match.py +0 -0
  106. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_login_guard.py +0 -0
  107. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_device_user_info_cache.py +0 -0
  108. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dormant_camera_cleanup.py +0 -0
  109. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_mux_terminal_write.py +0 -0
  110. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_not_ready_burst.py +0 -0
  111. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_pinning.py +0 -0
  112. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_decode_skip.py +0 -0
  113. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_serve_open_timeout.py +0 -0
  114. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_skip_signaling_wait.py +0 -0
  115. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_slow_probe.py +0 -0
  116. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_dtls_stdout_producer.py +0 -0
  117. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_egress_guard.py +0 -0
  118. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_env_seams.py +0 -0
  119. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_futile_keepalive.py +0 -0
  120. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc.py +0 -0
  121. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_go2rtc_cli.py +0 -0
  122. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_h264_decode_rate_limit.py +0 -0
  123. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_highport_nomination.py +0 -0
  124. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_hwaccel_probe.py +0 -0
  125. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_config_cache.py +0 -0
  126. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_ice_peer_reflexive.py +0 -0
  127. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_idle_release_both_loops.py +0 -0
  128. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_keepalive_renew.py +0 -0
  129. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_key_restart_sdp.py +0 -0
  130. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_keyframe_prompter.py +0 -0
  131. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_control.py +0 -0
  132. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  133. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_light_active_color_mode.py +0 -0
  134. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_live_stream_param.py +0 -0
  135. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_logging_caps.py +0 -0
  136. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_login_connect_deadline.py +0 -0
  137. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_login_reconnect_backoff.py +0 -0
  138. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_counted_only_when_plaintext.py +0 -0
  139. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_socket_rcvbuf.py +0 -0
  140. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_media_wait_floor.py +0 -0
  141. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_motion_poll.py +0 -0
  142. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_refetch.py +0 -0
  143. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_credential_self_heal.py +0 -0
  144. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_password_not_persisted.py +0 -0
  145. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_mqtt_session_reconnect.py +0 -0
  146. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_narrow_pc_ice.py +0 -0
  147. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_no_self_referential_go2rtc.py +0 -0
  148. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_no_undefined_names.py +0 -0
  149. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  150. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_offline_keepalive_pause.py +0 -0
  151. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_open_fail_logger.py +0 -0
  152. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_open_gate_delay.py +0 -0
  153. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_peerid_reuse.py +0 -0
  154. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt.py +0 -0
  155. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_persistent_mqtt_robustness.py +0 -0
  156. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_playback_tls.py +0 -0
  157. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_post_merge_hardening.py +0 -0
  158. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_remb.py +0 -0
  159. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_resolution_persists.py +0 -0
  160. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_restart_cycle.py +0 -0
  161. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_retry_policy.py +0 -0
  162. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_scripts_importable.py +0 -0
  163. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sctp_init_ack_tsn.py +0 -0
  164. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_adaptive.py +0 -0
  165. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_avio_dispatch.py +0 -0
  166. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_bridge_break_gate.py +0 -0
  167. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_echo_wait_timeout.py +0 -0
  168. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_fast_liveplay.py +0 -0
  169. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  170. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_idle_release.py +0 -0
  171. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_offer_video_pin.py +0 -0
  172. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_open_cleanup.py +0 -0
  173. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sctp_abort.py +0 -0
  174. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_audio.py +0 -0
  175. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_cmd.py +0 -0
  176. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_serve_stderr_drain.py +0 -0
  177. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_sprop.py +0 -0
  178. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_teardown_exit_log.py +0 -0
  179. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack.py +0 -0
  180. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_terminal_ack_latency.py +0 -0
  181. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdes_watchdog.py +0 -0
  182. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_narrowing_properties.py +0 -0
  183. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sdp_payload_type_narrowing.py +0 -0
  184. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_security_hardening.py +0 -0
  185. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serializable_login_info.py +0 -0
  186. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_loss_tolerance.py +0 -0
  187. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_serve_relay.py +0 -0
  188. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_session_stats.py +0 -0
  189. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_shared_house_seam.py +0 -0
  190. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_snapshot_cancellation.py +0 -0
  191. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_speak.py +0 -0
  192. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_capture_gate.py +0 -0
  193. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_sprop_staleness.py +0 -0
  194. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_srtp_rx_rekey.py +0 -0
  195. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_srtp_tx_key_note.py +0 -0
  196. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_cap.py +0 -0
  197. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_drain_ownership.py +0 -0
  198. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_idle.py +0 -0
  199. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_slot_cap.py +0 -0
  200. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_stream_teardown.py +0 -0
  201. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_talk.py +0 -0
  202. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_terminal_ack.py +0 -0
  203. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_token_file_robustness.py +0 -0
  204. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_token_refresh.py +0 -0
  205. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_turn_create_permission.py +0 -0
  206. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_upstream_compat.py +0 -0
  207. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_video_profile_log.py +0 -0
  208. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_check_throttle.py +0 -0
  209. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_presence.py +0 -0
  210. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_viewer_present_push_mode.py +0 -0
  211. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wait_or_event.py +0 -0
  212. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_gate_camera_evidence.py +0 -0
  213. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/tests/test_wake_readiness_retry.py +0 -0
  214. {python_aidot_cameras-0.17.3b2 → python_aidot_cameras-1.0.0b2}/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: 0.17.3b2
3
+ Version: 1.0.0b2
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
@@ -209,6 +209,8 @@ audio, idle release, the sprop cache path) are documented in
209
209
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
210
210
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
211
211
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
212
+ | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
213
+ | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. | unset (`96 97`) |
212
214
  | `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
213
215
  | `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) |
214
216
  | `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
@@ -178,6 +178,8 @@ audio, idle release, the sprop cache path) are documented in
178
178
  | `AIDOT_SDES_SKIP_TURN_PREALLOC` | Skip the SDES TURN relay pre-allocation (~2-3 s of cold-start latency) so signaling goes straight out with the host candidate. Faster on a LAN, at the cost of no relay fallback for a camera on a different segment / behind strict NAT. Experimental, opt-in (truthy = `1`/`true`/`yes`/`on`). | unset (off) |
179
179
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
180
180
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
181
+ | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
182
+ | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. | unset (`96 97`) |
181
183
  | `AIDOT_DTLS_FAST_LIVEPLAY` | The DTLS (A000088) analogue: skip the `livePlayReq`-echo and `livePlayResp` waits (the dominant LAN cold-start cost) while keeping the full ICE/TURN/DTLS handshake, so remote/relay viewing is unaffected. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
182
184
  | `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) |
183
185
  | `AIDOT_SERVE_RELAY` | Hold the public stream port via an internal relay that proxies to ffmpeg, so the first (cold) view connects instead of failing while ffmpeg can't pre-bind the port. Set to `0` to serve ffmpeg directly. | `1` (enabled) |
@@ -1823,13 +1823,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
1823
1823
  self.device_id, len(data))
1824
1824
  elif isinstance(body, dict):
1825
1825
  data = body.get("data") or {}
1826
+ # Keys, never values: this response can carry the per-device
1827
+ # streaming credentials (tutkAccount / tutkPassword - see
1828
+ # docs/DEFERRED_FEATURES.md), and the warning below is logged
1829
+ # at default level, so a whole-body dump lands in every user's
1830
+ # home-assistant.log. The matched-device log further down
1831
+ # already prints keys only; these two did not.
1826
1832
  if data:
1827
- _LOGGER.debug("batchGetDeviceUserInfo response for %s (status=%d): %s",
1828
- self.device_id, status, body)
1833
+ _LOGGER.debug(
1834
+ "batchGetDeviceUserInfo response for %s (status=%d):"
1835
+ " keys=%s, %d item(s)",
1836
+ self.device_id, status, sorted(body.keys()), len(data),
1837
+ )
1829
1838
  else:
1830
1839
  _LOGGER.warning(
1831
- "batchGetDeviceUserInfo no data for %s (status=%d): %s",
1832
- self.device_id, status, body,
1840
+ "batchGetDeviceUserInfo no data for %s (status=%d): keys=%s",
1841
+ self.device_id, status, sorted(body.keys()),
1833
1842
  )
1834
1843
  else:
1835
1844
  data = {}
@@ -662,8 +662,14 @@ def _load_sprop(devid: str) -> "Optional[str]":
662
662
  return None
663
663
 
664
664
 
665
- def _save_sprop(devid: str, sprop: str) -> None:
666
- """Persist the ``sprop-parameter-sets`` string for ``devid`` (best-effort).
665
+ def _save_sprop(devid: str, sprop: str) -> bool:
666
+ """Persist the ``sprop-parameter-sets`` for ``devid``. True if it wrote.
667
+
668
+ Returns whether anything was actually cached, because two of the three
669
+ outcomes here write nothing and the caller announces the write in the only
670
+ log line this feature has. An unconditional announcement claims a cache
671
+ that an unstable camera provably does not have, which is the observable an
672
+ investigation reads first.
667
673
 
668
674
  If a DIFFERENT value was already cached, this camera does not keep its
669
675
  parameter sets stable: mark it and drop the cache, so nothing is injected
@@ -672,7 +678,7 @@ def _save_sprop(devid: str, sprop: str) -> None:
672
678
  loses nothing by the injection being skipped.
673
679
  """
674
680
  if _sprop_is_unstable(devid):
675
- return # already known unstable; nothing to cache for this camera
681
+ return False # already known unstable; nothing to cache for this camera
676
682
  try:
677
683
  prev = _load_sprop(devid)
678
684
  if prev is not None and prev != sprop:
@@ -689,17 +695,19 @@ def _save_sprop(devid: str, sprop: str) -> None:
689
695
  os.remove(_sprop_cache_path(devid))
690
696
  except OSError:
691
697
  pass
692
- return
698
+ return False
693
699
  os.makedirs(_SPROP_DIR, exist_ok=True)
694
700
  tmp = _sprop_cache_path(devid) + ".tmp"
695
701
  with open(tmp, "w") as fh:
696
702
  fh.write(sprop)
697
703
  os.replace(tmp, _sprop_cache_path(devid))
704
+ return True
698
705
  except OSError as exc:
699
706
  # Surface (don't swallow): if the cache dir isn't writable the whole
700
707
  # sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
701
708
  _LOGGER.warning("sprop cache write failed (%s): %s - set AIDOT_SPROP_DIR "
702
709
  "to a writable path", _SPROP_DIR, exc)
710
+ return False
703
711
 
704
712
 
705
713
  def _inject_sprop(sdp: str, devid: str) -> str:
@@ -133,12 +133,19 @@ class SdesSession(AvioRequestMixin):
133
133
  def talk_supported(self) -> bool:
134
134
  """True if two-way audio can be used on this SDES session.
135
135
 
136
- Requires a talk-capable open (offer advertised sendrecv + a=ssrc). The
137
- camera audio address is filled in by the bridge on first inbound audio;
138
- async_start_talk tolerates being called before that - the pump waits for
139
- the address before emitting.
136
+ Requires a talk-capable open (offer advertised sendrecv + a=ssrc) AND a
137
+ session that is still running. The camera audio address is filled in by
138
+ the bridge on first inbound audio; async_start_talk tolerates being
139
+ called before that - the pump waits for the address before emitting.
140
+
141
+ Liveness is part of the answer because the talk state is a plain dict
142
+ that outlives ffmpeg, while everything that acts on it - the bridge
143
+ thread that sends SPEAKERSTART, the socket the pump writes to - dies
144
+ with ffmpeg. ``async_speak`` reuses the warm session whenever this is
145
+ true, so answering from the dict alone routed talk into a session that
146
+ could no longer carry it instead of opening a fresh one.
140
147
  """
141
- return bool(self._talk_state)
148
+ return bool(self._talk_state) and self.is_alive
142
149
 
143
150
  async def async_start_talk(
144
151
  self, pcm_provider: "Callable[[], Optional[bytes]]"
@@ -170,10 +177,23 @@ class SdesSession(AvioRequestMixin):
170
177
  target=_run_sdes_talk_pump, args=(self._talk_state,), daemon=True
171
178
  )
172
179
  self._talk_thread.start()
173
- if await _speaker_ack_accepted(waiter, SDES_SPEAKER_ACK_TIMEOUT_S):
180
+ accepted = await _speaker_ack_accepted(waiter, SDES_SPEAKER_ACK_TIMEOUT_S)
181
+ # _speaker_ack_accepted is generous by design: it treats an unfamiliar or
182
+ # absent ack as acceptance, because no genuine refusal has ever been
183
+ # identified and guessing at one would break working cameras. That
184
+ # generosity is about INTERPRETING the camera's answer, and it has
185
+ # nothing to say about the case where the question was never asked.
186
+ # `speaker_on` is set on exactly one line - immediately after the bridge
187
+ # thread dispatches SPEAKERSTART(848) - so a falsy flag here means our
188
+ # own bridge never sent the command, which happens whenever ffmpeg (and
189
+ # with it that thread) has already exited. The ack budget above already
190
+ # covers SDES_SPEAKERSTART_DELAY, so a live bridge has always set it by
191
+ # now and this cannot race a working session.
192
+ if accepted and self._talk_state.get("speaker_on"):
174
193
  return True
175
- # The camera refused. Stop the microphone rather than stream viewer
176
- # audio at a speaker that never opened.
194
+ # Either the camera refused, or the speaker command never left this
195
+ # process. Stop the microphone rather than stream viewer audio at a
196
+ # speaker that never opened.
177
197
  self._talk_state["want_speaker"] = False
178
198
  self._talk_state["provider"] = None
179
199
  return False
@@ -494,6 +494,107 @@ def _resolve_sdes_video_pt() -> Optional[int]:
494
494
  return pt if pt in _SDES_ANSWER_VIDEO_PTS else None
495
495
 
496
496
 
497
+ #: The video codecs the SDES OFFER advertises, keyed by payload type, with the
498
+ #: rtpmap/fmtp lines that describe each one. Deliberately NOT reusing
499
+ #: ``_SDES_ANSWER_VIDEO_PTS``: that tuple is what the answer template carries
500
+ #: and what the pin is validated against, and coupling the offer's wire order to
501
+ #: it means a later reorder of one silently reorders the other.
502
+ _SDES_OFFER_VIDEO_CODECS = {
503
+ 96: (
504
+ "a=rtpmap:96 H264/90000\r\n"
505
+ "a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;"
506
+ "profile-level-id=42e01f\r\n"
507
+ ),
508
+ 97: (
509
+ "a=rtpmap:97 H265/90000\r\n"
510
+ "a=fmtp:97 level-id=93\r\n"
511
+ ),
512
+ }
513
+
514
+ #: The order those codecs go on the wire today: H264 first, H265 second. This
515
+ #: is the shipped default and changing it changes every SDES camera's
516
+ #: negotiation, so it is overridden per-run by env and never edited here.
517
+ _SDES_OFFER_VIDEO_PT_ORDER = (96, 97)
518
+
519
+
520
+ def _resolve_sdes_video_pt_order() -> tuple:
521
+ """EXPERIMENTAL (opt-in, default off): reorder the OFFER's video codec list.
522
+
523
+ RFC 3264 section 5.1 makes the ``m=video`` payload-type list a *preference*
524
+ list, most-preferred first. The offer this module sends carries ``96 97``,
525
+ i.e. it already states a preference for H264 -- so the often-repeated
526
+ shorthand that our offer "expresses no preference" is not what the SDP says.
527
+ What is true is that nothing here ever *chose* that order: the line arrived
528
+ verbatim when this path was extracted from ``client.py`` and has never been
529
+ varied.
530
+
531
+ Measured on an A001064, the camera answers H264 most of the time and H265
532
+ occasionally for an otherwise identical request (nine and two across eleven
533
+ sessions one afternoon). Read against the offer, that is a camera which
534
+ honours our stated first choice most of the time and disregards it some of
535
+ the time -- which is a reason to expect *less* of reordering than of the
536
+ pin, and is recorded here so nobody reads this knob as established.
537
+
538
+ Why it is worth having anyway: the efficient profile (hevc 2560x1440 at
539
+ ~1.1 Mbps against h264 1280x720 at 2.5-4.0 Mbps) has only ever appeared when
540
+ BOTH codecs are on the wire. ``AIDOT_SDES_VIDEO_PT=97`` narrows to H265 and
541
+ returns no video at all, 3 of 3 rounds -- narrowing removes the option
542
+ rather than selecting it. Reordering is the only untried lever that leaves
543
+ both codecs offered, so the camera can still fall back to H264.
544
+
545
+ Accepts a comma- or space-separated payload-type list (``97,96``, ``97``).
546
+ Whatever is named goes first, in the order named; every advertised codec not
547
+ named is appended in the default order. So this can express a preference
548
+ and can never narrow the offer: the result is always a permutation of the
549
+ full advertised set, and an empty or entirely unusable value yields exactly
550
+ today's order. Narrowing already has its own variable, and the one time it
551
+ was measured it cost the picture.
552
+
553
+ Unknown payload types and duplicates are dropped rather than honoured: a
554
+ payload type this offer does not advertise has no rtpmap to go with it, and
555
+ listing one on the m-line would name a codec the camera was never given the
556
+ parameters for.
557
+ """
558
+ raw = os.environ.get("AIDOT_SDES_VIDEO_PT_ORDER", "") or ""
559
+ named: list = []
560
+ for tok in raw.replace(",", " ").split():
561
+ if not tok.isdigit():
562
+ continue
563
+ pt = int(tok)
564
+ if pt in _SDES_OFFER_VIDEO_CODECS and pt not in named:
565
+ named.append(pt)
566
+ return tuple(named) + tuple(
567
+ pt for pt in _SDES_OFFER_VIDEO_PT_ORDER if pt not in named
568
+ )
569
+
570
+
571
+ def _sdes_offer_video_codec_lines(order=None) -> tuple:
572
+ """Build the offer's video codec list and its rtpmap/fmtp block.
573
+
574
+ Returns ``(pt_list, attrs)`` where ``pt_list`` is the payload-type list for
575
+ the ``m=video`` line ("96 97") and ``attrs`` is the rtpmap/fmtp lines for
576
+ those payload types, in the same order. Both have to move together: an
577
+ m-line naming a payload type whose rtpmap was left behind is an offer the
578
+ camera cannot act on.
579
+
580
+ ``order`` of None is today's shipped order, so the default output is
581
+ byte-identical to the literal this replaced. Anything not advertised is
582
+ dropped, and an order that ends up empty falls back to the default -- an
583
+ ``m=video`` line with no payload type at all leaves the camera nothing to
584
+ send, which is the one outcome worse than an unpinned choice.
585
+ """
586
+ pts = tuple(
587
+ pt for pt in (order if order is not None else _SDES_OFFER_VIDEO_PT_ORDER)
588
+ if pt in _SDES_OFFER_VIDEO_CODECS
589
+ )
590
+ if not pts:
591
+ pts = _SDES_OFFER_VIDEO_PT_ORDER
592
+ return (
593
+ " ".join(str(pt) for pt in pts),
594
+ "".join(_SDES_OFFER_VIDEO_CODECS[pt] for pt in pts),
595
+ )
596
+
597
+
497
598
  def narrow_sdp_payload_types(sdp_text: str, keep_video=None, keep_audio=None) -> str:
498
599
  """Rewrite an SDP to advertise a single payload type per media line.
499
600
 
@@ -767,6 +868,108 @@ def _record_peer_reflexive(known, discovered, observed, is_self=None):
767
868
  return [*discovered, observed]
768
869
 
769
870
 
871
+ #: How many distinct inbound probe sources the stall report will name. Beyond
872
+ #: this the count alone is kept - a stall is explained by the first few sources,
873
+ #: and an unbounded list would turn one WARNING into a wall of text.
874
+ _MAX_PROBE_SOURCES = 6
875
+
876
+
877
+ def _probe_source_verdict(src, turn_peer_ip, turn_peer_port, *,
878
+ cam_peer, observed, known, learned):
879
+ """Say why one inbound STUN probe's source was, or was not, learned.
880
+
881
+ Nomination can only aim at an address, and when the camera's answer lists
882
+ one this host cannot route to, the only usable addresses are the ones its
883
+ own probes arrive from. Two vetoes drop a relay-carried probe before it can
884
+ become a peer-reflexive candidate, and both are silent in the code they
885
+ guard, so a session that stalls for one reason is indistinguishable from a
886
+ session that stalled for the other. This names them apart.
887
+
888
+ Takes the values the bridge already computed rather than re-deriving them:
889
+ ``cam_peer`` is ``_br_cam_peer`` (None once ``_is_self_peer_ip`` has refused
890
+ the XOR-PEER-ADDRESS) and ``observed`` is ``_br_obs`` (None once the
891
+ ``_bsrc`` fallback has refused the TURN server's own address too). A copy
892
+ of the decision here would rot the first time either site changed.
893
+
894
+ ``known`` is "this address is already in the nomination set" and ``learned``
895
+ is "this call grew the peer-reflexive set"; without both, an address that
896
+ ``_record_peer_reflexive`` refused on its own policy would be reported as
897
+ one we are already nominating, which is the opposite of the truth.
898
+
899
+ Pure, so the classification is testable without a camera.
900
+ """
901
+ if observed:
902
+ if learned:
903
+ return "learned"
904
+ if known:
905
+ return "known"
906
+ # _record_peer_reflexive took it and gave nothing back: its own self-IP
907
+ # check, or the peer-reflexive cap.
908
+ return "prflx-refused"
909
+ if turn_peer_ip and not turn_peer_port:
910
+ return "vetoed-no-peer-port"
911
+ if turn_peer_ip and not cam_peer:
912
+ # _is_self_peer_ip matched the XOR-PEER-ADDRESS.
913
+ return "vetoed-self-ip"
914
+ # No usable peer address at all, and the packet came from the relay, so the
915
+ # _bsrc fallback had nothing to offer but the TURN server itself.
916
+ return "vetoed-turn-source"
917
+
918
+
919
+ def _first_media_stall_report(device_id, waited_s, nominated,
920
+ use_candidate_sent, binding_success,
921
+ trigger_sent, probes, probes_dropped=0):
922
+ """Build the one line a first-media stall emits.
923
+
924
+ A session that never delivers a byte looks, in a log, exactly like one that
925
+ delivered late: the wait expires, the serve launches with unknown payload
926
+ types, and nothing states the reason. The reason is knowable at that
927
+ moment. Media only ever follows the AVIO LIVING trigger, that trigger is
928
+ armed only by an inbound STUN Binding Success Response, and that response
929
+ only comes back if something we nominated was reachable - so the nominated
930
+ set, the Binding Success count, the trigger flag and the verdict on every
931
+ inbound probe source are between them the whole explanation.
932
+
933
+ Addresses and counts only. This line reaches ``home-assistant.log`` and
934
+ users paste that file into public issue reports.
935
+
936
+ ``probes`` is a sequence of ``(source_label, verdict)`` from
937
+ :func:`_probe_source_verdict`. Pure, so the wording is testable.
938
+ """
939
+ _cands = ", ".join(f"{_ip}:{_port}" for _ip, _port in (nominated or []))
940
+ _probes = "; ".join(
941
+ f"{_where} -> {_verdict}" for _where, _verdict in (probes or [])
942
+ )
943
+ if probes_dropped:
944
+ _probes += f"; (+{probes_dropped} more source(s))"
945
+ _why = ""
946
+ if not binding_success:
947
+ _why = (
948
+ " No inbound STUN Binding Success arrived, so the AVIO LIVING"
949
+ " trigger was never armed and the camera never started sending."
950
+ )
951
+ elif not trigger_sent:
952
+ _why = (
953
+ " A Binding Success arrived and the trigger still did not go -"
954
+ " the trigger is not gated on anything else."
955
+ )
956
+ return (
957
+ "camera %s: SDES first media never arrived (%.0fs)."
958
+ " nominated=%s; use-candidate=%s; binding-success=%d; trigger=%s;"
959
+ " probes=%s.%s"
960
+ % (
961
+ device_id,
962
+ waited_s,
963
+ _cands or "none",
964
+ "sent" if use_candidate_sent else "not-sent",
965
+ binding_success,
966
+ "sent" if trigger_sent else "not-sent",
967
+ _probes or "none",
968
+ _why,
969
+ )
970
+ )
971
+
972
+
770
973
  def _ensure_srtp_rx_session(holder, key, build, *, on_built=None, on_error=None):
771
974
  """Return the bridge's SRTP receive session, rebuilt when ``key`` changes.
772
975
 
@@ -1380,6 +1583,13 @@ class _SdesOpenMixin:
1380
1583
  "spk_eligible_ts": None, # bridge: first time SPEAKERSTART is eligible
1381
1584
  "stop": False,
1382
1585
  } if _talk_offer else None
1586
+ # Video codec preference, expressed by m-line order (RFC 3264 5.1).
1587
+ # Default is today's 96 97 and the bytes are identical to the literal
1588
+ # this replaced; AIDOT_SDES_VIDEO_PT_ORDER reorders it without ever
1589
+ # narrowing it.
1590
+ _video_pt_order = _resolve_sdes_video_pt_order()
1591
+ _video_pt_list, _video_codec_attrs = _sdes_offer_video_codec_lines(
1592
+ _video_pt_order)
1383
1593
  sdes_offer_sdp = (
1384
1594
  "v=0\r\n"
1385
1595
  f"o=- {ts} {ts} IN IP4 {local_ip}\r\n"
@@ -1418,16 +1628,13 @@ class _SdesOpenMixin:
1418
1628
  if _audio_sock in _relay_addrs else ""
1419
1629
  )
1420
1630
  # video m-section
1421
- + f"m=video {_offer_video_port} RTP/SAVPF 96 97\r\n"
1631
+ + f"m=video {_offer_video_port} RTP/SAVPF {_video_pt_list}\r\n"
1422
1632
  f"c=IN IP4 {_offer_video_ip}\r\n"
1423
1633
  "a=recvonly\r\n"
1424
1634
  "a=mid:1\r\n"
1425
1635
  f"a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:{srtp_key_video}\r\n"
1426
- "a=rtpmap:96 H264/90000\r\n"
1427
- "a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n"
1428
- "a=rtpmap:97 H265/90000\r\n"
1429
- "a=fmtp:97 level-id=93\r\n"
1430
- "a=rtcp-mux\r\n"
1636
+ + _video_codec_attrs
1637
+ + "a=rtcp-mux\r\n"
1431
1638
  f"a=ice-ufrag:{_ufrag_v}\r\n"
1432
1639
  f"a=ice-pwd:{_pwd_v}\r\n"
1433
1640
  f"a=candidate:1 1 udp 2130706431 {local_ip} {video_port} typ host\r\n"
@@ -1460,8 +1667,21 @@ class _SdesOpenMixin:
1460
1667
  )
1461
1668
  )
1462
1669
 
1463
- # Opt-in: express a video-codec preference in the OFFER rather than
1464
- # advertising both 96/97 and letting the camera decide in its answer.
1670
+ # Receipt for the codec order above, emitted only when it differs from
1671
+ # the shipped one. A run that cannot show the knob was applied cannot
1672
+ # tell a result from a coincidence: an earlier attempt on this question
1673
+ # read as a confirmed effect for two sessions before the missing receipt
1674
+ # showed the pin had never reached the SDP at all. Ordering happens
1675
+ # before the pin below, so with both set the pin wins and the order is
1676
+ # moot - which is why both lines print rather than one.
1677
+ if _video_pt_order != _SDES_OFFER_VIDEO_PT_ORDER and _status:
1678
+ _status(f"SDES: offer video codec order={_video_pt_list}")
1679
+
1680
+ # Opt-in: NARROW the OFFER to one video codec rather than advertising
1681
+ # both 96/97 and letting the camera decide in its answer. Distinct from
1682
+ # the ordering above, which only states which of the two we would
1683
+ # rather have and always leaves both on the wire; this one takes the
1684
+ # other away, which for 97 took the video with it.
1465
1685
  #
1466
1686
  # The offer is the SDP that matters here. Traced live 2026-08-07 with
1467
1687
  # every status line printed: this path sends webrtcReq carrying OUR
@@ -2864,6 +3084,17 @@ class _SdesOpenMixin:
2864
3084
  if not _is_self_peer_ip(cam_addr[0]):
2865
3085
  _turn_send_indication(sock, cam_addr[0], cam_addr[1], _req)
2866
3086
 
3087
+ # Every candidate this session actually sent USE-CANDIDATE to, in the
3088
+ # order it was nominated. Instrumentation only, and it exists because
3089
+ # _bridge_uc_info cannot answer the question on the shape that stalls:
3090
+ # on a late answer - measured at +1.3 s, missing the pre-launch snapshot
3091
+ # - it is _nominate_from_answer_sdp below that nominates, and it neither
3092
+ # appends to _bridge_uc_info["cands"] nor flips ["sent"]. Reporting the
3093
+ # stall from that dict alone would print "nominated=none" on an open
3094
+ # that did nominate, and make "the answer carried no ICE credentials"
3095
+ # indistinguishable from "the answer carried one unroutable candidate".
3096
+ _nominated_seen: list = []
3097
+
2867
3098
  def _nominate_from_answer_sdp(sdp_text) -> int:
2868
3099
  """Nominate every candidate in ``sdp_text``; return how many.
2869
3100
 
@@ -2903,6 +3134,8 @@ class _SdesOpenMixin:
2903
3134
  _audio_sock, _ufrag_a, _pwd_a, _u, _p, (_c_ip, _c_port))
2904
3135
  _send_use_candidate(
2905
3136
  _video_sock, _ufrag_v, _pwd_v, _u, _p, (_c_ip, _c_port))
3137
+ if (_c_ip, _c_port) not in _nominated_seen:
3138
+ _nominated_seen.append((_c_ip, _c_port))
2906
3139
  return len(_cands)
2907
3140
 
2908
3141
  # Open the TURN relay's door for the camera BEFORE probing it.
@@ -3293,6 +3526,22 @@ class _SdesOpenMixin:
3293
3526
  _s.sendto(_data, _addr)
3294
3527
  _br_stun_resp_count = 0
3295
3528
  _tutk_trigger_sent = False
3529
+ # Instrumentation for the first-media stall report. These four are
3530
+ # bridge-thread locals that the main coroutine has to be able to
3531
+ # read when the wait expires, so each is mirrored onto _bridge_fn at
3532
+ # its write site - the same publication _sprop_done uses. Nothing
3533
+ # here is read by the bridge itself; they only ever describe.
3534
+ _br_binding_success_count = 0 # inbound STUN Binding Success (0x0101)
3535
+ _br_probe_verdicts: dict = {} # probe source -> why it was/wasn't learned
3536
+ # A SET, not a counter: one unrecorded source probing fifty times
3537
+ # is one source, and a per-packet count would put a wrong number in
3538
+ # the report for someone to reason from.
3539
+ _br_probe_overflow: set = set() # sources past _MAX_PROBE_SOURCES
3540
+ _bridge_fn._tutk_trigger_sent = False
3541
+ _bridge_fn._br_stun_resp_count = 0
3542
+ _bridge_fn._br_binding_success_count = 0
3543
+ _bridge_fn._br_probe_verdicts = {}
3544
+ _bridge_fn._br_probe_overflow = 0
3296
3545
  _last_trigger_ts = 0.0 # time of last AVIO LIVING send
3297
3546
  _trigger_bs = None # socket used for trigger
3298
3547
  _trigger_bsrc = None # camera addr for trigger
@@ -3799,6 +4048,49 @@ class _SdesOpenMixin:
3799
4048
  f" (not in the {len(_bridge_uc_info['cands'])}"
3800
4049
  f" advertised candidate(s)) - will nominate it"
3801
4050
  )
4051
+ # Record why this probe's source was, or was not,
4052
+ # usable. Both refusals above are silent, and which
4053
+ # of them fired decides what a fix would change - so
4054
+ # the fact is kept here and reported once, on the
4055
+ # stall path, rather than logged per packet.
4056
+ try:
4057
+ _br_pv_where = f"{_bsrc[0]}:{_bsrc[1]}"
4058
+ if _br_turn_peer_ip:
4059
+ _br_pv_where += (
4060
+ f" via {_br_turn_peer_ip}"
4061
+ f":{_br_turn_peer_port}"
4062
+ )
4063
+ if _br_pv_where in _br_probe_verdicts:
4064
+ pass
4065
+ elif len(_br_probe_verdicts) >= _MAX_PROBE_SOURCES:
4066
+ _br_probe_overflow.add(_br_pv_where)
4067
+ _bridge_fn._br_probe_overflow = len(
4068
+ _br_probe_overflow)
4069
+ else:
4070
+ _br_probe_verdicts[_br_pv_where] = (
4071
+ _probe_source_verdict(
4072
+ _bsrc,
4073
+ _br_turn_peer_ip,
4074
+ _br_turn_peer_port,
4075
+ cam_peer=_br_cam_peer,
4076
+ observed=_br_obs,
4077
+ known=bool(
4078
+ _br_obs is not None
4079
+ and (_br_obs in _br_prflx_was
4080
+ or _br_obs in
4081
+ _bridge_uc_info["cands"])
4082
+ ),
4083
+ learned=(
4084
+ _br_prflx_now is not _br_prflx_was
4085
+ ),
4086
+ )
4087
+ )
4088
+ # Rebind, never mutate: the reader is the
4089
+ # main coroutine on another thread.
4090
+ _bridge_fn._br_probe_verdicts = dict(
4091
+ _br_probe_verdicts)
4092
+ except Exception:
4093
+ _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_probe_source_verdict', exc_info=True)
3802
4094
  try:
3803
4095
  if _br_turn_peer_ip is None and _bsrc[0] != _hp_host:
3804
4096
  _br_prefer_direct_stun[_bs] = True
@@ -3864,6 +4156,8 @@ class _SdesOpenMixin:
3864
4156
  else:
3865
4157
  _bs.sendto(_bresp, _bsrc)
3866
4158
  _br_stun_resp_count += 1
4159
+ _bridge_fn._br_stun_resp_count = (
4160
+ _br_stun_resp_count)
3867
4161
  # Late USE-CANDIDATE: send when answer arrived
3868
4162
  # after bridge started (empty at setup time).
3869
4163
  if not _bridge_uc_info["sent"] and _bridge_uc_info["ufrag"]:
@@ -3957,6 +4251,16 @@ class _SdesOpenMixin:
3957
4251
  f" relay path unusable"
3958
4252
  )
3959
4253
  continue
4254
+ # Count the inbound Binding Success BEFORE the
4255
+ # trigger gate below. Counting inside it would make
4256
+ # this an alias for _tutk_trigger_sent and collapse
4257
+ # two different diagnoses - "none ever arrived" and
4258
+ # "one arrived and the trigger still did not go" -
4259
+ # into a single indistinguishable state.
4260
+ if _bpkt[:2] == b'\x01\x01':
4261
+ _br_binding_success_count += 1
4262
+ _bridge_fn._br_binding_success_count = (
4263
+ _br_binding_success_count)
3960
4264
  # STUN BindingSuccess (0x0101) from camera: ICE complete.
3961
4265
  # Send AES-128-CBC encrypted SESSION_MODE_REQ (AVIO LIVING).
3962
4266
  #
@@ -3970,6 +4274,7 @@ class _SdesOpenMixin:
3970
4274
  if (_use_plain_rtp and not _tutk_trigger_sent
3971
4275
  and _bpkt[:2] == b'\x01\x01'):
3972
4276
  _tutk_trigger_sent = True
4277
+ _bridge_fn._tutk_trigger_sent = True
3973
4278
  import struct as _st_tk
3974
4279
  import random as _rand_tk
3975
4280
  _ts_ms = int(_time_br.time() * 1000)
@@ -4700,9 +5005,23 @@ class _SdesOpenMixin:
4700
5005
  if 7 in _psc and 8 in _psc:
4701
5006
  _sprop_new = _build_sprop(_psc[7], _psc[8])
4702
5007
  if _sprop_new != _load_sprop(self.device_id):
4703
- _save_sprop(self.device_id, _sprop_new)
4704
- _status("bridge: cached sprop-parameter"
5008
+ # Gated on the result: two of
5009
+ # _save_sprop's three outcomes write
5010
+ # nothing, and this is the only line
5011
+ # the sprop path ever emits.
5012
+ if _save_sprop(self.device_id,
5013
+ _sprop_new):
5014
+ _status(
5015
+ "bridge: cached sprop-parameter"
4705
5016
  f"-sets for {self.device_id}")
5017
+ else:
5018
+ _status(
5019
+ "bridge: parameter sets NOT "
5020
+ f"cached for {self.device_id} "
5021
+ "(camera marked unstable, or "
5022
+ "the cache is unwritable) - "
5023
+ "out-of-band injection stays "
5024
+ "off for it")
4706
5025
  _bridge_fn._sprop_done = True
4707
5026
  # Rebase RTP timestamps to start near 0. Camera picks a
4708
5027
  # random starting timestamp (RFC 3550 section 5.1); the 90 kHz
@@ -5037,6 +5356,48 @@ class _SdesOpenMixin:
5037
5356
  " camera's answer during the first-media wait" % _n_nom
5038
5357
  )
5039
5358
  await asyncio.sleep(0.1)
5359
+ if _first_video_pt[0] is None:
5360
+ # The wait expired with nothing received. Everything needed to say
5361
+ # why is in hand at exactly this moment and was, until now, thrown
5362
+ # away: media only ever follows the AVIO LIVING trigger, that
5363
+ # trigger is armed only by an inbound STUN Binding Success, and
5364
+ # that response only comes back if something we nominated was
5365
+ # reachable. When it was not, the only usable addresses are the
5366
+ # ones the camera's own probes arrived from - and two silent vetoes
5367
+ # can drop a relay-carried probe. One WARNING, on this path only:
5368
+ # an open that delivers media reaches none of this.
5369
+ try:
5370
+ _stall_probes = list(
5371
+ (getattr(_bridge_fn, "_br_probe_verdicts", None) or {}).items()
5372
+ )
5373
+ # Union of every nomination source, deduped and order-preserving:
5374
+ # the answer-peek path records into _nominated_seen, the setup /
5375
+ # trickle / late-answer paths into _bridge_uc_info, and the
5376
+ # bridge's peer-reflexive learning into ["prflx"]. Any one of
5377
+ # them alone under-reports on some shape.
5378
+ _stall_nominated: list = []
5379
+ for _sn in (*_nominated_seen, *_bridge_uc_info["cands"],
5380
+ *_bridge_uc_info["prflx"]):
5381
+ if _sn not in _stall_nominated:
5382
+ _stall_nominated.append(_sn)
5383
+ _LOGGER.warning("%s", _first_media_stall_report(
5384
+ device_id=getattr(self, "device_id", "?"),
5385
+ waited_s=_FIRST_MEDIA_WAIT_S,
5386
+ nominated=_stall_nominated,
5387
+ use_candidate_sent=bool(
5388
+ _bridge_uc_info["sent"] or _nominated_seen),
5389
+ binding_success=int(
5390
+ getattr(_bridge_fn, "_br_binding_success_count", 0)),
5391
+ trigger_sent=bool(
5392
+ getattr(_bridge_fn, "_tutk_trigger_sent", False)),
5393
+ probes=_stall_probes,
5394
+ probes_dropped=int(
5395
+ getattr(_bridge_fn, "_br_probe_overflow", 0)),
5396
+ ))
5397
+ except Exception:
5398
+ _LOGGER.debug("camera %s: swallowed exception in %s",
5399
+ getattr(self, "device_id", "?"),
5400
+ '_first_media_stall_report', exc_info=True)
5040
5401
  if _serve_audio and _first_audio_pt[0] is None:
5041
5402
  _apt_deadline = time.monotonic() + _AUDIO_PT_GRACE_S
5042
5403
  while _first_audio_pt[0] is None and time.monotonic() < _apt_deadline:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "0.17.3b2"
7
+ version = "1.0.0b2"
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"