python-aidot-cameras 0.12.7__tar.gz → 0.12.9__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.7/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.9}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/__init__.py +2 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/client.py +86 -9
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/go2rtc.py +23 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/pyproject.toml +1 -1
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.12.9/tests/test_stream_slot_cap.py +80 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_upstream_compat.py +7 -0
- python_aidot_cameras-0.12.9/tests/test_viewer_presence.py +71 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/LICENSE +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/README.md +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/sdes_open.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.7/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.9}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.9
|
|
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
|
|
@@ -12,6 +12,7 @@ import time as _time
|
|
|
12
12
|
|
|
13
13
|
from .client import AidotClient, CameraClient
|
|
14
14
|
from .device_client import CameraDeviceClient, DeviceInformation, DeviceStatusData
|
|
15
|
+
from .camera.client import configure_stream_limits
|
|
15
16
|
from .discover import CameraDiscover, Discover
|
|
16
17
|
from .exceptions import (
|
|
17
18
|
AidotAuthFailed,
|
|
@@ -147,4 +148,5 @@ __all__ = [
|
|
|
147
148
|
"HTTPError",
|
|
148
149
|
"InvalidHost",
|
|
149
150
|
"InvalidURL",
|
|
151
|
+
"configure_stream_limits",
|
|
150
152
|
]
|
|
@@ -571,6 +571,51 @@ def _get_webrtc_open_gate() -> "asyncio.Semaphore":
|
|
|
571
571
|
|
|
572
572
|
|
|
573
573
|
_STREAM_SLOTS: "Optional[asyncio.Semaphore]" = None
|
|
574
|
+
_STREAM_SLOTS_CAP: int = 0
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _stream_slots_default() -> int:
|
|
578
|
+
"""The configured cap: an explicit env override, else the built-in default."""
|
|
579
|
+
try:
|
|
580
|
+
return max(1, int(os.environ.get("AIDOT_MAX_CONCURRENT_STREAMS", "3")))
|
|
581
|
+
except (ValueError, TypeError):
|
|
582
|
+
return 3
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def configure_stream_limits(max_streams: int) -> int:
|
|
586
|
+
"""Raise the concurrent-serve cap to fit the fleet; return the cap in force.
|
|
587
|
+
|
|
588
|
+
The default cap of 3 is a host-protection guard, but it silently STARVES an
|
|
589
|
+
account with more cameras than that: a camera holds its slot for the life of
|
|
590
|
+
its serve, so on a fleet of 4 DTLS cameras the fourth waits forever while the
|
|
591
|
+
other three are kept alive - it simply never streams, with no error anywhere.
|
|
592
|
+
Confirmed live on a 4-camera fleet, where the library logged "waiting for a
|
|
593
|
+
stream slot (cap reached)" for the odd one out on every attempt.
|
|
594
|
+
|
|
595
|
+
A consumer that knows how many cameras it is about to drive (the Home
|
|
596
|
+
Assistant integration knows its camera count) should call this so the cap is
|
|
597
|
+
never below the fleet size.
|
|
598
|
+
|
|
599
|
+
``AIDOT_MAX_CONCURRENT_STREAMS`` still wins when set: an operator who
|
|
600
|
+
deliberately capped a small host means it, and would rather have cameras take
|
|
601
|
+
turns than have the host fall over. The cap only ever grows - shrinking it
|
|
602
|
+
would strand a camera that already holds a slot.
|
|
603
|
+
"""
|
|
604
|
+
global _STREAM_SLOTS, _STREAM_SLOTS_CAP
|
|
605
|
+
if os.environ.get("AIDOT_MAX_CONCURRENT_STREAMS"):
|
|
606
|
+
_get_stream_slots() # ensure it exists so the cap is populated
|
|
607
|
+
return _STREAM_SLOTS_CAP
|
|
608
|
+
target = max(1, int(max_streams))
|
|
609
|
+
slots = _get_stream_slots()
|
|
610
|
+
if target > _STREAM_SLOTS_CAP:
|
|
611
|
+
for _ in range(target - _STREAM_SLOTS_CAP):
|
|
612
|
+
slots.release() # grows the semaphore's permit count
|
|
613
|
+
_LOGGER.info(
|
|
614
|
+
"concurrent-serve cap raised from %d to %d to fit the camera fleet",
|
|
615
|
+
_STREAM_SLOTS_CAP, target,
|
|
616
|
+
)
|
|
617
|
+
_STREAM_SLOTS_CAP = target
|
|
618
|
+
return _STREAM_SLOTS_CAP
|
|
574
619
|
|
|
575
620
|
|
|
576
621
|
def _get_stream_slots() -> "asyncio.Semaphore":
|
|
@@ -586,13 +631,10 @@ def _get_stream_slots() -> "asyncio.Semaphore":
|
|
|
586
631
|
-50002 SESSION_EXCEED terminal error, which we mirror separately). This cap
|
|
587
632
|
is an HA-specific resource guard: HA dashboards can request N cameras at once,
|
|
588
633
|
which a phone never does. Deliberate divergence, kept for host protection."""
|
|
589
|
-
global _STREAM_SLOTS
|
|
634
|
+
global _STREAM_SLOTS, _STREAM_SLOTS_CAP
|
|
590
635
|
if _STREAM_SLOTS is None:
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
except (ValueError, TypeError):
|
|
594
|
-
_n = 3
|
|
595
|
-
_STREAM_SLOTS = asyncio.Semaphore(_n)
|
|
636
|
+
_STREAM_SLOTS_CAP = _stream_slots_default()
|
|
637
|
+
_STREAM_SLOTS = asyncio.Semaphore(_STREAM_SLOTS_CAP)
|
|
596
638
|
return _STREAM_SLOTS
|
|
597
639
|
|
|
598
640
|
|
|
@@ -2720,7 +2762,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2720
2762
|
_LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_register_with_go2rtc', exc_info=True)
|
|
2721
2763
|
if not (self._streaming_active and self._go2rtc_url and self._keepalive_rtsp_url):
|
|
2722
2764
|
return
|
|
2723
|
-
name =
|
|
2765
|
+
name = self._go2rtc_stream_name()
|
|
2724
2766
|
try:
|
|
2725
2767
|
async with aiohttp.ClientSession() as _s2:
|
|
2726
2768
|
url = await prefer_go2rtc(
|
|
@@ -2746,7 +2788,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2746
2788
|
return
|
|
2747
2789
|
import aiohttp
|
|
2748
2790
|
from .go2rtc import Go2rtcClient
|
|
2749
|
-
name =
|
|
2791
|
+
name = self._go2rtc_stream_name()
|
|
2750
2792
|
try:
|
|
2751
2793
|
async with aiohttp.ClientSession() as _s2:
|
|
2752
2794
|
await Go2rtcClient(_s2, base).remove_stream(name)
|
|
@@ -2861,6 +2903,41 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2861
2903
|
found = True
|
|
2862
2904
|
return found if read_any else None
|
|
2863
2905
|
|
|
2906
|
+
def _go2rtc_stream_name(self) -> str:
|
|
2907
|
+
"""The go2rtc stream name for this camera - one definition, so the
|
|
2908
|
+
register, deregister and viewer-count paths cannot drift apart."""
|
|
2909
|
+
return f"aidot_{self.device_id[:12]}"
|
|
2910
|
+
|
|
2911
|
+
async def _viewer_present(self, port: int) -> "Optional[bool]":
|
|
2912
|
+
"""Is anyone ACTUALLY watching? True / False / None (unknown).
|
|
2913
|
+
|
|
2914
|
+
Ask go2rtc first when the stream is registered with it, because the serve
|
|
2915
|
+
socket cannot answer this question: go2rtc attaches to the serve as its
|
|
2916
|
+
producer and stays attached for as long as the stream exists, viewer or
|
|
2917
|
+
no viewer. A TCP peer is therefore always present, idle-release never
|
|
2918
|
+
fires, and every camera keeps streaming indefinitely after a single view -
|
|
2919
|
+
which is what "why are all my cameras streaming?" looks like from outside.
|
|
2920
|
+
Confirmed live: five cameras, no viewers, still producing after 7 minutes
|
|
2921
|
+
against a 5 minute idle window.
|
|
2922
|
+
|
|
2923
|
+
Falls back to the TCP-table check when go2rtc is not in use, and returns
|
|
2924
|
+
None (never release) if neither can answer.
|
|
2925
|
+
"""
|
|
2926
|
+
name = self._go2rtc_stream_name()
|
|
2927
|
+
base = self._go2rtc_url
|
|
2928
|
+
if base and name:
|
|
2929
|
+
try:
|
|
2930
|
+
import aiohttp
|
|
2931
|
+
|
|
2932
|
+
from .go2rtc import Go2rtcClient
|
|
2933
|
+
async with aiohttp.ClientSession() as _s:
|
|
2934
|
+
viewers = await Go2rtcClient(_s, base).viewer_count(name)
|
|
2935
|
+
if viewers is not None:
|
|
2936
|
+
return viewers > 0
|
|
2937
|
+
except Exception as exc:
|
|
2938
|
+
_LOGGER.debug("go2rtc viewer check failed: %s", exc)
|
|
2939
|
+
return self._sdes_serve_consumer_present(port)
|
|
2940
|
+
|
|
2864
2941
|
async def _sdes_keepalive_loop(self) -> None:
|
|
2865
2942
|
"""Keep the cold-start serve relay alive around the SDES keepalive loop.
|
|
2866
2943
|
|
|
@@ -2977,7 +3054,7 @@ class CameraMixin(_CameraControlsMixin, _WebRTCOpenMixin, _SdesOpenMixin):
|
|
|
2977
3054
|
_stalled = True
|
|
2978
3055
|
break
|
|
2979
3056
|
if _idle_on and _serve_port is not None:
|
|
2980
|
-
_present = self.
|
|
3057
|
+
_present = await self._viewer_present(_serve_port)
|
|
2981
3058
|
if _present: # True -> a viewer is pulling; stay alive
|
|
2982
3059
|
_last_consumer = time.monotonic()
|
|
2983
3060
|
elif _idle_release_due(_present, _last_consumer,
|
|
@@ -72,6 +72,29 @@ class Go2rtcClient:
|
|
|
72
72
|
async def has_stream(self, name: str) -> bool:
|
|
73
73
|
return name in (await self.list_streams())
|
|
74
74
|
|
|
75
|
+
async def viewer_count(self, name: str) -> "Optional[int]":
|
|
76
|
+
"""How many CONSUMERS go2rtc has for ``name``; None if it cannot be read.
|
|
77
|
+
|
|
78
|
+
This is the only trustworthy answer to "is anyone actually watching?".
|
|
79
|
+
The serve socket cannot answer it: go2rtc attaches to the serve as its
|
|
80
|
+
producer and stays attached, so a TCP peer is always present whether or
|
|
81
|
+
not a viewer exists. Idle-release keyed on the socket therefore never
|
|
82
|
+
fires and every camera streams forever after a single view.
|
|
83
|
+
|
|
84
|
+
None means unknown (go2rtc unreachable or the stream absent), and callers
|
|
85
|
+
must fail safe by not releasing on it.
|
|
86
|
+
"""
|
|
87
|
+
streams = await self.list_streams()
|
|
88
|
+
entry = streams.get(name)
|
|
89
|
+
if not isinstance(entry, dict):
|
|
90
|
+
return None
|
|
91
|
+
consumers = entry.get("consumers")
|
|
92
|
+
if consumers is None:
|
|
93
|
+
return 0 # go2rtc reports null rather than [] when there are none
|
|
94
|
+
if isinstance(consumers, list):
|
|
95
|
+
return len(consumers)
|
|
96
|
+
return None
|
|
97
|
+
|
|
75
98
|
async def ensure_stream(self, name: str, source: str) -> bool:
|
|
76
99
|
"""Register (or replace) ``name`` -> ``source``. Returns True on 200."""
|
|
77
100
|
try:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.12.
|
|
7
|
+
version = "0.12.9"
|
|
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.12.7 → python_aidot_cameras-0.12.9/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.12.
|
|
3
|
+
Version: 0.12.9
|
|
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
|
|
@@ -127,10 +127,12 @@ tests/test_speak.py
|
|
|
127
127
|
tests/test_stream_cap.py
|
|
128
128
|
tests/test_stream_drain_ownership.py
|
|
129
129
|
tests/test_stream_idle.py
|
|
130
|
+
tests/test_stream_slot_cap.py
|
|
130
131
|
tests/test_stream_teardown.py
|
|
131
132
|
tests/test_talk.py
|
|
132
133
|
tests/test_terminal_ack.py
|
|
133
134
|
tests/test_token_file_robustness.py
|
|
134
135
|
tests/test_token_refresh.py
|
|
135
136
|
tests/test_upstream_compat.py
|
|
137
|
+
tests/test_viewer_presence.py
|
|
136
138
|
tests/test_wait_or_event.py
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""The concurrent-serve cap must never sit below the camera fleet.
|
|
2
|
+
|
|
3
|
+
A camera holds its slot for the life of its serve, so a cap lower than the number
|
|
4
|
+
of cameras does not merely slow things down - the odd one out waits forever and
|
|
5
|
+
simply never streams, with no error surfaced anywhere. Confirmed live on a fleet
|
|
6
|
+
of 4 DTLS cameras against the default cap of 3: the library logged "waiting for a
|
|
7
|
+
stream slot (cap reached)" for the fourth on every attempt, and that camera was
|
|
8
|
+
exactly the one that would not play in Home Assistant.
|
|
9
|
+
"""
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
import aidot_cameras.camera.client as cc
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _reset(monkeypatch):
|
|
19
|
+
monkeypatch.delenv("AIDOT_MAX_CONCURRENT_STREAMS", raising=False)
|
|
20
|
+
cc._STREAM_SLOTS = None
|
|
21
|
+
cc._STREAM_SLOTS_CAP = 0
|
|
22
|
+
yield
|
|
23
|
+
cc._STREAM_SLOTS = None
|
|
24
|
+
cc._STREAM_SLOTS_CAP = 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_default_is_the_host_protection_cap():
|
|
28
|
+
async def go():
|
|
29
|
+
assert cc._stream_slots_default() == 3
|
|
30
|
+
cc._get_stream_slots()
|
|
31
|
+
return cc._STREAM_SLOTS_CAP
|
|
32
|
+
assert asyncio.run(go()) == 3
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_a_bigger_fleet_raises_the_cap():
|
|
36
|
+
async def go():
|
|
37
|
+
return cc.configure_stream_limits(5)
|
|
38
|
+
assert asyncio.run(go()) == 5
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_every_camera_in_the_fleet_can_hold_a_slot_at_once():
|
|
42
|
+
# The actual regression: with a cap of 3 and 4 cameras, the fourth blocks.
|
|
43
|
+
async def go():
|
|
44
|
+
cc.configure_stream_limits(4)
|
|
45
|
+
slots = cc._get_stream_slots()
|
|
46
|
+
for _ in range(4):
|
|
47
|
+
await asyncio.wait_for(slots.acquire(), timeout=0.5)
|
|
48
|
+
return True
|
|
49
|
+
assert asyncio.run(go()) is True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_the_cap_never_shrinks():
|
|
53
|
+
# Shrinking would strand a camera that already holds a slot.
|
|
54
|
+
async def go():
|
|
55
|
+
cc.configure_stream_limits(6)
|
|
56
|
+
return cc.configure_stream_limits(2)
|
|
57
|
+
assert asyncio.run(go()) == 6
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_an_explicit_operator_cap_wins(monkeypatch):
|
|
61
|
+
# Someone who capped a small host means it, and would rather cameras took
|
|
62
|
+
# turns than have the host fall over.
|
|
63
|
+
monkeypatch.setenv("AIDOT_MAX_CONCURRENT_STREAMS", "1")
|
|
64
|
+
async def go():
|
|
65
|
+
return cc.configure_stream_limits(8)
|
|
66
|
+
assert asyncio.run(go()) == 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_raising_is_idempotent():
|
|
70
|
+
async def go():
|
|
71
|
+
cc.configure_stream_limits(5)
|
|
72
|
+
cc.configure_stream_limits(5)
|
|
73
|
+
slots = cc._get_stream_slots()
|
|
74
|
+
for _ in range(5):
|
|
75
|
+
await asyncio.wait_for(slots.acquire(), timeout=0.5)
|
|
76
|
+
# A sixth must block: the cap is 5, not 10.
|
|
77
|
+
with pytest.raises(asyncio.TimeoutError):
|
|
78
|
+
await asyncio.wait_for(slots.acquire(), timeout=0.2)
|
|
79
|
+
return True
|
|
80
|
+
assert asyncio.run(go()) is True
|
|
@@ -259,3 +259,10 @@ def test_device_state_is_upstreams_own_enum_reexported():
|
|
|
259
259
|
assert DeviceState is _upstream
|
|
260
260
|
for name in ("IDLE", "AUTHENTICATED"):
|
|
261
261
|
assert hasattr(DeviceState, name), name
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_configure_stream_limits_is_public():
|
|
265
|
+
# The Home Assistant integration calls this to size the concurrent-serve cap
|
|
266
|
+
# to its camera count; a cap below the fleet silently starves a camera.
|
|
267
|
+
import aidot_cameras
|
|
268
|
+
assert callable(getattr(aidot_cameras, "configure_stream_limits", None))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Idle-release must key on real viewers, not on go2rtc being attached.
|
|
2
|
+
|
|
3
|
+
go2rtc attaches to the camera serve as its PRODUCER and stays attached for as long
|
|
4
|
+
as the stream is registered - viewer or no viewer. So the serve socket always has
|
|
5
|
+
an ESTABLISHED peer, the idle window never elapses, and every camera keeps
|
|
6
|
+
streaming forever after a single view. Confirmed live: five cameras, nobody
|
|
7
|
+
watching, still producing after 7 minutes against a 5 minute idle window.
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from aidot_cameras.camera.go2rtc import Go2rtcClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class _Resp:
|
|
15
|
+
def __init__(self, payload, status=200):
|
|
16
|
+
self._p, self.status = payload, status
|
|
17
|
+
|
|
18
|
+
async def __aenter__(self):
|
|
19
|
+
return self
|
|
20
|
+
|
|
21
|
+
async def __aexit__(self, *a):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
async def json(self):
|
|
25
|
+
return self._p
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Session:
|
|
29
|
+
def __init__(self, payload, status=200):
|
|
30
|
+
self._p, self._status = payload, status
|
|
31
|
+
|
|
32
|
+
def get(self, *a, **kw):
|
|
33
|
+
return _Resp(self._p, self._status)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _count(payload, name="aidot_abc", status=200):
|
|
37
|
+
client = Go2rtcClient(_Session(payload, status), "http://go2rtc:1984")
|
|
38
|
+
return asyncio.run(client.viewer_count(name))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_no_viewers_when_consumers_is_empty():
|
|
42
|
+
# A producer attached with no consumers is exactly the idle case that was
|
|
43
|
+
# being misread as "someone is watching".
|
|
44
|
+
assert _count({"aidot_abc": {"producers": [{"url": "x"}], "consumers": []}}) == 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_no_viewers_when_go2rtc_reports_null():
|
|
48
|
+
# go2rtc returns null rather than [] for some streams; that is still nobody.
|
|
49
|
+
assert _count({"aidot_abc": {"producers": [{"url": "x"}], "consumers": None}}) == 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_counts_real_viewers():
|
|
53
|
+
assert _count({"aidot_abc": {"consumers": [{"id": 1}, {"id": 2}]}}) == 2
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_unknown_when_the_stream_is_absent():
|
|
57
|
+
# Unknown must stay unknown so the caller fails safe and does NOT release.
|
|
58
|
+
assert _count({"something_else": {"consumers": []}}) is None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_unknown_when_go2rtc_is_unreachable():
|
|
62
|
+
assert _count({}, status=500) is None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_release_policy_treats_unknown_as_do_not_release():
|
|
66
|
+
from aidot_cameras.camera.protocol import _idle_release_due
|
|
67
|
+
now = 1000.0
|
|
68
|
+
assert _idle_release_due(None, now - 9999, now, 300) is False # unknown
|
|
69
|
+
assert _idle_release_due(True, now - 9999, now, 300) is False # watching
|
|
70
|
+
assert _idle_release_due(False, now - 9999, now, 300) is True # idle, elapsed
|
|
71
|
+
assert _idle_release_due(False, now - 10, now, 300) is False # idle, too soon
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/clock.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
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rate.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
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/sdes_open.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/aidot_cameras/camera/webrtc_open.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
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/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.12.7 → python_aidot_cameras-0.12.9}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_highport_nomination.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_light_active_color_mode.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_credential_refetch.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_credential_self_heal.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_ffmpeg_stderr_log.py
RENAMED
|
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.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_sdp_payload_type_narrowing.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.7 → python_aidot_cameras-0.12.9}/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.12.7 → python_aidot_cameras-0.12.9}/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.12.7 → python_aidot_cameras-0.12.9}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|