python-aidot-cameras 0.12.17__tar.gz → 0.13.0__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.
- {python_aidot_cameras-0.12.17/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.0}/PKG-INFO +2 -2
- python_aidot_cameras-0.13.0/aidot_cameras/_upstream.py +396 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/sdes_open.py +79 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/webrtc_open.py +1 -1
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/client.py +243 -65
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/const.py +16 -3
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/crypto.py +12 -6
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/device_client.py +90 -24
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/discover.py +36 -12
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/pyproject.toml +15 -4
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0/python_aidot_cameras.egg-info}/PKG-INFO +2 -2
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/SOURCES.txt +5 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/requires.txt +1 -1
- python_aidot_cameras-0.13.0/tests/test_constants_imports_resolve.py +62 -0
- python_aidot_cameras-0.13.0/tests/test_ice_peer_reflexive.py +92 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_light_active_color_mode.py +4 -6
- python_aidot_cameras-0.13.0/tests/test_live_validate_policy.py +281 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_post_merge_hardening.py +37 -20
- python_aidot_cameras-0.13.0/tests/test_shared_house_seam.py +98 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_token_refresh.py +34 -20
- python_aidot_cameras-0.13.0/tests/test_upstream_compat.py +568 -0
- python_aidot_cameras-0.12.17/tests/test_upstream_compat.py +0 -268
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/LICENSE +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/README.md +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/client.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_battery_detection.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_battery_skip_turn.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dormant_camera_cleanup.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_mux_terminal_write.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_idle_release_both_loops.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_key_restart_sdp.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_no_self_referential_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_peerid_reuse.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_stream_slot_cap.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_turn_create_permission.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_viewer_check_throttle.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_viewer_presence.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_wait_or_event.py +0 -0
- {python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/tests/test_wake_readiness_retry.py +0 -0
{python_aidot_cameras-0.12.17/python_aidot_cameras.egg-info → python_aidot_cameras-0.13.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
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
|
|
@@ -11,7 +11,7 @@ Classifier: Operating System :: OS Independent
|
|
|
11
11
|
Requires-Python: >=3.11
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: python-aidot
|
|
14
|
+
Requires-Dist: python-aidot<0.4,>=0.3.55
|
|
15
15
|
Requires-Dist: aiohttp>=3.9
|
|
16
16
|
Requires-Dist: paho-mqtt>=1.6.1
|
|
17
17
|
Requires-Dist: cryptography>=42.0
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""One place that knows which upstream ``python-aidot`` is installed.
|
|
2
|
+
|
|
3
|
+
Upstream ships two incompatible shapes of the same private API, and both are
|
|
4
|
+
live: Home Assistant core pins ``0.3.56`` while this library was written against
|
|
5
|
+
``0.3.55``. They are not a forward/backward pair - ``0.3.56`` is a **revert** of
|
|
6
|
+
the refactor that ``0.3.54`` introduced and ``0.3.55`` carried, so the "old"
|
|
7
|
+
shape is also the current one:
|
|
8
|
+
|
|
9
|
+
0.3.53 dict-based client, ``aes_utils``, ``login_const`` <- shape A
|
|
10
|
+
0.3.54 typed dataclasses, ``api/``, ``utils/crypto`` <- shape B
|
|
11
|
+
0.3.55 shape B
|
|
12
|
+
0.3.56 shape A again (plus the ``models/`` package)
|
|
13
|
+
|
|
14
|
+
Because upstream has flip-flopped once, neither shape can be assumed permanent,
|
|
15
|
+
so both are supported rather than tracked. Every difference is resolved here,
|
|
16
|
+
once, at import time; no other module in this package may branch on the upstream
|
|
17
|
+
version. If you find yourself adding a second ``try: import aidot...`` anywhere
|
|
18
|
+
else, put it here instead.
|
|
19
|
+
|
|
20
|
+
The detection is by capability (does the name import?), never by parsing a
|
|
21
|
+
version string: upstream shipped shape A under both ``0.3.53`` and ``0.3.56``
|
|
22
|
+
with a five-day excursion in between, so a version comparison would encode the
|
|
23
|
+
excursion rather than the shape.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from enum import IntEnum
|
|
27
|
+
from typing import Any, Optional
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------------- #
|
|
30
|
+
# Shape detection
|
|
31
|
+
# --------------------------------------------------------------------------- #
|
|
32
|
+
|
|
33
|
+
try: # shape B (0.3.54 - 0.3.55): typed account/device dataclasses + CloudApi
|
|
34
|
+
from aidot.models.auth_model import UserInformation as _UserInformation
|
|
35
|
+
HAS_TYPED_ACCOUNT = True
|
|
36
|
+
except ImportError: # shape A (<=0.3.53, >=0.3.56): plain dicts
|
|
37
|
+
_UserInformation = None
|
|
38
|
+
HAS_TYPED_ACCOUNT = False
|
|
39
|
+
|
|
40
|
+
#: True when upstream exposes the typed account layer (``user_info``,
|
|
41
|
+
#: ``_cloud_api``, ``UserInformation``). Prefer the named helpers below to
|
|
42
|
+
#: branching on this directly.
|
|
43
|
+
UPSTREAM_SHAPE = "typed" if HAS_TYPED_ACCOUNT else "dict"
|
|
44
|
+
|
|
45
|
+
#: Upstream's account record class, or ``None`` on shape A where the account is
|
|
46
|
+
#: a plain dict. Only used as a type annotation and an isinstance guard.
|
|
47
|
+
UserInformation = _UserInformation
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# --------------------------------------------------------------------------- #
|
|
51
|
+
# Constants: four moved from ``aidot.const`` to ``aidot.login_const``
|
|
52
|
+
# --------------------------------------------------------------------------- #
|
|
53
|
+
|
|
54
|
+
try: # shape B
|
|
55
|
+
from aidot.const import (
|
|
56
|
+
API_URL_TEMPLATE,
|
|
57
|
+
APP_ID,
|
|
58
|
+
DEFAULT_REGION,
|
|
59
|
+
PUBLIC_KEY_PEM,
|
|
60
|
+
)
|
|
61
|
+
except ImportError: # shape A
|
|
62
|
+
from aidot.login_const import (
|
|
63
|
+
API_URL_TEMPLATE,
|
|
64
|
+
APP_ID,
|
|
65
|
+
DEFAULT_REGION,
|
|
66
|
+
PUBLIC_KEY_PEM,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --------------------------------------------------------------------------- #
|
|
71
|
+
# Crypto: ``aidot.utils.crypto`` -> ``aidot.aes_utils``
|
|
72
|
+
# --------------------------------------------------------------------------- #
|
|
73
|
+
|
|
74
|
+
try: # shape B
|
|
75
|
+
from aidot.utils.crypto import (
|
|
76
|
+
aes_decrypt,
|
|
77
|
+
aes_decrypt_to_json,
|
|
78
|
+
aes_encrypt,
|
|
79
|
+
)
|
|
80
|
+
except ImportError: # shape A
|
|
81
|
+
from aidot.aes_utils import (
|
|
82
|
+
aes_decrypt,
|
|
83
|
+
aes_decrypt_to_json,
|
|
84
|
+
aes_encrypt,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def rsa_encrypt(message: str, public_key: Any = None) -> str:
|
|
89
|
+
"""RSA/PKCS1v15 encrypt ``message``, returned base64.
|
|
90
|
+
|
|
91
|
+
Implemented here rather than re-exported. Shape B had
|
|
92
|
+
``aidot.utils.crypto.rsa_encrypt(message, public_key)``; shape A replaced it
|
|
93
|
+
with ``aidot.client.rsa_password_encrypt(message)``, which bakes the key in
|
|
94
|
+
and takes one argument. Neither signature is available on both shapes, and
|
|
95
|
+
``aidot_cameras.crypto.rsa_encrypt`` is public surface the integration repo
|
|
96
|
+
may import (see the back-compat rule in CLAUDE.md), so the two-argument form
|
|
97
|
+
is kept and satisfied locally. ``public_key`` defaults to upstream's
|
|
98
|
+
``PUBLIC_KEY_PEM``, which is the only key any caller ever passed.
|
|
99
|
+
"""
|
|
100
|
+
from cryptography.hazmat.backends import default_backend
|
|
101
|
+
from cryptography.hazmat.primitives import serialization
|
|
102
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
103
|
+
import base64
|
|
104
|
+
|
|
105
|
+
if public_key is None:
|
|
106
|
+
public_key = PUBLIC_KEY_PEM
|
|
107
|
+
loaded = serialization.load_pem_public_key(
|
|
108
|
+
public_key.encode() if isinstance(public_key, str) else public_key,
|
|
109
|
+
backend=default_backend(),
|
|
110
|
+
)
|
|
111
|
+
encrypted = loaded.encrypt(message.encode("utf-8"), padding.PKCS1v15())
|
|
112
|
+
return base64.b64encode(encrypted).decode("utf-8")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# --------------------------------------------------------------------------- #
|
|
116
|
+
# Device record: ``DeviceModel`` (shape B) / ``DeviceInformation`` (shape A)
|
|
117
|
+
# --------------------------------------------------------------------------- #
|
|
118
|
+
|
|
119
|
+
try: # shape B
|
|
120
|
+
from aidot.models.device_model import DeviceModel as DeviceRecord
|
|
121
|
+
except ImportError: # shape A
|
|
122
|
+
from aidot.models.device_model import DeviceInformation as DeviceRecord
|
|
123
|
+
|
|
124
|
+
#: The typed cloud device record. Both shapes expose ``from_json`` /
|
|
125
|
+
#: ``to_dict`` and the core field set this package reads (``id``, ``modelId``,
|
|
126
|
+
#: ``aesKey``, ``password``, ``product``); shape A drops ~20 fields shape B
|
|
127
|
+
#: carried and defaults everything to ``None`` rather than ``""``/empty, so
|
|
128
|
+
#: never rely on a nested default being present - check for ``None`` first.
|
|
129
|
+
DeviceModel = DeviceRecord
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# --------------------------------------------------------------------------- #
|
|
133
|
+
# DeviceState: upstream's LAN session enum, deleted in shape A
|
|
134
|
+
# --------------------------------------------------------------------------- #
|
|
135
|
+
|
|
136
|
+
try: # shape B
|
|
137
|
+
from aidot.device_client import DeviceState as _DeviceState
|
|
138
|
+
DEVICE_STATE_IS_UPSTREAMS = True
|
|
139
|
+
except ImportError: # shape A: no state machine, just two booleans
|
|
140
|
+
DEVICE_STATE_IS_UPSTREAMS = False
|
|
141
|
+
|
|
142
|
+
class _DeviceState(IntEnum):
|
|
143
|
+
"""Stand-in for upstream's deleted ``DeviceState``.
|
|
144
|
+
|
|
145
|
+
Shape A tracks the LAN session with ``_connecting`` /
|
|
146
|
+
``_connect_and_login`` booleans instead of an enum. The enum is still
|
|
147
|
+
re-exported from ``aidot_cameras.device_client`` because the Home
|
|
148
|
+
Assistant integration annotates and compares against it, and dropping a
|
|
149
|
+
public name outright would break it at import.
|
|
150
|
+
|
|
151
|
+
Members and values are copied from 0.3.55 so a stored or hard-coded
|
|
152
|
+
comparison keeps its meaning. On shape A nothing upstream ever
|
|
153
|
+
*produces* one of these - use ``device_session_authenticated()`` rather
|
|
154
|
+
than comparing a client's private state against it.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
IDLE = 0
|
|
158
|
+
INITIALIZING = 1
|
|
159
|
+
CONNECTING = 2
|
|
160
|
+
CONNECTED = 3
|
|
161
|
+
CONNECTION_FAILED = 4
|
|
162
|
+
AUTHENTICATING = 5
|
|
163
|
+
AUTHENTICATED = 6
|
|
164
|
+
AUTHENTICATION_FAILED = 7
|
|
165
|
+
DISCONNECTED = 8
|
|
166
|
+
|
|
167
|
+
DeviceState = _DeviceState
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def device_session_authenticated(device_client: Any) -> bool:
|
|
171
|
+
"""Whether a device client's LAN session is logged in, on either shape.
|
|
172
|
+
|
|
173
|
+
Shape B exposes ``_state == DeviceState.AUTHENTICATED``; shape A replaced
|
|
174
|
+
that with the ``connect_and_login`` property. Consumers that only want the
|
|
175
|
+
answer should call this instead of touching either private attribute.
|
|
176
|
+
"""
|
|
177
|
+
if DEVICE_STATE_IS_UPSTREAMS:
|
|
178
|
+
return getattr(device_client, "_state", None) == DeviceState.AUTHENTICATED
|
|
179
|
+
return bool(getattr(device_client, "connect_and_login", False))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# --------------------------------------------------------------------------- #
|
|
183
|
+
# Reconnect timer handle: ``_reconnect_timer`` -> ``_reconnect_handle``
|
|
184
|
+
# --------------------------------------------------------------------------- #
|
|
185
|
+
|
|
186
|
+
#: Attribute names a pending reconnect may be parked under, newest first.
|
|
187
|
+
#: ``CameraClient.async_close`` cancels whichever exists so a closed account
|
|
188
|
+
#: cannot re-open a TCP connection ~45 s later (upstream cancels neither).
|
|
189
|
+
RECONNECT_HANDLE_ATTRS = ("_reconnect_handle", "_reconnect_timer")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def cancel_pending_reconnect(device_client: Any) -> bool:
|
|
193
|
+
"""Cancel a device client's pending reconnect, whichever shape it uses.
|
|
194
|
+
|
|
195
|
+
Returns True if something was cancelled. Deliberately not a silent
|
|
196
|
+
``getattr(x, "_reconnect_timer", None)``: that spelling exists only on shape
|
|
197
|
+
B, so on shape A it would find nothing, cancel nothing, and report success -
|
|
198
|
+
the leak fix would go quiet instead of failing. Callers that care should
|
|
199
|
+
log the False case.
|
|
200
|
+
"""
|
|
201
|
+
for attr in RECONNECT_HANDLE_ATTRS:
|
|
202
|
+
handle = getattr(device_client, attr, None)
|
|
203
|
+
if handle is not None:
|
|
204
|
+
handle.cancel()
|
|
205
|
+
return True
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --------------------------------------------------------------------------- #
|
|
210
|
+
# Account HTTP surface: ``CloudApi`` (shape B) -> client methods (shape A)
|
|
211
|
+
# --------------------------------------------------------------------------- #
|
|
212
|
+
|
|
213
|
+
async def api_get_houses(client: Any) -> Optional[list]:
|
|
214
|
+
"""List the account's houses."""
|
|
215
|
+
if HAS_TYPED_ACCOUNT:
|
|
216
|
+
return await client._cloud_api.get_houses()
|
|
217
|
+
return await client.async_get_houses()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
async def api_get_devices(client: Any, house_id: str) -> Optional[list]:
|
|
221
|
+
"""List one house's devices."""
|
|
222
|
+
if HAS_TYPED_ACCOUNT:
|
|
223
|
+
return await client._cloud_api.get_devices(house_id)
|
|
224
|
+
return await client.async_get_devices(house_id)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def api_get_products(client: Any, product_ids: str) -> Optional[list]:
|
|
228
|
+
"""Fetch product metadata for a comma-joined id list."""
|
|
229
|
+
if HAS_TYPED_ACCOUNT:
|
|
230
|
+
return await client._cloud_api.get_products(product_ids)
|
|
231
|
+
return await client.async_get_products(product_ids)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
async def api_refresh_token(client: Any) -> Any:
|
|
235
|
+
"""Exchange the refresh token for a new access token.
|
|
236
|
+
|
|
237
|
+
Both shapes return the response body and raise ``AidotAuthFailed`` when the
|
|
238
|
+
refresh token itself is rejected; shape B additionally returns ``None`` on
|
|
239
|
+
some non-auth failures, so callers must treat a falsy return as "no token".
|
|
240
|
+
"""
|
|
241
|
+
if HAS_TYPED_ACCOUNT:
|
|
242
|
+
return await client._cloud_api.refresh_token()
|
|
243
|
+
return await client.async_refresh_token()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# --------------------------------------------------------------------------- #
|
|
247
|
+
# Account fields: ``user_info`` dataclass (shape B) -> plain attrs (shape A)
|
|
248
|
+
# --------------------------------------------------------------------------- #
|
|
249
|
+
|
|
250
|
+
def account_region(client: Any) -> str:
|
|
251
|
+
"""The account's API region ("us", "eu", ...)."""
|
|
252
|
+
if HAS_TYPED_ACCOUNT:
|
|
253
|
+
return getattr(client.user_info, "region", "") or ""
|
|
254
|
+
return getattr(client, "_region", "") or ""
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def account_refresh_token(client: Any) -> str:
|
|
258
|
+
"""The stored refresh token, or "" when there is none."""
|
|
259
|
+
if HAS_TYPED_ACCOUNT:
|
|
260
|
+
return getattr(client.user_info, "refreshToken", "") or ""
|
|
261
|
+
from .const import CONF_REFRESH_TOKEN
|
|
262
|
+
return (client.login_info or {}).get(CONF_REFRESH_TOKEN) or ""
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def account_token_ttl(client: Any, default: int = 7200) -> int:
|
|
266
|
+
"""Access-token lifetime in seconds, defaulting to the server's 7200."""
|
|
267
|
+
if HAS_TYPED_ACCOUNT:
|
|
268
|
+
raw = getattr(client.user_info, "expiresIn", 0)
|
|
269
|
+
else:
|
|
270
|
+
raw = (client.login_info or {}).get("expiresIn", 0)
|
|
271
|
+
try:
|
|
272
|
+
return int(raw or default)
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
return default
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def account_record(client: Any) -> Any:
|
|
278
|
+
"""What to pass as a device client's second constructor argument.
|
|
279
|
+
|
|
280
|
+
Shape B wants the ``UserInformation`` dataclass; shape A wants the raw
|
|
281
|
+
``login_info`` dict. See ``device_client_args``.
|
|
282
|
+
"""
|
|
283
|
+
if HAS_TYPED_ACCOUNT:
|
|
284
|
+
return client.user_info
|
|
285
|
+
return client.login_info
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def device_client_args(
|
|
289
|
+
device_record: Any,
|
|
290
|
+
raw_device: dict,
|
|
291
|
+
typed_account: Any,
|
|
292
|
+
login_info: Optional[dict],
|
|
293
|
+
) -> tuple:
|
|
294
|
+
"""The ``(device, user_info)`` pair upstream's ``DeviceClient`` expects.
|
|
295
|
+
|
|
296
|
+
Shape B: ``(DeviceModel, UserInformation)`` - typed dataclasses.
|
|
297
|
+
Shape A: ``(dict, dict)`` - the raw cloud records, natively.
|
|
298
|
+
|
|
299
|
+
Shape A taking raw dicts is why this package carried ``raw_device`` /
|
|
300
|
+
``login_info`` alongside the typed models in the first place: the typed
|
|
301
|
+
round trip drops every camera-only field. On shape A the originals go
|
|
302
|
+
straight through and nothing is lost.
|
|
303
|
+
|
|
304
|
+
Takes the four values rather than the account client, because the only
|
|
305
|
+
caller (``CameraDeviceClient.__init__``) is handed them directly and has no
|
|
306
|
+
client reference. ``typed_account`` may be a ``UserInformation`` or, from
|
|
307
|
+
an older caller, already a dict - on shape A a dict ``login_info`` wins and
|
|
308
|
+
a dict ``typed_account`` is the fallback, so both call styles work.
|
|
309
|
+
"""
|
|
310
|
+
if HAS_TYPED_ACCOUNT:
|
|
311
|
+
return (device_record, typed_account)
|
|
312
|
+
account = login_info if isinstance(login_info, dict) else None
|
|
313
|
+
if account is None:
|
|
314
|
+
account = typed_account if isinstance(typed_account, dict) else {}
|
|
315
|
+
if not isinstance(typed_account, dict) and hasattr(typed_account, "to_dict"):
|
|
316
|
+
account = typed_account.to_dict()
|
|
317
|
+
return (raw_device, account)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
#: True when upstream's ``DeviceClient`` exposes a ``read_data`` seam - one
|
|
321
|
+
#: decrypted frame at a time, which this package hooks to recover camera-only
|
|
322
|
+
#: attribute keys that upstream's typed ``DeviceAttr`` model drops. Shape A
|
|
323
|
+
#: inlines the read into ``receive_data``, so there is no seam and the raw JSON
|
|
324
|
+
#: never escapes. See ``CameraDeviceClient._notify_status_update``.
|
|
325
|
+
try:
|
|
326
|
+
from aidot.device_client import DeviceClient as _DeviceClient
|
|
327
|
+
HAS_READ_DATA_SEAM = callable(getattr(_DeviceClient, "read_data", None))
|
|
328
|
+
except ImportError: # pragma: no cover - upstream is a hard dependency
|
|
329
|
+
HAS_READ_DATA_SEAM = False
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# --------------------------------------------------------------------------- #
|
|
333
|
+
# Discovery: static ``Discover`` (shape B) -> instance ``Discover`` (shape A)
|
|
334
|
+
# --------------------------------------------------------------------------- #
|
|
335
|
+
|
|
336
|
+
#: True when upstream's ``Discover`` keeps discovered addresses in the
|
|
337
|
+
#: process-wide ``DISCOVERED_DEVICE`` class dict. Shape A made discovery
|
|
338
|
+
#: instance-scoped (``self._discover.discovered_device``), which is the shape
|
|
339
|
+
#: this package's ``CameraDiscover`` already presents - so on shape A the map
|
|
340
|
+
#: no longer has to be shared through the base class to reach upstream's
|
|
341
|
+
#: ``get_device_client``.
|
|
342
|
+
try:
|
|
343
|
+
from aidot.discover import Discover as _Discover
|
|
344
|
+
HAS_SHARED_DISCOVERY_MAP = isinstance(
|
|
345
|
+
getattr(_Discover, "DISCOVERED_DEVICE", None), dict
|
|
346
|
+
)
|
|
347
|
+
except ImportError: # pragma: no cover - upstream is a hard dependency
|
|
348
|
+
HAS_SHARED_DISCOVERY_MAP = False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def broadcast_protocol_args(callback: Any, user_id: str) -> tuple:
|
|
352
|
+
"""Positional args for upstream's ``BroadcastProtocol.__init__``.
|
|
353
|
+
|
|
354
|
+
Shape B takes ``(callback)`` and has no notion of the account id; shape A
|
|
355
|
+
takes ``(callback, user_id)`` and uses it to build the request payload.
|
|
356
|
+
"""
|
|
357
|
+
import inspect
|
|
358
|
+
from aidot.discover import BroadcastProtocol
|
|
359
|
+
|
|
360
|
+
params = list(inspect.signature(BroadcastProtocol.__init__).parameters)
|
|
361
|
+
return (callback, user_id) if "user_id" in params else (callback,)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
#: Names this module intends as its public surface within the package.
|
|
365
|
+
__all__ = [
|
|
366
|
+
"API_URL_TEMPLATE",
|
|
367
|
+
"APP_ID",
|
|
368
|
+
"DEFAULT_REGION",
|
|
369
|
+
"DEVICE_STATE_IS_UPSTREAMS",
|
|
370
|
+
"HAS_READ_DATA_SEAM",
|
|
371
|
+
"HAS_SHARED_DISCOVERY_MAP",
|
|
372
|
+
"HAS_TYPED_ACCOUNT",
|
|
373
|
+
"PUBLIC_KEY_PEM",
|
|
374
|
+
"RECONNECT_HANDLE_ATTRS",
|
|
375
|
+
"UPSTREAM_SHAPE",
|
|
376
|
+
"DeviceModel",
|
|
377
|
+
"DeviceRecord",
|
|
378
|
+
"DeviceState",
|
|
379
|
+
"UserInformation",
|
|
380
|
+
"account_record",
|
|
381
|
+
"account_refresh_token",
|
|
382
|
+
"account_region",
|
|
383
|
+
"account_token_ttl",
|
|
384
|
+
"aes_decrypt",
|
|
385
|
+
"aes_decrypt_to_json",
|
|
386
|
+
"aes_encrypt",
|
|
387
|
+
"api_get_devices",
|
|
388
|
+
"api_get_houses",
|
|
389
|
+
"api_get_products",
|
|
390
|
+
"api_refresh_token",
|
|
391
|
+
"broadcast_protocol_args",
|
|
392
|
+
"cancel_pending_reconnect",
|
|
393
|
+
"device_client_args",
|
|
394
|
+
"device_session_authenticated",
|
|
395
|
+
"rsa_encrypt",
|
|
396
|
+
]
|
{python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -181,6 +181,46 @@ def _bridge_should_break(rc, teardown_requested: bool) -> bool:
|
|
|
181
181
|
return rc is not None and not teardown_requested
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
# Cap on peer-reflexive candidates learned from inbound probes. A camera has
|
|
185
|
+
# one working path; a handful of source addresses is normal churn (NAT rebind,
|
|
186
|
+
# audio vs video socket), anything beyond that is noise and must not be allowed
|
|
187
|
+
# to grow the nomination set without bound.
|
|
188
|
+
_MAX_PRFLX_CANDS = 4
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _record_peer_reflexive(known, discovered, observed, is_self=None):
|
|
192
|
+
"""Learn the address a STUN probe actually arrived from.
|
|
193
|
+
|
|
194
|
+
ICE calls this a peer-reflexive candidate (RFC 8445 s7.3.1.3): the camera
|
|
195
|
+
is reachable at the source of its own connectivity check whether or not it
|
|
196
|
+
ever advertised that address. We were nominating only what the answer SDP
|
|
197
|
+
listed, which fails whenever the listed address is not reachable from here
|
|
198
|
+
- a camera on a foreign subnet advertises a private candidate we have no
|
|
199
|
+
route to, the nomination goes into a black hole, and the session sits in
|
|
200
|
+
ICE "Checking" while the camera's probes keep arriving from an address we
|
|
201
|
+
never nominate. Observed on the A001064 PTZ while it was on a separate
|
|
202
|
+
192.168.100.0/24: it advertised 192.168.100.13 as its only candidate.
|
|
203
|
+
|
|
204
|
+
Returns the new discovered list, or ``discovered`` unchanged when there is
|
|
205
|
+
nothing to learn. Callers must REBIND rather than mutate in place - the
|
|
206
|
+
bridge thread iterates this list and must never see it change under it.
|
|
207
|
+
|
|
208
|
+
Pure function so the policy is unit-testable without a camera.
|
|
209
|
+
"""
|
|
210
|
+
if not observed:
|
|
211
|
+
return discovered
|
|
212
|
+
_ip, _port = observed
|
|
213
|
+
if not _ip or not _port:
|
|
214
|
+
return discovered
|
|
215
|
+
if is_self is not None and is_self(_ip):
|
|
216
|
+
return discovered
|
|
217
|
+
if observed in known or observed in discovered:
|
|
218
|
+
return discovered
|
|
219
|
+
if len(discovered) >= _MAX_PRFLX_CANDS:
|
|
220
|
+
return discovered
|
|
221
|
+
return [*discovered, observed]
|
|
222
|
+
|
|
223
|
+
|
|
184
224
|
class _SdesOpenMixin:
|
|
185
225
|
async def _open_sdes_stream(self, **kwargs) -> "SdesSession":
|
|
186
226
|
"""Allocate-and-hand-off wrapper around _open_sdes_stream_impl.
|
|
@@ -2940,6 +2980,31 @@ class _SdesOpenMixin:
|
|
|
2940
2980
|
f" {_bsrc[0]}:{_bsrc[1]}"
|
|
2941
2981
|
f" attrs=[{', '.join(_attrs)}]"
|
|
2942
2982
|
)
|
|
2983
|
+
# Learn where this probe actually came from. The
|
|
2984
|
+
# camera reached us from here, so this address is
|
|
2985
|
+
# known-good in a way an advertised one is not.
|
|
2986
|
+
# Relay-carried probes carry the camera's real
|
|
2987
|
+
# address in XOR-PEER-ADDRESS; _bsrc is the TURN
|
|
2988
|
+
# server and would be useless to nominate.
|
|
2989
|
+
_br_obs = (
|
|
2990
|
+
_br_cam_peer if _br_cam_peer
|
|
2991
|
+
else (None if _bsrc[0] == _hp_host else _bsrc)
|
|
2992
|
+
)
|
|
2993
|
+
_br_prflx_was = _bridge_uc_info["prflx"]
|
|
2994
|
+
_br_prflx_now = _record_peer_reflexive(
|
|
2995
|
+
_bridge_uc_info["cands"], _br_prflx_was,
|
|
2996
|
+
_br_obs, _is_self_peer_ip,
|
|
2997
|
+
)
|
|
2998
|
+
if _br_prflx_now is not _br_prflx_was:
|
|
2999
|
+
# Rebind, never append: the nomination tick
|
|
3000
|
+
# iterates this list.
|
|
3001
|
+
_bridge_uc_info["prflx"] = _br_prflx_now
|
|
3002
|
+
_status(
|
|
3003
|
+
f"ICE: learned peer-reflexive camera"
|
|
3004
|
+
f" candidate {_br_obs[0]}:{_br_obs[1]}"
|
|
3005
|
+
f" (not in the {len(_bridge_uc_info['cands'])}"
|
|
3006
|
+
f" advertised candidate(s)) - will nominate it"
|
|
3007
|
+
)
|
|
2943
3008
|
try:
|
|
2944
3009
|
if _br_turn_peer_ip is None and _bsrc[0] != _hp_host:
|
|
2945
3010
|
_br_prefer_direct_stun[_bs] = True
|
|
@@ -3814,6 +3879,16 @@ class _SdesOpenMixin:
|
|
|
3814
3879
|
# Fall back to the creds parsed late into _bridge_uc_info so this
|
|
3815
3880
|
# ungated periodic tick still nominates them. [SDES-LATECREDS-FIX]
|
|
3816
3881
|
_uc_cands = _cam_ice_cands or _bridge_uc_info.get("cands")
|
|
3882
|
+
# Union in anything learned from the camera's own probes.
|
|
3883
|
+
# Advertised candidates stay FIRST and are still nominated,
|
|
3884
|
+
# so a camera we can already reach directly is unaffected;
|
|
3885
|
+
# this only adds a path where there was none.
|
|
3886
|
+
_uc_prflx = _bridge_uc_info.get("prflx")
|
|
3887
|
+
if _uc_prflx:
|
|
3888
|
+
_uc_cands = [
|
|
3889
|
+
*(_uc_cands or []),
|
|
3890
|
+
*(c for c in _uc_prflx if c not in (_uc_cands or [])),
|
|
3891
|
+
]
|
|
3817
3892
|
_uc_cufrag = _cam_ice_ufrag or _bridge_uc_info.get("ufrag")
|
|
3818
3893
|
_uc_cpwd = _cam_ice_pwd or _bridge_uc_info.get("pwd")
|
|
3819
3894
|
if _uc_cands and _uc_cufrag and _uc_cpwd and (_br_now - _br_last_uc) >= 2.5:
|
|
@@ -3884,6 +3959,10 @@ class _SdesOpenMixin:
|
|
|
3884
3959
|
"pwd": _cam_ice_pwd,
|
|
3885
3960
|
"cands": list(_cam_ice_cands),
|
|
3886
3961
|
"sent": bool(_cam_ice_cands), # True if already sent at setup
|
|
3962
|
+
# Addresses the camera's own probes arrived from, which may differ
|
|
3963
|
+
# from anything it advertised. Written by the bridge thread only;
|
|
3964
|
+
# read there too, so no cross-thread ordering to reason about.
|
|
3965
|
+
"prflx": [],
|
|
3887
3966
|
}
|
|
3888
3967
|
|
|
3889
3968
|
_bridge_thread = _threading_br.Thread(
|
{python_aidot_cameras-0.12.17 → python_aidot_cameras-0.13.0}/aidot_cameras/camera/webrtc_open.py
RENAMED
|
@@ -335,7 +335,7 @@ class _WebRTCOpenMixin:
|
|
|
335
335
|
# only the higher of the camera's consecutive [P, P+1] ICE ports (its
|
|
336
336
|
# live DTLS socket). aioice's aggressive nomination of BOTH ports made
|
|
337
337
|
# the camera latch the lower one and withhold DTLS - the dominant ~25%
|
|
338
|
-
# failure. A/B on
|
|
338
|
+
# failure. A/B on an A000088: high-port-only ~87% vs ~25% baseline vs
|
|
339
339
|
# ~12% low-port. Self-gating no-op for non-A000088 peers.
|
|
340
340
|
# See _install_highport_nomination_patch.
|
|
341
341
|
_install_highport_nomination_patch()
|