python-aidot-cameras 0.5.4__tar.gz → 0.5.6__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.5.4/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.6}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/pyproject.toml +1 -1
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/device_client.py +103 -21
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/LICENSE +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/README.md +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/setup.cfg +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6}/tests/test_token_refresh.py +0 -0
{python_aidot_cameras-0.5.4/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.6}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.6
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.6"
|
|
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"
|
|
@@ -4042,7 +4042,11 @@ class DeviceClient(object):
|
|
|
4042
4042
|
except asyncio.CancelledError:
|
|
4043
4043
|
return
|
|
4044
4044
|
|
|
4045
|
-
async def start_keepalive(
|
|
4045
|
+
async def start_keepalive(
|
|
4046
|
+
self,
|
|
4047
|
+
rtsp_push_url: Optional[str] = None,
|
|
4048
|
+
fast_connect: Optional[bool] = None,
|
|
4049
|
+
) -> None:
|
|
4046
4050
|
"""Start a persistent stream that keeps the camera session alive.
|
|
4047
4051
|
|
|
4048
4052
|
For SDES cameras (A001064/A001513): opens an indefinite ffmpeg stream and
|
|
@@ -4054,8 +4058,15 @@ class DeviceClient(object):
|
|
|
4054
4058
|
pulls (real play/pause video, no decode/re-encode). Without a serve URL
|
|
4055
4059
|
it falls back to the on_frame keepalive loop (latest_jpeg / MJPEG).
|
|
4056
4060
|
|
|
4061
|
+
``fast_connect`` enables LAN-direct mode (skip the ~2.5s livePlay/ICE-config
|
|
4062
|
+
waits + TURN relay; see _async_open_webrtc_stream_impl). ``None`` leaves it
|
|
4063
|
+
to the ``AIDOT_FAST_CONNECT`` env var; ``True``/``False`` override it (e.g.
|
|
4064
|
+
from a Home Assistant config-entry option, since HA OS can't set env vars).
|
|
4065
|
+
|
|
4057
4066
|
Safe to call multiple times - does nothing if already running.
|
|
4058
4067
|
"""
|
|
4068
|
+
if fast_connect is not None:
|
|
4069
|
+
self._fast_connect_opt = fast_connect
|
|
4059
4070
|
if self._stream_task is not None and not self._stream_task.done():
|
|
4060
4071
|
return
|
|
4061
4072
|
self._keepalive_rtsp_url = rtsp_push_url
|
|
@@ -5154,6 +5165,24 @@ class DeviceClient(object):
|
|
|
5154
5165
|
|
|
5155
5166
|
use_sdes = force_sdes if force_sdes is not None else self.is_sdes_camera
|
|
5156
5167
|
|
|
5168
|
+
# AIDOT_FAST_CONNECT (default off): LAN-direct mode. Both transports stall
|
|
5169
|
+
# the offer on a TURN relay allocation to the cloud TURN server before ICE
|
|
5170
|
+
# can even start — for DTLS, aiortc's setLocalDescription blocks until ICE
|
|
5171
|
+
# gathering (incl. TURN Allocate) completes; for SDES we synchronously
|
|
5172
|
+
# pre-allocate relay before building the offer. On a LAN the camera's host
|
|
5173
|
+
# candidate wins anyway, so this is pure cold-start latency (~2-3 s). When
|
|
5174
|
+
# set, we skip relay allocation so the offer goes out immediately and the
|
|
5175
|
+
# LAN host candidate connects in ~1 s. TRADE-OFF: no relay means cameras
|
|
5176
|
+
# on a different network segment / behind strict NAT cannot connect, so
|
|
5177
|
+
# this is opt-in and off by default.
|
|
5178
|
+
# Prefer an explicit per-camera setting (e.g. HA config-entry option via
|
|
5179
|
+
# start_keepalive(fast_connect=...)); fall back to the env var otherwise.
|
|
5180
|
+
_fast_connect = getattr(self, "_fast_connect_opt", None)
|
|
5181
|
+
if _fast_connect is None:
|
|
5182
|
+
_fast_connect = os.environ.get("AIDOT_FAST_CONNECT", "").strip().lower() in (
|
|
5183
|
+
"1", "true", "yes", "on",
|
|
5184
|
+
)
|
|
5185
|
+
|
|
5157
5186
|
# Wake battery cameras via the cloud HTTP low-power endpoint before the
|
|
5158
5187
|
# handshake (matches the app, which fires the HTTP wake so a sleeping camera
|
|
5159
5188
|
# gets the signal even with no live MQTT session; the MQTT
|
|
@@ -5924,36 +5953,61 @@ class DeviceClient(object):
|
|
|
5924
5953
|
_status(f"livePlayReq sent peerid={peer_id}")
|
|
5925
5954
|
# Wait for the broker to echo livePlayReq back (confirms delivery).
|
|
5926
5955
|
# Proceed as soon as the echo arrives; fall through after 0.5 s.
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5956
|
+
# AIDOT_FAST_CONNECT skips this too (the echo often just times out,
|
|
5957
|
+
# costing a flat 0.5 s); we proceed straight to SDP/ICE.
|
|
5958
|
+
if not _fast_connect:
|
|
5959
|
+
try:
|
|
5960
|
+
await asyncio.wait_for(liveplay_echo_ev.wait(), timeout=0.5)
|
|
5961
|
+
except asyncio.TimeoutError:
|
|
5962
|
+
pass
|
|
5931
5963
|
# livePlayResp carries camera-side accept/reject. If the camera
|
|
5932
5964
|
# rejects start-play, continuing to SDP/ICE causes large STUN churn
|
|
5933
5965
|
# but no media. Fail fast with the concrete code.
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5966
|
+
# MEASURED (2026-06-07, live A/B): this up-to-2 s wait is the DOMINANT
|
|
5967
|
+
# cold-start cost (camera-awake → ICE-servers ≈ 2.5 s = 0.5 s echo +
|
|
5968
|
+
# 2.0 s here). The official app does NOT wait for/parse livePlayResp
|
|
5969
|
+
# (parity-confirmed). AIDOT_FAST_CONNECT skips it and proceeds to
|
|
5970
|
+
# SDP/ICE immediately — losing fast-fail on rejection (rare; ICE then
|
|
5971
|
+
# fails instead) in exchange for ~2 s off every LAN connect.
|
|
5972
|
+
if not _fast_connect:
|
|
5973
|
+
try:
|
|
5974
|
+
_lp_resp = await asyncio.wait_for(
|
|
5975
|
+
asyncio.shield(liveplay_resp_fut), timeout=2.0
|
|
5943
5976
|
)
|
|
5944
|
-
|
|
5945
|
-
|
|
5977
|
+
_lp_code = int(_lp_resp.get("code", 200))
|
|
5978
|
+
_lp_on = int(_lp_resp.get("livePlay", 1))
|
|
5979
|
+
if _lp_code not in (0, 200) or _lp_on == 0:
|
|
5980
|
+
raise RuntimeError(
|
|
5981
|
+
f"livePlay rejected by camera (code={_lp_code}, livePlay={_lp_on})"
|
|
5982
|
+
)
|
|
5983
|
+
except asyncio.TimeoutError:
|
|
5984
|
+
pass
|
|
5985
|
+
else:
|
|
5986
|
+
_status(
|
|
5987
|
+
"AIDOT_FAST_CONNECT: skipping livePlayResp wait (~2s) -"
|
|
5988
|
+
" proceeding to SDP/ICE (app-parity, no fast-fail on reject)"
|
|
5989
|
+
)
|
|
5946
5990
|
# Short extra wait for getIceConfigResp - the server may only respond
|
|
5947
5991
|
# once a live camera session is active (i.e. after livePlayReq).
|
|
5948
5992
|
# Waiting here ensures TURN credentials arrive before RTCPeerConnection
|
|
5949
|
-
# is created.
|
|
5950
|
-
#
|
|
5951
|
-
|
|
5993
|
+
# is created. MEASURED (2026-06-07, live): this wait is the DOMINANT
|
|
5994
|
+
# cold-start cost — ~2.5 s for getIceConfigResp to arrive after camera
|
|
5995
|
+
# wake (NOT the TURN gather, which is ~70 ms). AIDOT_FAST_CONNECT
|
|
5996
|
+
# skips it: we proceed immediately on STUN/host candidates (no relay),
|
|
5997
|
+
# so a LAN camera connects in ~1 s. getIceConfigResp only supplies the
|
|
5998
|
+
# per-session TURN relay credentials, which LAN-direct doesn't need;
|
|
5999
|
+
# remote/strict-NAT cameras do, hence this is opt-in (see flag above).
|
|
6000
|
+
if not ice_config_fut.done() and not _fast_connect:
|
|
5952
6001
|
try:
|
|
5953
6002
|
await asyncio.wait_for(asyncio.shield(ice_config_fut), timeout=3.0)
|
|
5954
6003
|
_status("getIceConfigResp received (post-livePlayReq)")
|
|
5955
6004
|
except asyncio.TimeoutError:
|
|
5956
6005
|
pass # proceed without TURN; synthetic candidates are fallback
|
|
6006
|
+
elif _fast_connect:
|
|
6007
|
+
_status(
|
|
6008
|
+
"AIDOT_FAST_CONNECT: skipping getIceConfigResp wait"
|
|
6009
|
+
" (~2.5s) - STUN/host candidates only, LAN-direct"
|
|
6010
|
+
)
|
|
5957
6011
|
|
|
5958
6012
|
# ------------------------------------------------------------------ #
|
|
5959
6013
|
# Branch: SDES-SRTP cameras use ffmpeg; DTLS cameras use aiortc
|
|
@@ -6152,6 +6206,28 @@ class DeviceClient(object):
|
|
|
6152
6206
|
f"ICE servers: STUN={_stun_url}"
|
|
6153
6207
|
f" relay×{len(_turn_entries)}: {_turn_entries}"
|
|
6154
6208
|
)
|
|
6209
|
+
if _fast_connect:
|
|
6210
|
+
# Strip TURN URIs so aiortc's setLocalDescription doesn't block on a
|
|
6211
|
+
# TURN Allocate round-trip during ICE gathering — the LAN host
|
|
6212
|
+
# candidate then connects in ~1 s. Keep STUN (cheap, no allocate).
|
|
6213
|
+
_stun_only = []
|
|
6214
|
+
for _srv in _ice_servers:
|
|
6215
|
+
_su = [
|
|
6216
|
+
u for u in (_srv.urls if isinstance(_srv.urls, list) else [_srv.urls])
|
|
6217
|
+
if not str(u).startswith(("turn:", "turns:"))
|
|
6218
|
+
]
|
|
6219
|
+
if _su:
|
|
6220
|
+
_stun_only.append(
|
|
6221
|
+
RTCIceServer(urls=_su, username=_srv.username,
|
|
6222
|
+
credential=_srv.credential)
|
|
6223
|
+
)
|
|
6224
|
+
_ice_servers = _stun_only or [
|
|
6225
|
+
RTCIceServer(urls=["stun:stun.l.google.com:19302"])
|
|
6226
|
+
]
|
|
6227
|
+
_status(
|
|
6228
|
+
"AIDOT_FAST_CONNECT: stripped TURN (STUN-only, LAN-direct) -"
|
|
6229
|
+
f" {len(_ice_servers)} ICE server(s), no relay gather stall"
|
|
6230
|
+
)
|
|
6155
6231
|
pc = RTCPeerConnection(
|
|
6156
6232
|
configuration=RTCConfiguration(iceServers=_ice_servers)
|
|
6157
6233
|
)
|
|
@@ -8810,7 +8886,10 @@ class DeviceClient(object):
|
|
|
8810
8886
|
# Allocate relay now so offer c= and m= carry relay IP/port.
|
|
8811
8887
|
# Camera reads offer's c= to know where to send SRTP - relay address
|
|
8812
8888
|
# here means SRTP reaches us even through port-restricted / hairpin NAT.
|
|
8813
|
-
|
|
8889
|
+
# AIDOT_FAST_CONNECT skips this blocking pre-allocation (LAN-direct mode):
|
|
8890
|
+
# the offer goes out immediately with host/srflx candidates and the LAN
|
|
8891
|
+
# path connects without waiting on a cloud TURN Allocate round-trip.
|
|
8892
|
+
if _sdes_turn_entries and not _fast_connect:
|
|
8814
8893
|
try:
|
|
8815
8894
|
import re as _re_pre, hashlib as _hlk_pre
|
|
8816
8895
|
_our_te_pre = next(
|
|
@@ -9500,7 +9579,10 @@ class DeviceClient(object):
|
|
|
9500
9579
|
# Early allocation was done inside the echo handler above. This block
|
|
9501
9580
|
# handles cameras that did not produce an echo (non-echo-reversal path)
|
|
9502
9581
|
# so _relay_addrs is still empty at this point.
|
|
9503
|
-
|
|
9582
|
+
# Skipped in AIDOT_FAST_CONNECT (LAN-direct): no relay alloc, host/srflx
|
|
9583
|
+
# candidates only (the synchronous allocate would block the post-offer
|
|
9584
|
+
# candidate trickle on a cloud round-trip).
|
|
9585
|
+
if not _relay_addrs and not _fast_connect:
|
|
9504
9586
|
try:
|
|
9505
9587
|
import re as _re_relay, hashlib as _hlk
|
|
9506
9588
|
_our_turn_entry = None
|
{python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.6/src/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.5.
|
|
3
|
+
Version: 0.5.6
|
|
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
|
|
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
|