python-aidot-cameras 0.12.6__tar.gz → 0.12.8__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.6/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.8}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/__init__.py +2 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/client.py +48 -6
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/sdes_open.py +25 -13
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/pyproject.toml +1 -1
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
- python_aidot_cameras-0.12.8/tests/test_stream_slot_cap.py +80 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_upstream_compat.py +7 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/LICENSE +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/MANIFEST.in +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/README.md +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/__main__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/__init__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/events.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/__init__.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/constants.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/controls.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/go2rtc.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/lan_control.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/models.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/playback.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/protocol.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/sdes.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/tutk.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/webrtc.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/webrtc_open.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/client.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/const.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/credentials.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/crypto.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/device_client.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/discover.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/exceptions.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/g711.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/py.typed +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/setup.cfg +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_aioice_compat.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_backoff.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_deferred_hardening.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_device_login_guard.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_device_user_info_cache.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_not_ready_burst.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_pinning.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_serve_decode_skip.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_serve_open_timeout.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_skip_signaling_wait.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_slow_probe.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_egress_guard.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_go2rtc.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_go2rtc_cli.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_h264_decode_rate_limit.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_ice_config_cache.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_keepalive_renew.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_keyframe_prompter.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_lan_control.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_light_active_color_mode.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_live_stream_param.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_logging_caps.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_credential_refetch.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_credential_self_heal.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_password_not_persisted.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_session_reconnect.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_narrow_pc_ice.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_no_undefined_names.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_offline_keepalive_pause.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_open_fail_logger.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_open_gate_delay.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_persistent_mqtt.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_persistent_mqtt_robustness.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_playback_tls.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_post_merge_hardening.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_retry_policy.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_adaptive.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_bridge_break_gate.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_echo_wait_timeout.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_fast_liveplay.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_idle_release.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_open_cleanup.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_serve_audio.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_serve_cmd.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_sprop.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_teardown_exit_log.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdp_payload_type_narrowing.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_security_hardening.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_serializable_login_info.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_serve_relay.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_session_stats.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_stream_drain_ownership.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_stream_idle.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_stream_teardown.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_token_file_robustness.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_token_refresh.py +0 -0
- {python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_wait_or_event.py +0 -0
{python_aidot_cameras-0.12.6/python_aidot_cameras.egg-info → python_aidot_cameras-0.12.8}/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.8
|
|
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
|
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/sdes_open.py
RENAMED
|
@@ -38,12 +38,20 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
38
38
|
# The SDP offers both because the camera picks one per session.
|
|
39
39
|
_SDP_ALTERNATE_PT = {96: 97, 97: 96, 0: 8, 8: 0}
|
|
40
40
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
|
|
41
|
+
# How long to wait for the FIRST media of a session before launching the serve.
|
|
42
|
+
# Nothing useful can happen before then: the payload types are unknown, so the SDP
|
|
43
|
+
# cannot be narrowed, and ffmpeg would bind the wrong depacketizers. Sized to the
|
|
44
|
+
# documented cold-start window for a battery camera (25-70s). Measured: a cold
|
|
45
|
+
# session started media at ~21s, which the previous 15s deadline missed entirely -
|
|
46
|
+
# it launched blind with BOTH payload types still unknown, which is what left the
|
|
47
|
+
# audio line advertising PCMU on a PCMA camera.
|
|
48
|
+
_FIRST_MEDIA_WAIT_S = 45.0
|
|
49
|
+
|
|
50
|
+
# Extra time for the audio payload type once video is known. Measured on an
|
|
51
|
+
# A001513: audio follows video by 40-70ms, because the camera answers BUNDLE and
|
|
52
|
+
# both share one 5-tuple - so audio is never "late" and this only absorbs jitter.
|
|
53
|
+
# Kept tight so a camera that genuinely sends no audio barely delays the picture.
|
|
54
|
+
_AUDIO_PT_GRACE_S = 1.0
|
|
47
55
|
|
|
48
56
|
|
|
49
57
|
def narrow_sdp_payload_types(sdp_text: str, keep_video=None, keep_audio=None) -> str:
|
|
@@ -3541,13 +3549,17 @@ class _SdesOpenMixin:
|
|
|
3541
3549
|
# picture, while an unnarrowed audio line guarantees the mux never writes its
|
|
3542
3550
|
# PAT/PMT, which loses the picture AND the audio. Wait for both, then take
|
|
3543
3551
|
# whatever has been observed by the deadline.
|
|
3544
|
-
#
|
|
3545
|
-
#
|
|
3546
|
-
#
|
|
3547
|
-
#
|
|
3548
|
-
#
|
|
3549
|
-
|
|
3550
|
-
|
|
3552
|
+
# Wait for the session's first media, then briefly for audio.
|
|
3553
|
+
#
|
|
3554
|
+
# Measured on an A001513: a cold session's media starts at ~21s, and its
|
|
3555
|
+
# video and audio payload types then arrive 40-70ms apart because the camera
|
|
3556
|
+
# answers BUNDLE. So audio is never "late" relative to video - the old 15s
|
|
3557
|
+
# video deadline simply expired BEFORE any media at all, and the serve
|
|
3558
|
+
# launched with both payload types unknown. Waiting for first media costs
|
|
3559
|
+
# no picture latency: ffmpeg cannot produce before media exists, and
|
|
3560
|
+
# launching earlier only binds the wrong depacketizers.
|
|
3561
|
+
_media_deadline = time.monotonic() + _FIRST_MEDIA_WAIT_S
|
|
3562
|
+
while _first_video_pt[0] is None and time.monotonic() < _media_deadline:
|
|
3551
3563
|
await asyncio.sleep(0.1)
|
|
3552
3564
|
if _serve_audio and _first_audio_pt[0] is None:
|
|
3553
3565
|
_apt_deadline = time.monotonic() + _AUDIO_PT_GRACE_S
|
|
@@ -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.8"
|
|
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.6 → python_aidot_cameras-0.12.8/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.8
|
|
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
|
|
@@ -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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/rtp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/sdp.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/stats.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/_vendor/aiortc/utils.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/__init__.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/constants.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/controls.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/lan_control.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/playback.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/aidot_cameras/camera/protocol.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/tests/test_deferred_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_device_login_guard.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_device_user_info_cache.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_not_ready_burst.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_serve_decode_skip.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_dtls_serve_open_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/tests/test_h264_decode_rate_limit.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_credential_refetch.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_credential_self_heal.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_mqtt_session_reconnect.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_no_undefined_names.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_offline_keepalive_pause.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_persistent_mqtt_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_post_merge_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_bridge_break_gate.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_echo_wait_timeout.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_fast_liveplay.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/tests/test_sdes_teardown_exit_log.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_sdp_payload_type_narrowing.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/tests/test_security_hardening.py
RENAMED
|
File without changes
|
{python_aidot_cameras-0.12.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/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.6 → python_aidot_cameras-0.12.8}/tests/test_token_file_robustness.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|