python-aidot-cameras 0.11.12__tar.gz → 0.11.14__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.11.12/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.14}/PKG-INFO +1 -1
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/client.py +23 -1
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/sdes_open.py +43 -4
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/device_client.py +15 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/pyproject.toml +1 -1
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/python_aidot_cameras.egg-info/SOURCES.txt +3 -0
- python_aidot_cameras-0.11.14/tests/test_dtls_serve_open_timeout.py +75 -0
- python_aidot_cameras-0.11.14/tests/test_light_active_color_mode.py +58 -0
- python_aidot_cameras-0.11.14/tests/test_sdes_bridge_break_gate.py +229 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/LICENSE +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/README.md +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/__main__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/constants.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/controls.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/models.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/playback.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/protocol.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/sdes.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/tutk.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/client.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/const.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/discover.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/g711.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/login_const.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/__init__.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/device_client_model.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/device_model.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/discover_model.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/py.typed +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/setup.cfg +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.11.12/python_aidot_cameras.egg-info → python_aidot_cameras-0.11.14}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.14
|
|
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
|
|
@@ -117,6 +117,26 @@ _SLOW_PROBE_LOG_EVERY = int(os.environ.get("AIDOT_DTLS_SLOW_PROBE_LOG_EVERY", "6
|
|
|
117
117
|
# asyncio.sleep(interval) call, so stop() is not delayed by up to 10 minutes.
|
|
118
118
|
_SLOW_PROBE_SLEEP_CHUNK_S = float(os.environ.get("AIDOT_DTLS_SLOW_PROBE_CHUNK_S", "5"))
|
|
119
119
|
|
|
120
|
+
|
|
121
|
+
def _parse_env_float(name: str, default: float) -> float:
|
|
122
|
+
"""Parse a float env var, falling back to ``default`` on any missing or
|
|
123
|
+
malformed value (matches the try/except idiom used by
|
|
124
|
+
_get_webrtc_open_gate / _get_stream_slots below for AIDOT_MAX_CONCURRENT_*
|
|
125
|
+
rather than raising at import time)."""
|
|
126
|
+
try:
|
|
127
|
+
return float(os.environ.get(name, str(default)))
|
|
128
|
+
except (ValueError, TypeError):
|
|
129
|
+
return default
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# DTLS serve-open timeout: how long a single async_open_webrtc_stream() call
|
|
133
|
+
# in the serve loop is allowed to take before giving up. Left at the
|
|
134
|
+
# _async_open_webrtc_stream_impl default (30.0s) so battery cameras that need
|
|
135
|
+
# the full window to wake still work; tunable via
|
|
136
|
+
# AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S for operators who want faster-failing
|
|
137
|
+
# retries against a known-dead camera.
|
|
138
|
+
_DTLS_SERVE_OPEN_TIMEOUT_S = _parse_env_float("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", 30.0)
|
|
139
|
+
|
|
120
140
|
# Strong refs to fire-and-forget tasks: asyncio only keeps weak refs, so a
|
|
121
141
|
# discarded task can be garbage-collected mid-flight. Discarded on completion.
|
|
122
142
|
_BG_TASKS: set = set()
|
|
@@ -3499,7 +3519,9 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
3499
3519
|
return
|
|
3500
3520
|
_last_open_at = loop.time()
|
|
3501
3521
|
try:
|
|
3502
|
-
session = await self.async_open_webrtc_stream(
|
|
3522
|
+
session = await self.async_open_webrtc_stream(
|
|
3523
|
+
on_frame=lambda _f: None, timeout=_DTLS_SERVE_OPEN_TIMEOUT_S,
|
|
3524
|
+
)
|
|
3503
3525
|
except asyncio.CancelledError:
|
|
3504
3526
|
return
|
|
3505
3527
|
except AidotCameraBusy as busy:
|
|
@@ -63,6 +63,24 @@ def _classify_ffmpeg_exit(rc: int, teardown_requested: bool) -> int:
|
|
|
63
63
|
return logging.WARNING
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def _bridge_should_break(rc, teardown_requested: bool) -> bool:
|
|
67
|
+
"""Whether the bridge observe loop should end on this poll() result.
|
|
68
|
+
|
|
69
|
+
rc is the held proc's poll() result (None while it is still running).
|
|
70
|
+
Ending the loop is only correct when ffmpeg has actually exited AND no
|
|
71
|
+
locally-initiated teardown is in flight for it: a non-None rc seen while
|
|
72
|
+
teardown_requested is True is the key-restart window (or any other
|
|
73
|
+
flagged local kill) - the OLD proc's exit, not a reason to stop. That
|
|
74
|
+
window always resolves promptly one of two ways, so skipping the break
|
|
75
|
+
here cannot hang the bridge thread: either the key-restart repoints
|
|
76
|
+
_proc_holder[0] at the live new proc (next poll() is None again), or a
|
|
77
|
+
genuine teardown closes the loopback sockets, which makes the loop's
|
|
78
|
+
select() raise and exit via its own except-break. Pure function so the
|
|
79
|
+
policy is unit-testable without a live bridge thread.
|
|
80
|
+
"""
|
|
81
|
+
return rc is not None and not teardown_requested
|
|
82
|
+
|
|
83
|
+
|
|
66
84
|
class _SdesOpenMixin:
|
|
67
85
|
async def _open_sdes_stream(self, **kwargs) -> "SdesSession":
|
|
68
86
|
"""Allocate-and-hand-off wrapper around _open_sdes_stream_impl.
|
|
@@ -2145,6 +2163,13 @@ class _SdesOpenMixin:
|
|
|
2145
2163
|
_trigger_bsrc = None # camera addr for trigger
|
|
2146
2164
|
_sdes_probe_received = False # True after first 0xC8 probe from camera
|
|
2147
2165
|
_last_hb_ts = 0.0 # time of last AVIO HEARTBEAT send
|
|
2166
|
+
# One-shot guard for the "ffmpeg exited" log below: while the held
|
|
2167
|
+
# proc keeps reporting the same stale exit code across a
|
|
2168
|
+
# teardown-window skip (see _bridge_should_break), only the first
|
|
2169
|
+
# tick logs it. Reset to False whenever the held proc is next
|
|
2170
|
+
# seen alive (poll() None), so a later, genuine exit of THAT proc
|
|
2171
|
+
# still gets its own line.
|
|
2172
|
+
_br_exit_logged = False
|
|
2148
2173
|
_lo_a = _socket_br.socket(_socket_br.AF_INET, _socket_br.SOCK_DGRAM)
|
|
2149
2174
|
_lo_v = _socket_br.socket(_socket_br.AF_INET, _socket_br.SOCK_DGRAM)
|
|
2150
2175
|
try:
|
|
@@ -2155,15 +2180,24 @@ class _SdesOpenMixin:
|
|
|
2155
2180
|
)
|
|
2156
2181
|
except Exception:
|
|
2157
2182
|
break
|
|
2158
|
-
# Stop the bridge when ffmpeg exits (normal end or crash)
|
|
2183
|
+
# Stop the bridge when ffmpeg exits (normal end or crash) -
|
|
2184
|
+
# but NOT on a stale exit seen during a flagged teardown
|
|
2185
|
+
# window (key-restart proc replace, stop(), _reap(), the
|
|
2186
|
+
# DTLS-fallback abort): _bridge_should_break() skips the
|
|
2187
|
+
# break there, and the loop resolves it either via the
|
|
2188
|
+
# key-restart's _proc_holder[0] repoint (next poll() None,
|
|
2189
|
+
# below) or via a genuine teardown closing the loopback
|
|
2190
|
+
# sockets, which raises out of the select() above.
|
|
2159
2191
|
_br_proc = _proc_holder[0]
|
|
2160
2192
|
if _br_proc is not None:
|
|
2161
2193
|
_br_rc = _br_proc.poll()
|
|
2162
2194
|
if _br_rc is not None:
|
|
2163
|
-
|
|
2195
|
+
_br_teardown_requested = bool(_teardown_holder[0])
|
|
2196
|
+
if _br_rc != 0 and not _br_exit_logged:
|
|
2197
|
+
_br_exit_logged = True
|
|
2164
2198
|
import logging as _log_br
|
|
2165
2199
|
_br_level = _classify_ffmpeg_exit(
|
|
2166
|
-
_br_rc,
|
|
2200
|
+
_br_rc, _br_teardown_requested
|
|
2167
2201
|
)
|
|
2168
2202
|
_br_msg = (
|
|
2169
2203
|
"SDES bridge: ffmpeg exited with code %d"
|
|
@@ -2175,7 +2209,12 @@ class _SdesOpenMixin:
|
|
|
2175
2209
|
_log_br.getLogger(__name__).log(
|
|
2176
2210
|
_br_level, _br_msg, _br_rc
|
|
2177
2211
|
)
|
|
2178
|
-
|
|
2212
|
+
if _bridge_should_break(
|
|
2213
|
+
_br_rc, _br_teardown_requested
|
|
2214
|
+
):
|
|
2215
|
+
break
|
|
2216
|
+
else:
|
|
2217
|
+
_br_exit_logged = False
|
|
2179
2218
|
|
|
2180
2219
|
# AVIO HEARTBEAT (cmd=5156) every 10s, sent as an ENCRYPTED SCTP
|
|
2181
2220
|
# DATA chunk (PPID=53) - exactly like LIVING - NOT a raw 0xC8 AVIO
|
|
@@ -56,6 +56,7 @@ class DeviceStatusData:
|
|
|
56
56
|
rgbw: tuple[int, int, int, int] = (255, 0, 0, 0)
|
|
57
57
|
cct: int = 2700
|
|
58
58
|
dimming: int = 100
|
|
59
|
+
active_color_mode: "str | None" = None
|
|
59
60
|
|
|
60
61
|
def update(self, attr: DeviceAttr) -> None:
|
|
61
62
|
"""Update status from DeviceAttr model."""
|
|
@@ -82,6 +83,20 @@ class DeviceStatusData:
|
|
|
82
83
|
if attr.CCT is not None:
|
|
83
84
|
self.cct = attr.CCT
|
|
84
85
|
|
|
86
|
+
# Track which color mode is actually active. Bulbs report state as
|
|
87
|
+
# deltas: a CCT-mode push carries only CCT, an RGB-mode push carries
|
|
88
|
+
# only RGBW. The getDevAttr login-sync instead returns BOTH the
|
|
89
|
+
# retained RGBW register and CCT together, which is ambiguous about
|
|
90
|
+
# which mode is active; that case (and RGBW==0, which is just the
|
|
91
|
+
# register's power-on default rather than a real color pick) must
|
|
92
|
+
# update the values above without changing active_color_mode.
|
|
93
|
+
rgbw_present = attr.RGBW is not None
|
|
94
|
+
cct_present = attr.CCT is not None
|
|
95
|
+
if rgbw_present and not cct_present and attr.RGBW != 0:
|
|
96
|
+
self.active_color_mode = "rgbw"
|
|
97
|
+
elif cct_present and not rgbw_present:
|
|
98
|
+
self.active_color_mode = "cct"
|
|
99
|
+
|
|
85
100
|
|
|
86
101
|
class DeviceInformation:
|
|
87
102
|
"""Static device metadata and capability flags parsed from the cloud record."""
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.11.
|
|
7
|
+
version = "0.11.14"
|
|
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"
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.14
|
|
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
|
|
@@ -80,6 +80,7 @@ tests/test_device_user_info_cache.py
|
|
|
80
80
|
tests/test_dtls_not_ready_burst.py
|
|
81
81
|
tests/test_dtls_pinning.py
|
|
82
82
|
tests/test_dtls_serve_decode_skip.py
|
|
83
|
+
tests/test_dtls_serve_open_timeout.py
|
|
83
84
|
tests/test_dtls_skip_signaling_wait.py
|
|
84
85
|
tests/test_dtls_slow_probe.py
|
|
85
86
|
tests/test_egress_guard.py
|
|
@@ -91,6 +92,7 @@ tests/test_ice_config_cache.py
|
|
|
91
92
|
tests/test_keepalive_renew.py
|
|
92
93
|
tests/test_keyframe_prompter.py
|
|
93
94
|
tests/test_lan_control.py
|
|
95
|
+
tests/test_light_active_color_mode.py
|
|
94
96
|
tests/test_live_stream_param.py
|
|
95
97
|
tests/test_logging_caps.py
|
|
96
98
|
tests/test_motion_poll.py
|
|
@@ -106,6 +108,7 @@ tests/test_playback_tls.py
|
|
|
106
108
|
tests/test_post_merge_hardening.py
|
|
107
109
|
tests/test_retry_policy.py
|
|
108
110
|
tests/test_sdes_adaptive.py
|
|
111
|
+
tests/test_sdes_bridge_break_gate.py
|
|
109
112
|
tests/test_sdes_echo_wait_timeout.py
|
|
110
113
|
tests/test_sdes_fast_liveplay.py
|
|
111
114
|
tests/test_sdes_ffmpeg_stderr_log.py
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S: env-tunable timeout for the DTLS serve
|
|
2
|
+
loop's async_open_webrtc_stream call (aidot/camera/client.py, ~line 3502).
|
|
3
|
+
|
|
4
|
+
Without a ``timeout`` kwarg that call is hard-pinned to the 30.0s default in
|
|
5
|
+
_async_open_webrtc_stream_impl (webrtc_open.py ~146-151); a dead/wedged DTLS
|
|
6
|
+
camera then burns a full 30s per open attempt. This makes it tunable while
|
|
7
|
+
keeping the default unchanged (battery cameras can legitimately need the full
|
|
8
|
+
30s to wake).
|
|
9
|
+
|
|
10
|
+
Repo convention: no pytest-asyncio; drive coroutines with asyncio.run().
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
import types
|
|
14
|
+
|
|
15
|
+
import aidot.camera.client as camera_client
|
|
16
|
+
from aidot.camera.client import CameraMixin, _parse_env_float
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# --- _parse_env_float: the try/except-fallback idiom used elsewhere in this
|
|
20
|
+
# module for numeric AIDOT_* env vars (see _get_webrtc_open_gate /
|
|
21
|
+
# _get_stream_slots' AIDOT_MAX_CONCURRENT_OPENS / _STREAMS handling). ------- #
|
|
22
|
+
|
|
23
|
+
def test_parse_env_float_defaults_when_unset(monkeypatch):
|
|
24
|
+
monkeypatch.delenv("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", raising=False)
|
|
25
|
+
assert _parse_env_float("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", 30.0) == 30.0
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_parse_env_float_override(monkeypatch):
|
|
29
|
+
monkeypatch.setenv("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", "12.5")
|
|
30
|
+
assert _parse_env_float("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", 30.0) == 12.5
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_parse_env_float_malformed_falls_back_to_default(monkeypatch):
|
|
34
|
+
monkeypatch.setenv("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", "not-a-number")
|
|
35
|
+
assert _parse_env_float("AIDOT_DTLS_SERVE_OPEN_TIMEOUT_S", 30.0) == 30.0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_module_constant_defaults_to_30():
|
|
39
|
+
# Guards the preserved-behavior requirement: unset env -> 30.0, matching
|
|
40
|
+
# _async_open_webrtc_stream_impl's prior hard-pinned default exactly.
|
|
41
|
+
assert camera_client._DTLS_SERVE_OPEN_TIMEOUT_S == 30.0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# --- Nice-to-have: the serve loop passes the configured timeout through ---- #
|
|
45
|
+
|
|
46
|
+
class _Ready:
|
|
47
|
+
def clear(self):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def set(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_dtls_serve_loop_passes_configured_timeout(monkeypatch):
|
|
55
|
+
calls = []
|
|
56
|
+
|
|
57
|
+
class _Fake:
|
|
58
|
+
_streaming_active = True
|
|
59
|
+
_keepalive_rtsp_url = "http://127.0.0.1:8999/cam.ts"
|
|
60
|
+
device_id = "TESTCAM"
|
|
61
|
+
_serve_ready = _Ready()
|
|
62
|
+
|
|
63
|
+
async def async_open_webrtc_stream(self, *a, **k):
|
|
64
|
+
calls.append(k)
|
|
65
|
+
raise asyncio.CancelledError
|
|
66
|
+
|
|
67
|
+
fake = _Fake()
|
|
68
|
+
|
|
69
|
+
async def _run():
|
|
70
|
+
await types.MethodType(CameraMixin._dtls_serve_loop_inner, fake)()
|
|
71
|
+
|
|
72
|
+
asyncio.run(_run())
|
|
73
|
+
|
|
74
|
+
assert calls, "async_open_webrtc_stream was never called"
|
|
75
|
+
assert calls[0].get("timeout") == camera_client._DTLS_SERVE_OPEN_TIMEOUT_S
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for DeviceStatusData.active_color_mode tracking.
|
|
2
|
+
|
|
3
|
+
AiDot RGBW+CCT bulbs report state as deltas: a CCT-mode push carries only
|
|
4
|
+
CCT (RGBW is None); an RGB-mode push carries only RGBW (CCT is None). But
|
|
5
|
+
the getDevAttr login-sync returns BOTH the retained RGBW register and CCT
|
|
6
|
+
together, which is ambiguous about which mode is actually active. This
|
|
7
|
+
must update the retained values but must NOT change active_color_mode.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aidot.device_client import DeviceStatusData
|
|
11
|
+
from aidot.models.device_client_model import DeviceAttr
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_cct_only_delta_sets_cct_mode():
|
|
15
|
+
status = DeviceStatusData()
|
|
16
|
+
status.update(DeviceAttr(OnOff=1, Dimming=100, CCT=3000))
|
|
17
|
+
assert status.active_color_mode == "cct"
|
|
18
|
+
assert status.cct == 3000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_rgbw_only_delta_sets_rgbw_mode():
|
|
22
|
+
status = DeviceStatusData()
|
|
23
|
+
status.update(DeviceAttr(RGBW=0x00FF0000))
|
|
24
|
+
assert status.active_color_mode == "rgbw"
|
|
25
|
+
assert status.rgbw == (0, 255, 0, 0)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_ambiguous_sync_does_not_change_mode_from_unknown():
|
|
29
|
+
# Starting from a fresh (unknown/None) mode, an ambiguous sync carrying
|
|
30
|
+
# both RGBW and CCT must update the values but leave the mode None.
|
|
31
|
+
status = DeviceStatusData()
|
|
32
|
+
assert status.active_color_mode is None
|
|
33
|
+
status.update(DeviceAttr(OnOff=1, Dimming=100, RGBW=-1660909312, CCT=3000))
|
|
34
|
+
assert status.active_color_mode is None
|
|
35
|
+
assert status.rgbw == (157, 0, 137, 0)
|
|
36
|
+
assert status.cct == 3000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_ambiguous_sync_does_not_revert_established_mode():
|
|
40
|
+
status = DeviceStatusData()
|
|
41
|
+
# Unambiguous CCT-only delta establishes "cct".
|
|
42
|
+
status.update(DeviceAttr(OnOff=1, Dimming=100, CCT=3000))
|
|
43
|
+
assert status.active_color_mode == "cct"
|
|
44
|
+
# A subsequent ambiguous sync (both RGBW and CCT present) must NOT flip
|
|
45
|
+
# the mode back to "rgbw", even though RGBW is present in the payload.
|
|
46
|
+
status.update(DeviceAttr(OnOff=1, Dimming=100, RGBW=-1660909312, CCT=3000))
|
|
47
|
+
assert status.active_color_mode == "cct"
|
|
48
|
+
assert status.rgbw == (157, 0, 137, 0)
|
|
49
|
+
assert status.cct == 3000
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_rgbw_zero_only_does_not_set_rgbw_mode():
|
|
53
|
+
status = DeviceStatusData()
|
|
54
|
+
status.update(DeviceAttr(RGBW=0))
|
|
55
|
+
assert status.active_color_mode is None
|
|
56
|
+
# Existing default-red value behavior must be unchanged.
|
|
57
|
+
assert status.rgdb == 0xFF000000
|
|
58
|
+
assert status.rgbw == (255, 0, 0, 0)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Gate the SDES bridge observe-loop break on the teardown flag.
|
|
2
|
+
|
|
3
|
+
During a key-restart (sdes_open.py's SRTP-key-mismatch branch, ~3680-3739) the
|
|
4
|
+
OLD ffmpeg proc is terminated and the shared teardown flag is set BEFORE the
|
|
5
|
+
NEW proc is spawned and _proc_holder[0] is repointed to it. If the bridge
|
|
6
|
+
thread's observe-loop tick lands in that window, it used to see the OLD
|
|
7
|
+
proc's non-None exit code and break unconditionally - tearing down the bridge
|
|
8
|
+
loop, closing the loopback sockets, and starving the freshly restarted
|
|
9
|
+
ffmpeg (NO_MEDIA until the liveness watchdog forces a 40-60s reconnect).
|
|
10
|
+
|
|
11
|
+
The break decision is extracted into a pure helper, _bridge_should_break(rc,
|
|
12
|
+
teardown_requested), so the policy is unit-testable in isolation; the loop
|
|
13
|
+
itself is embedded in a large closure inside _open_sdes_stream_impl that
|
|
14
|
+
cannot be invoked standalone, so the composed loop-level tests below drive a
|
|
15
|
+
small mirror of the loop's shape (select-gated wait / proc-holder poll / the
|
|
16
|
+
pre-existing except-break on socket close) using the real, imported
|
|
17
|
+
_bridge_should_break to make the decision - the same function the production
|
|
18
|
+
loop calls - plus a source-inspection check that the production loop really
|
|
19
|
+
does call it, so the mirror cannot silently drift from the real code.
|
|
20
|
+
"""
|
|
21
|
+
import inspect
|
|
22
|
+
import select
|
|
23
|
+
import socket
|
|
24
|
+
|
|
25
|
+
import aidot.camera.sdes_open as sdes_open
|
|
26
|
+
from aidot.camera.sdes_open import _bridge_should_break
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# --------------------------------------------------------------------- #
|
|
30
|
+
# Pure helper truth table
|
|
31
|
+
# --------------------------------------------------------------------- #
|
|
32
|
+
|
|
33
|
+
def test_break_on_clean_exit_without_teardown():
|
|
34
|
+
assert _bridge_should_break(0, False) is True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_break_on_signal_death_without_teardown():
|
|
38
|
+
assert _bridge_should_break(-9, False) is True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_no_break_on_signal_death_during_teardown():
|
|
42
|
+
assert _bridge_should_break(-9, True) is False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_no_break_on_clean_exit_during_teardown():
|
|
46
|
+
assert _bridge_should_break(0, True) is False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_no_break_while_still_running():
|
|
50
|
+
assert _bridge_should_break(None, False) is False
|
|
51
|
+
assert _bridge_should_break(None, True) is False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --------------------------------------------------------------------- #
|
|
55
|
+
# Source-structure guard: the real loop must call the pure helper, not
|
|
56
|
+
# reimplement (or bypass) the decision inline.
|
|
57
|
+
# --------------------------------------------------------------------- #
|
|
58
|
+
|
|
59
|
+
def test_production_loop_calls_the_shared_helper():
|
|
60
|
+
src = inspect.getsource(sdes_open)
|
|
61
|
+
start = src.index("def _bridge_fn():")
|
|
62
|
+
end = src.index("def _bridge_fn():", start + 1) if src.count(
|
|
63
|
+
"def _bridge_fn():"
|
|
64
|
+
) > 1 else len(src)
|
|
65
|
+
block = src[start:end]
|
|
66
|
+
assert "_bridge_should_break(" in block, (
|
|
67
|
+
"the bridge observe loop must delegate the break decision to "
|
|
68
|
+
"_bridge_should_break so the policy stays unit-testable and in sync"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------- #
|
|
73
|
+
# Loop-level behavior with fakes
|
|
74
|
+
# --------------------------------------------------------------------- #
|
|
75
|
+
|
|
76
|
+
class _FakeProc:
|
|
77
|
+
"""Minimal Popen stand-in: poll() returns the fixed rc once "exited"."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, rc):
|
|
80
|
+
self._rc = rc
|
|
81
|
+
|
|
82
|
+
def poll(self):
|
|
83
|
+
return self._rc
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _run_observe_loop(sock_a, sock_b, proc_holder, teardown_holder,
|
|
87
|
+
max_ticks=100):
|
|
88
|
+
"""Mirror of the sdes_open.py bridge observe loop's shape.
|
|
89
|
+
|
|
90
|
+
Same structure as the production loop: a select()-gated wait whose
|
|
91
|
+
except clause breaks (the socket-close escape hatch), then a proc poll
|
|
92
|
+
that only breaks when the real _bridge_should_break helper says so, with
|
|
93
|
+
an exit-code log fired at most once per skip window. Returns
|
|
94
|
+
(outcome, ticks, log_count) so tests can assert on all three without any
|
|
95
|
+
real sleeping or threading - select's timeout is kept tiny since the
|
|
96
|
+
real 0.5s constant is not itself under test here.
|
|
97
|
+
"""
|
|
98
|
+
exit_logged = False
|
|
99
|
+
log_count = 0
|
|
100
|
+
for tick in range(1, max_ticks + 1):
|
|
101
|
+
try:
|
|
102
|
+
select.select([sock_a, sock_b], [], [], 0.01)
|
|
103
|
+
except Exception:
|
|
104
|
+
return "closed", tick, log_count
|
|
105
|
+
proc = proc_holder[0]
|
|
106
|
+
if proc is not None:
|
|
107
|
+
rc = proc.poll()
|
|
108
|
+
if rc is not None:
|
|
109
|
+
if rc != 0 and not exit_logged:
|
|
110
|
+
exit_logged = True
|
|
111
|
+
log_count += 1
|
|
112
|
+
if _bridge_should_break(rc, bool(teardown_holder[0])):
|
|
113
|
+
return "broke", tick, log_count
|
|
114
|
+
continue
|
|
115
|
+
exit_logged = False
|
|
116
|
+
return "exhausted", max_ticks, log_count
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _udp_pair():
|
|
120
|
+
a = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
121
|
+
b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
122
|
+
return a, b
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_teardown_window_exit_does_not_break_the_loop():
|
|
126
|
+
# Old proc reports a signal death while teardown is flagged (the
|
|
127
|
+
# key-restart window) - the loop must NOT break on that tick.
|
|
128
|
+
a, b = _udp_pair()
|
|
129
|
+
try:
|
|
130
|
+
proc_holder = [_FakeProc(-9)]
|
|
131
|
+
teardown_holder = [True]
|
|
132
|
+
outcome, ticks, log_count = _run_observe_loop(
|
|
133
|
+
a, b, proc_holder, teardown_holder, max_ticks=3
|
|
134
|
+
)
|
|
135
|
+
assert outcome == "exhausted"
|
|
136
|
+
assert log_count == 1 # logged once, not suppressed entirely
|
|
137
|
+
finally:
|
|
138
|
+
a.close()
|
|
139
|
+
b.close()
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_repoint_to_live_proc_resumes_normal_operation():
|
|
143
|
+
# Same window, but the key-restart repoints _proc_holder[0] to the new,
|
|
144
|
+
# live proc after the first tick - the loop must keep running (not
|
|
145
|
+
# break) and must not re-log once the new proc is polling clean.
|
|
146
|
+
a, b = _udp_pair()
|
|
147
|
+
try:
|
|
148
|
+
proc_holder = [_FakeProc(-9)]
|
|
149
|
+
teardown_holder = [True]
|
|
150
|
+
|
|
151
|
+
exit_logged = False
|
|
152
|
+
log_count = 0
|
|
153
|
+
outcomes = []
|
|
154
|
+
for i in range(4):
|
|
155
|
+
if i == 1:
|
|
156
|
+
proc_holder[0] = _FakeProc(None) # key-restart repoint
|
|
157
|
+
try:
|
|
158
|
+
select.select([a, b], [], [], 0.01)
|
|
159
|
+
except Exception:
|
|
160
|
+
outcomes.append("closed")
|
|
161
|
+
break
|
|
162
|
+
proc = proc_holder[0]
|
|
163
|
+
rc = proc.poll()
|
|
164
|
+
if rc is not None:
|
|
165
|
+
if rc != 0 and not exit_logged:
|
|
166
|
+
exit_logged = True
|
|
167
|
+
log_count += 1
|
|
168
|
+
if _bridge_should_break(rc, bool(teardown_holder[0])):
|
|
169
|
+
outcomes.append("broke")
|
|
170
|
+
break
|
|
171
|
+
else:
|
|
172
|
+
exit_logged = False
|
|
173
|
+
outcomes.append("continued")
|
|
174
|
+
assert outcomes == ["continued", "continued", "continued", "continued"]
|
|
175
|
+
assert log_count == 1
|
|
176
|
+
finally:
|
|
177
|
+
a.close()
|
|
178
|
+
b.close()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_genuine_teardown_closes_sockets_and_exits_via_except_break():
|
|
182
|
+
# No key-restart repoint arrives; instead a real teardown (stop(), the
|
|
183
|
+
# DTLS-fallback abort, or _reap()) closes the loopback sockets - select()
|
|
184
|
+
# must raise and the loop must exit via the pre-existing except-break.
|
|
185
|
+
a, b = _udp_pair()
|
|
186
|
+
proc_holder = [_FakeProc(-9)]
|
|
187
|
+
teardown_holder = [True]
|
|
188
|
+
|
|
189
|
+
a.close()
|
|
190
|
+
b.close()
|
|
191
|
+
|
|
192
|
+
outcome, ticks, log_count = _run_observe_loop(
|
|
193
|
+
a, b, proc_holder, teardown_holder, max_ticks=10
|
|
194
|
+
)
|
|
195
|
+
assert outcome == "closed"
|
|
196
|
+
assert ticks == 1
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_genuine_crash_without_teardown_still_breaks():
|
|
200
|
+
# No teardown in flight: an unexpected ffmpeg crash must still break the
|
|
201
|
+
# loop immediately, exactly as before this fix.
|
|
202
|
+
a, b = _udp_pair()
|
|
203
|
+
try:
|
|
204
|
+
proc_holder = [_FakeProc(-9)]
|
|
205
|
+
teardown_holder = [False]
|
|
206
|
+
outcome, ticks, log_count = _run_observe_loop(
|
|
207
|
+
a, b, proc_holder, teardown_holder, max_ticks=10
|
|
208
|
+
)
|
|
209
|
+
assert outcome == "broke"
|
|
210
|
+
assert ticks == 1
|
|
211
|
+
assert log_count == 1
|
|
212
|
+
finally:
|
|
213
|
+
a.close()
|
|
214
|
+
b.close()
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def test_log_fires_at_most_once_across_a_multi_tick_skip_window():
|
|
218
|
+
a, b = _udp_pair()
|
|
219
|
+
try:
|
|
220
|
+
proc_holder = [_FakeProc(-9)]
|
|
221
|
+
teardown_holder = [True]
|
|
222
|
+
outcome, ticks, log_count = _run_observe_loop(
|
|
223
|
+
a, b, proc_holder, teardown_holder, max_ticks=20
|
|
224
|
+
)
|
|
225
|
+
assert outcome == "exhausted"
|
|
226
|
+
assert log_count == 1
|
|
227
|
+
finally:
|
|
228
|
+
a.close()
|
|
229
|
+
b.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/base.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/g711.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/g722.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/h264.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/opus.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/codecs/vpx.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/contrib/media.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/exceptions.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/jitterbuffer.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/mediastreams.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcdatachannel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtpreceiver.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/_vendor/aiortc/rtcrtpsender.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/device_client_model.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/aidot/models/discover_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_dtls_skip_signaling_wait.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_ice_config_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_keyframe_prompter.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_live_stream_param.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_open_fail_logger.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_ffmpeg_stderr_log.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_idle_release.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_open_cleanup.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_serve_audio.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_serializable_login_info.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_stream_drain_ownership.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.11.12 → python_aidot_cameras-0.11.14}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|