python-aidot-cameras 1.0.0b2__tar.gz → 1.0.0b3__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 (215) hide show
  1. {python_aidot_cameras-1.0.0b2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b3}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/client.py +16 -0
  3. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/sdes_open.py +61 -15
  4. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/pyproject.toml +1 -1
  5. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  6. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
  7. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_feature_probe_needs_a_live_session.py +28 -0
  8. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ice_peer_reflexive.py +7 -3
  9. python_aidot_cameras-1.0.0b3/tests/test_live_session_carries_talk.py +113 -0
  10. python_aidot_cameras-1.0.0b3/tests/test_self_peer_is_an_address_not_an_ip.py +101 -0
  11. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/LICENSE +0 -0
  12. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/MANIFEST.in +0 -0
  13. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/README.md +0 -0
  14. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/__main__.py +0 -0
  16. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_upstream.py +0 -0
  17. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  20. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  21. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  22. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  23. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  24. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  25. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  26. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  27. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  28. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  29. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  30. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  31. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  32. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  33. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  34. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  35. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  36. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  37. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  38. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  39. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  40. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  41. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  43. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  44. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  45. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  46. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  47. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  48. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  49. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  50. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/ble_gateway.py +0 -0
  51. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/__init__.py +0 -0
  52. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/constants.py +0 -0
  53. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/controls.py +0 -0
  54. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/go2rtc.py +0 -0
  55. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/hwaccel.py +0 -0
  56. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/lan_control.py +0 -0
  57. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/models.py +0 -0
  58. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/playback.py +0 -0
  59. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/protocol.py +0 -0
  60. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/sdes.py +0 -0
  61. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/tutk.py +0 -0
  62. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc.py +0 -0
  63. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/camera/webrtc_open.py +0 -0
  64. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/client.py +0 -0
  65. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/const.py +0 -0
  66. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/credentials.py +0 -0
  67. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/crypto.py +0 -0
  68. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/device_client.py +0 -0
  69. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/discover.py +0 -0
  70. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/exceptions.py +0 -0
  71. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/g711.py +0 -0
  72. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/lan_retry.py +0 -0
  73. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/aidot_cameras/py.typed +0 -0
  74. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  75. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  76. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/requires.txt +0 -0
  77. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  78. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/setup.cfg +0 -0
  79. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_aioice_compat.py +0 -0
  80. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_alarm_event.py +0 -0
  81. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_answer_sdp_video_pt.py +0 -0
  82. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_audio_mux_offline.py +0 -0
  83. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_request.py +0 -0
  84. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_response_parse.py +0 -0
  85. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_avio_response_router.py +0 -0
  86. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_backoff.py +0 -0
  87. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_detection.py +0 -0
  88. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_evidence_corroboration.py +0 -0
  89. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_battery_skip_turn.py +0 -0
  90. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ble_gateway.py +0 -0
  91. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_bridge_state_sentinels.py +0 -0
  92. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_busy_backoff.py +0 -0
  93. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_constants_imports_resolve.py +0 -0
  94. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_control_verdicts.py +0 -0
  95. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_deferred_hardening.py +0 -0
  96. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_cmd_seq_match.py +0 -0
  97. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_login_guard.py +0 -0
  98. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_device_user_info_cache.py +0 -0
  99. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dormant_camera_cleanup.py +0 -0
  100. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_mux_terminal_write.py +0 -0
  101. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_not_ready_burst.py +0 -0
  102. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_pinning.py +0 -0
  103. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_resolution_ack.py +0 -0
  104. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_serve_decode_skip.py +0 -0
  105. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_serve_open_timeout.py +0 -0
  106. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_skip_signaling_wait.py +0 -0
  107. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_slow_probe.py +0 -0
  108. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_dtls_stdout_producer.py +0 -0
  109. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_egress_guard.py +0 -0
  110. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_env_seams.py +0 -0
  111. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_futile_keepalive.py +0 -0
  112. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  113. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_go2rtc.py +0 -0
  114. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_go2rtc_cli.py +0 -0
  115. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_h264_decode_rate_limit.py +0 -0
  116. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_highport_nomination.py +0 -0
  117. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_hwaccel_probe.py +0 -0
  118. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ice_config_cache.py +0 -0
  119. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_idle_release_both_loops.py +0 -0
  120. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_keepalive_renew.py +0 -0
  121. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_key_restart_sdp.py +0 -0
  122. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_keyframe_prompter.py +0 -0
  123. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_lan_control.py +0 -0
  124. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  125. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_light_active_color_mode.py +0 -0
  126. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_live_stream_param.py +0 -0
  127. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_live_validate_policy.py +0 -0
  128. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_logging_caps.py +0 -0
  129. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_login_connect_deadline.py +0 -0
  130. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_login_reconnect_backoff.py +0 -0
  131. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_counted_only_when_plaintext.py +0 -0
  132. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_socket_rcvbuf.py +0 -0
  133. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_media_wait_floor.py +0 -0
  134. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_motion_poll.py +0 -0
  135. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_credential_refetch.py +0 -0
  136. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_credential_self_heal.py +0 -0
  137. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_password_not_persisted.py +0 -0
  138. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_mqtt_session_reconnect.py +0 -0
  139. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_narrow_pc_ice.py +0 -0
  140. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_key_material_in_logs.py +0 -0
  141. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_self_referential_go2rtc.py +0 -0
  142. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_no_undefined_names.py +0 -0
  143. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  144. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_offline_keepalive_pause.py +0 -0
  145. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_open_fail_logger.py +0 -0
  146. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_open_gate_delay.py +0 -0
  147. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_peerid_reuse.py +0 -0
  148. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_persistent_mqtt.py +0 -0
  149. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_persistent_mqtt_robustness.py +0 -0
  150. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_playback_tls.py +0 -0
  151. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_post_merge_hardening.py +0 -0
  152. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  153. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_remb.py +0 -0
  154. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_resolution_persists.py +0 -0
  155. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_restart_cycle.py +0 -0
  156. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_retry_policy.py +0 -0
  157. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_scripts_importable.py +0 -0
  158. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sctp_init_ack_tsn.py +0 -0
  159. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_adaptive.py +0 -0
  160. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_avio_dispatch.py +0 -0
  161. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_bridge_break_gate.py +0 -0
  162. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_echo_wait_timeout.py +0 -0
  163. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_fast_liveplay.py +0 -0
  164. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  165. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_first_media_stall_report.py +0 -0
  166. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_idle_release.py +0 -0
  167. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_offer_video_order.py +0 -0
  168. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_offer_video_pin.py +0 -0
  169. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_open_cleanup.py +0 -0
  170. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_sctp_abort.py +0 -0
  171. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_audio.py +0 -0
  172. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_cmd.py +0 -0
  173. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_serve_stderr_drain.py +0 -0
  174. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_sprop.py +0 -0
  175. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_talk.py +0 -0
  176. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_teardown_exit_log.py +0 -0
  177. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_terminal_ack.py +0 -0
  178. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_terminal_ack_latency.py +0 -0
  179. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdes_watchdog.py +0 -0
  180. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdp_narrowing_properties.py +0 -0
  181. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sdp_payload_type_narrowing.py +0 -0
  182. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_security_hardening.py +0 -0
  183. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serializable_login_info.py +0 -0
  184. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serve_loss_tolerance.py +0 -0
  185. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_serve_relay.py +0 -0
  186. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_session_stats.py +0 -0
  187. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_shared_house_seam.py +0 -0
  188. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_snapshot_cancellation.py +0 -0
  189. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_speak.py +0 -0
  190. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_capture_gate.py +0 -0
  191. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_staleness.py +0 -0
  192. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  193. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_srtp_rx_rekey.py +0 -0
  194. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_srtp_tx_key_note.py +0 -0
  195. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_cap.py +0 -0
  196. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_drain_ownership.py +0 -0
  197. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_idle.py +0 -0
  198. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_slot_cap.py +0 -0
  199. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_stream_teardown.py +0 -0
  200. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_talk.py +0 -0
  201. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  202. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_terminal_ack.py +0 -0
  203. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_token_file_robustness.py +0 -0
  204. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_token_refresh.py +0 -0
  205. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_turn_create_permission.py +0 -0
  206. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  207. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_upstream_compat.py +0 -0
  208. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_video_profile_log.py +0 -0
  209. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_check_throttle.py +0 -0
  210. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_presence.py +0 -0
  211. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_viewer_present_push_mode.py +0 -0
  212. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wait_or_event.py +0 -0
  213. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wake_gate_camera_evidence.py +0 -0
  214. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/tests/test_wake_readiness_retry.py +0 -0
  215. {python_aidot_cameras-1.0.0b2 → python_aidot_cameras-1.0.0b3}/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.0b2
3
+ Version: 1.0.0b3
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
@@ -3477,6 +3477,22 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
3477
3477
  rtsp_push_url=self._keepalive_rtsp_url,
3478
3478
  timeout=(_FAST_OPEN_TIMEOUT if _use_fast else 120.0),
3479
3479
  reuse_peer_id=_loop_peer_id,
3480
+ # Ask for talk so THIS session can carry it. async_speak
3481
+ # reuses _stream_session only when it is talk-capable, and
3482
+ # an SDES session that did not ask never gets a return audio
3483
+ # track - so without this every press-to-talk opened a
3484
+ # second camera session alongside the live view, and the
3485
+ # camera holds a viewer slot for about 120 s after one ends.
3486
+ #
3487
+ # This changes the SDP offer on the path every SDES live
3488
+ # view takes, which is the kind of change that has caused
3489
+ # fleet-wide blackouts here, so it rests on evidence rather
3490
+ # than reasoning: the release harness has opened all seven
3491
+ # cameras talk-capable across three fleet runs with no
3492
+ # streaming regression. The flag reaches only the offer
3493
+ # (audio sendrecv + a=ssrc) and the shared talk state; it
3494
+ # does not touch the rtsp_push branch this loop uses.
3495
+ talk=True,
3480
3496
  )
3481
3497
  except asyncio.CancelledError:
3482
3498
  self._fast_attempt_override = None
@@ -835,6 +835,42 @@ def _bridge_should_break(rc, teardown_requested: bool) -> bool:
835
835
  _MAX_PRFLX_CANDS = 4
836
836
 
837
837
 
838
+ def _is_self_transport_address(ip, port, *, local_ip, public_ip, own_ports):
839
+ """Is this address US, rather than a peer that merely looks like us?
840
+
841
+ The guard exists so we never nominate our own address: doing that has this
842
+ host answering its own connectivity check, which would arm the media trigger
843
+ at nobody. What it must NOT do is refuse the camera, and for one common
844
+ deployment it did - a camera in the same house reaches the TURN server from
845
+ the same public IP we do, so an address-only comparison called it us. Seven
846
+ first-media stalls were that (`vetoed-self-ip`, ROAD-TO-1.0 item 3): the
847
+ camera's relay-carried Binding Request could not be answered, because the
848
+ branch that wraps the response in a Send Indication is guarded by this, so
849
+ ICE never completed and no media ever started.
850
+
851
+ ICE identifies a candidate by its transport address, and so does this: our
852
+ own is one ip:PORT, and a peer sharing our NAT has the same ip with a
853
+ different port. Comparing the pair keeps the guard exactly as strong for
854
+ the case it was written for and stops it catching the camera.
855
+
856
+ ``port`` may be None where a caller genuinely does not know it. That keeps
857
+ the old, conservative answer - widening a decision on an address we cannot
858
+ fully identify would be guessing, and this guard is the safe default.
859
+
860
+ Loopback and this host's own LAN address are us on ANY port: a different
861
+ port there is still this machine.
862
+
863
+ Pure, so the policy is testable without a camera.
864
+ """
865
+ if not ip:
866
+ return False
867
+ if ip in {"127.0.0.1", "0.0.0.0", local_ip}:
868
+ return True
869
+ if public_ip and ip == public_ip:
870
+ return port is None or port in set(own_ports or ())
871
+ return False
872
+
873
+
838
874
  def _record_peer_reflexive(known, discovered, observed, is_self=None):
839
875
  """Learn the address a STUN probe actually arrived from.
840
876
 
@@ -859,7 +895,7 @@ def _record_peer_reflexive(known, discovered, observed, is_self=None):
859
895
  _ip, _port = observed
860
896
  if not _ip or not _port:
861
897
  return discovered
862
- if is_self is not None and is_self(_ip):
898
+ if is_self is not None and is_self(_ip, _port):
863
899
  return discovered
864
900
  if observed in known or observed in discovered:
865
901
  return discovered
@@ -2386,14 +2422,18 @@ class _SdesOpenMixin:
2386
2422
  + (f" and :{_hp_port2}" if _hp_port != _hp_port2 else "")
2387
2423
  )
2388
2424
 
2389
- def _is_self_peer_ip(_ip: "Optional[str]") -> bool:
2390
- if not _ip:
2391
- return False
2392
- if _ip in {"127.0.0.1", "0.0.0.0", local_ip}:
2393
- return True
2394
- if _public_ip and _ip == _public_ip:
2395
- return True
2396
- return False
2425
+ # The ports our own srflx candidates advertise - the only ports on the
2426
+ # public IP that are actually this host. Anything else there is another
2427
+ # device behind the same NAT, which for this fleet means the camera.
2428
+ _own_srflx_ports = (audio_port, video_port)
2429
+
2430
+ def _is_self_peer_ip(_ip: "Optional[str]", _port: "Optional[int]" = None
2431
+ ) -> bool:
2432
+ return _is_self_transport_address(
2433
+ _ip, _port,
2434
+ local_ip=local_ip, public_ip=_public_ip,
2435
+ own_ports=_own_srflx_ports,
2436
+ )
2397
2437
 
2398
2438
  _selfloop_drop_count = 0
2399
2439
  _bridge_selfloop_drop_count = 0
@@ -2526,7 +2566,8 @@ class _SdesOpenMixin:
2526
2566
  if _turn_peer_ip_sw and _prefer_direct_stun.get(_sock, False):
2527
2567
  pass
2528
2568
  elif (_turn_peer_ip_sw and _sock in _relay_addrs
2529
- and not _is_self_peer_ip(_turn_peer_ip_sw)):
2569
+ and not _is_self_peer_ip(_turn_peer_ip_sw,
2570
+ _turn_peer_port_sw)):
2530
2571
  # Arrived via TURN - respond via Send Indication
2531
2572
  _ri_sw = _relay_addrs[_sock]
2532
2573
  _t_host_sw, _t_port_sw = _ri_sw[4], _ri_sw[5]
@@ -2552,7 +2593,8 @@ class _SdesOpenMixin:
2552
2593
  + _STUN_MAGIC + os.urandom(12)
2553
2594
  + _si_body)
2554
2595
  _sock.sendto(_send_ind, (_t_host_sw, _t_port_sw))
2555
- elif _turn_peer_ip_sw and _is_self_peer_ip(_turn_peer_ip_sw):
2596
+ elif _turn_peer_ip_sw and _is_self_peer_ip(
2597
+ _turn_peer_ip_sw, _turn_peer_port_sw):
2556
2598
  # Self-loop Data Indication (peer == our own
2557
2599
  # local/srflx address). Responding via TURN
2558
2600
  # creates an endless STUN echo loop and no media.
@@ -3081,7 +3123,7 @@ class _SdesOpenMixin:
3081
3123
  # addresses, and still ZERO relay-carried inbound packets.
3082
3124
  # The direct send above is untouched, so directly reachable
3083
3125
  # cameras behave exactly as before.
3084
- if not _is_self_peer_ip(cam_addr[0]):
3126
+ if not _is_self_peer_ip(cam_addr[0], cam_addr[1]):
3085
3127
  _turn_send_indication(sock, cam_addr[0], cam_addr[1], _req)
3086
3128
 
3087
3129
  # Every candidate this session actually sent USE-CANDIDATE to, in the
@@ -4002,7 +4044,8 @@ class _SdesOpenMixin:
4002
4044
  _br_cam_peer = (
4003
4045
  (_br_turn_peer_ip, _br_turn_peer_port)
4004
4046
  if _br_turn_peer_ip and _br_turn_peer_port
4005
- and not _is_self_peer_ip(_br_turn_peer_ip)
4047
+ and not _is_self_peer_ip(_br_turn_peer_ip,
4048
+ _br_turn_peer_port)
4006
4049
  else None
4007
4050
  )
4008
4051
 
@@ -4110,7 +4153,9 @@ class _SdesOpenMixin:
4110
4153
  if _br_turn_peer_ip and _br_prefer_direct_stun.get(_bs, False):
4111
4154
  pass
4112
4155
  elif (_br_turn_peer_ip and _bs in _relay_addrs
4113
- and not _is_self_peer_ip(_br_turn_peer_ip)):
4156
+ and not _is_self_peer_ip(
4157
+ _br_turn_peer_ip,
4158
+ _br_turn_peer_port)):
4114
4159
  # Arrived via TURN - respond via Send Indication
4115
4160
  _bri = _relay_addrs[_bs]
4116
4161
  _br_t_host, _br_t_port = _bri[4], _bri[5]
@@ -4143,7 +4188,8 @@ class _SdesOpenMixin:
4143
4188
  + _br_si_body
4144
4189
  )
4145
4190
  _bs.sendto(_br_send_ind, (_br_t_host, _br_t_port))
4146
- elif _br_turn_peer_ip and _is_self_peer_ip(_br_turn_peer_ip):
4191
+ elif _br_turn_peer_ip and _is_self_peer_ip(
4192
+ _br_turn_peer_ip, _br_turn_peer_port):
4147
4193
  _bridge_selfloop_drop_count += 1
4148
4194
  if (_bridge_selfloop_drop_count <= 5
4149
4195
  or _bridge_selfloop_drop_count % 50 == 0):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0b2"
7
+ version = "1.0.0b3"
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.0b2
3
+ Version: 1.0.0b3
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
@@ -119,6 +119,7 @@ tests/test_keyframe_prompter.py
119
119
  tests/test_lan_control.py
120
120
  tests/test_lan_retry_reaches_the_storming_devices.py
121
121
  tests/test_light_active_color_mode.py
122
+ tests/test_live_session_carries_talk.py
122
123
  tests/test_live_stream_param.py
123
124
  tests/test_live_validate_policy.py
124
125
  tests/test_logging_caps.py
@@ -176,6 +177,7 @@ tests/test_sdes_watchdog.py
176
177
  tests/test_sdp_narrowing_properties.py
177
178
  tests/test_sdp_payload_type_narrowing.py
178
179
  tests/test_security_hardening.py
180
+ tests/test_self_peer_is_an_address_not_an_ip.py
179
181
  tests/test_serializable_login_info.py
180
182
  tests/test_serve_loss_tolerance.py
181
183
  tests/test_serve_relay.py
@@ -79,6 +79,34 @@ def test_ptz_is_not_run_when_the_session_is_already_closed():
79
79
  assert "closed" in out["ptz_error"]
80
80
 
81
81
 
82
+ def test_the_snapshot_reports_how_long_it_took():
83
+ # The SDES snapshot budget has been wrong twice and both times the only
84
+ # evidence was a verdict, so the replacement budget was a guess. Reporting
85
+ # the elapsed time makes the next runs answer it.
86
+ class _Snapper(_DeviceClient):
87
+ async def async_snapshot(self, path, timeout=10.0):
88
+ return False # verdict is irrelevant here; the timing is not
89
+
90
+ out = asyncio.run(probe_features(_Snapper(), {}, _Session(alive=True)))
91
+ assert "snapshot_s" in out
92
+ assert isinstance(out["snapshot_s"], float)
93
+
94
+
95
+ def test_the_snapshot_budget_clears_the_slowest_measured_snapshot():
96
+ # Measured across the fleet: SDES 17.2-23.6 s, DTLS 2.8-3.0 s. Two earlier
97
+ # budgets were set just above the then-known maximum and both timed out a
98
+ # camera afterwards, so this asserts headroom rather than a value.
99
+ from feature_probe import _snapshot_budget
100
+
101
+ slowest_sdes_seen = 23.6
102
+ sdes = _snapshot_budget({"modelId": "LK.IPC.A001064"}, 10.0)
103
+ assert sdes >= slowest_sdes_seen * 1.5, (
104
+ "an SDES budget without real headroom is how this failed twice")
105
+ # DTLS is a different path and an order of magnitude faster; widening it
106
+ # would only slow down reporting a camera that is genuinely broken.
107
+ assert _snapshot_budget({"modelId": "LK.IPC.A000088"}, 10.0) == 10.0
108
+
109
+
82
110
  def test_a_session_that_does_not_publish_liveness_counts_as_live():
83
111
  # Only SdesSession has is_alive - it tracks the ffmpeg the bridge dies with.
84
112
  # The DTLS session keeps no ffmpeg and publishes nothing, and absence of the
@@ -14,7 +14,11 @@ from aidot_cameras.camera.sdes_open import (
14
14
  ADVERTISED = [("192.168.100.13", 40000)]
15
15
 
16
16
 
17
- def _never_self(_ip):
17
+ def _never_self(_ip, _port=None):
18
+ # The predicate takes a transport address, not an address: a peer behind
19
+ # our own NAT shares our public IP on a different port, and refusing it on
20
+ # the IP alone stalled real streams. See
21
+ # test_self_peer_is_an_address_not_an_ip.py.
18
22
  return False
19
23
 
20
24
 
@@ -53,8 +57,8 @@ def test_our_own_address_is_never_learned():
53
57
 
54
58
  Nominating ourselves would form a pair that can never carry camera media.
55
59
  """
56
- def _is_self(ip):
57
- return ip == "192.168.0.110"
60
+ def _is_self(ip, port=None):
61
+ return (ip, port) == ("192.168.0.110", 3478)
58
62
 
59
63
  out = _record_peer_reflexive(
60
64
  ADVERTISED, [], ("192.168.0.110", 3478), _is_self)
@@ -0,0 +1,113 @@
1
+ """The SDES live-view session is opened talk-capable, so talk reuses it.
2
+
3
+ `async_speak` reuses `_stream_session` when it is talk-capable and otherwise
4
+ opens a session of its own. None of the loops that set `_stream_session` opened
5
+ with `talk=True`, and on SDES a session that did not ask for talk never gets a
6
+ return audio track - so the reuse branch was unreachable and every press-to-talk
7
+ opened a SECOND camera session alongside the live view. The camera holds a viewer
8
+ slot for about 120 s after a session ends, so that is not free.
9
+
10
+ The offer change this rests on is not new code: the release harness has opened
11
+ every camera talk-capable across three fleet runs and seven cameras with no
12
+ streaming regression. What is new is the keepalive loop asking for it.
13
+
14
+ Two things are locked here, because either alone would rot:
15
+ - the SDES keepalive loop asks for talk;
16
+ - `async_speak` really does reuse a talk-capable live session instead of
17
+ opening another one.
18
+ """
19
+ import asyncio
20
+ import inspect
21
+ import os
22
+ import sys
23
+
24
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
25
+
26
+ import aidot_cameras.camera.client as cc
27
+
28
+
29
+ def _fn_source(qualname: str) -> str:
30
+ for cls in vars(cc).values():
31
+ if isinstance(cls, type) and qualname in cls.__dict__:
32
+ return inspect.getsource(cls.__dict__[qualname])
33
+ raise AssertionError(f"{qualname} not found")
34
+
35
+
36
+ def test_the_sdes_keepalive_loop_opens_talk_capable():
37
+ src = _fn_source("_sdes_keepalive_loop_inner")
38
+ # The loop has exactly one async_open_webrtc_stream call; it must ask for
39
+ # talk, or the session it publishes as _stream_session cannot carry any.
40
+ assert src.count("async_open_webrtc_stream") == 1, (
41
+ "this test assumes a single open call in the loop - re-read it if that "
42
+ "changed")
43
+ assert "talk=True" in src, (
44
+ "the live-view session must be talk-capable or async_speak can never "
45
+ "reuse it")
46
+
47
+
48
+ class _Session:
49
+ """A live, talk-capable session that records what was asked of it."""
50
+
51
+ talk_supported = True
52
+
53
+ def __init__(self):
54
+ self.started = 0
55
+ self.stopped = 0
56
+
57
+ async def async_start_talk(self, provider):
58
+ self.started += 1
59
+ while provider() is not None:
60
+ pass # drain the clip as the real pump would
61
+ return True
62
+
63
+ async def async_stop_talk(self):
64
+ return True
65
+
66
+ async def stop(self):
67
+ self.stopped += 1
68
+
69
+
70
+ class _Client:
71
+ """Just enough of the device client for async_speak's reuse branch."""
72
+
73
+ device_id = "cam"
74
+
75
+ def __init__(self, session):
76
+ self._stream_session = session
77
+ self.opens = 0
78
+
79
+ async def async_open_webrtc_stream(self, *a, **kw):
80
+ self.opens += 1
81
+ raise AssertionError("async_speak must not open a second session")
82
+
83
+ async_speak = cc.CameraMixin.async_speak
84
+
85
+
86
+ def test_async_speak_reuses_the_live_session_instead_of_opening_another():
87
+ session = _Session()
88
+ client = _Client(session)
89
+ frames = iter([b"\x00" * 320] * 3)
90
+
91
+ ok = asyncio.run(client.async_speak(lambda: next(frames, None),
92
+ max_seconds=5))
93
+
94
+ assert ok is True
95
+ assert client.opens == 0 # no second camera session
96
+ assert session.started == 1
97
+ # And it must not close the session it borrowed - the live view is using it.
98
+ assert session.stopped == 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ import traceback
103
+ _fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
104
+ _fail = 0
105
+ for _fn in _fns:
106
+ try:
107
+ _fn()
108
+ print(f"PASS {_fn.__name__}")
109
+ except Exception:
110
+ _fail += 1
111
+ print(f"FAIL {_fn.__name__}")
112
+ traceback.print_exc()
113
+ raise SystemExit(1 if _fail else 0)
@@ -0,0 +1,101 @@
1
+ """A camera behind our own NAT is not us, and refusing it stalls the stream.
2
+
3
+ Seven first-media stall reports name `vetoed-self-ip` (see ROAD-TO-1.0 item 3).
4
+ The shape is always the same: the camera's connectivity check reaches us as a
5
+ TURN Data Indication whose XOR-PEER-ADDRESS is THIS host's public IP on a port
6
+ that is not ours - because the camera sits behind the same NAT, on another
7
+ subnet of the same house. `_is_self_peer_ip` compared the address alone, so it
8
+ answered "that is us".
9
+
10
+ Two things then break, and the second is the stall:
11
+
12
+ * the camera's address is never learned as a peer-reflexive candidate;
13
+ * the Binding Success Response is never sent back through the relay, because
14
+ the branch that wraps it in a Send Indication is guarded by the same check.
15
+
16
+ With no response, the camera's check never completes, ICE never succeeds, the
17
+ AVIO LIVING trigger is never armed and the camera never sends a byte. That is
18
+ exactly `binding-success=0; trigger=not-sent` with no media.
19
+
20
+ The safety property this check exists for is unchanged and must stay unchanged:
21
+ nominating OUR OWN address would have us answer our own check. Our own address
22
+ is one ip:PORT. A peer sharing our NAT has the same ip and a different port, so
23
+ comparing the pair keeps the guard and drops the false positive - which is also
24
+ what ICE means by a transport address.
25
+ """
26
+ import os
27
+ import sys
28
+
29
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
+
31
+ from aidot_cameras.camera.sdes_open import (
32
+ _is_self_transport_address,
33
+ _record_peer_reflexive,
34
+ )
35
+
36
+ LOCAL = "192.168.0.50"
37
+ PUBLIC = "173.53.36.206"
38
+ OURS = (41000, 41002) # the ports our own srflx candidates advertise
39
+
40
+
41
+ def _is_self(ip, port=None):
42
+ return _is_self_transport_address(
43
+ ip, port, local_ip=LOCAL, public_ip=PUBLIC, own_ports=OURS)
44
+
45
+
46
+ def test_our_own_reflexive_address_is_still_refused():
47
+ # The guard's whole purpose. Both advertised ports, exactly as offered.
48
+ assert _is_self(PUBLIC, 41000) is True
49
+ assert _is_self(PUBLIC, 41002) is True
50
+
51
+
52
+ def test_loopback_and_this_hosts_lan_address_are_refused_on_any_port():
53
+ for ip in ("127.0.0.1", "0.0.0.0", LOCAL):
54
+ assert _is_self(ip, 41000) is True
55
+ assert _is_self(ip, 59999) is True
56
+
57
+
58
+ def test_a_camera_sharing_our_public_ip_on_another_port_is_not_us():
59
+ # The measured case: XOR-PEER-ADDRESS is our public IP, port 49887, which
60
+ # is the port the camera advertised on its own host candidate.
61
+ assert _is_self(PUBLIC, 49887) is False
62
+ assert _is_self(PUBLIC, 46836) is False
63
+
64
+
65
+ def test_an_unknown_port_still_refuses_the_public_ip():
66
+ # Call sites that cannot supply a port keep the old, conservative answer.
67
+ # Widening those blind would be guessing, and the guard is the safe default.
68
+ assert _is_self(PUBLIC, None) is True
69
+
70
+
71
+ def test_an_unrelated_address_is_never_us():
72
+ assert _is_self("54.144.38.43", 5349) is False
73
+
74
+
75
+ def test_the_learner_uses_the_port_too():
76
+ # _record_peer_reflexive is where the address becomes a nomination target,
77
+ # so it has to ask the same question the same way.
78
+ advertised = [("192.168.100.3", 34400)]
79
+
80
+ learned = _record_peer_reflexive(
81
+ advertised, [], (PUBLIC, 49887), _is_self)
82
+ assert learned == [(PUBLIC, 49887)], "a peer behind our NAT must be learnable"
83
+
84
+ refused = _record_peer_reflexive(
85
+ advertised, [], (PUBLIC, 41000), _is_self)
86
+ assert refused == [], "our own advertised address must never be learned"
87
+
88
+
89
+ if __name__ == "__main__":
90
+ import traceback
91
+ _fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
92
+ _fail = 0
93
+ for _fn in _fns:
94
+ try:
95
+ _fn()
96
+ print(f"PASS {_fn.__name__}")
97
+ except Exception:
98
+ _fail += 1
99
+ print(f"FAIL {_fn.__name__}")
100
+ traceback.print_exc()
101
+ raise SystemExit(1 if _fail else 0)