python-aidot-cameras 0.5.4__tar.gz → 0.5.5__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.
Files changed (29) hide show
  1. {python_aidot_cameras-0.5.4/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.5}/PKG-INFO +1 -1
  2. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/pyproject.toml +1 -1
  3. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/device_client.py +57 -5
  4. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/LICENSE +0 -0
  6. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/README.md +0 -0
  7. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/setup.cfg +0 -0
  8. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/__init__.py +0 -0
  9. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/aes_utils.py +0 -0
  10. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/client.py +0 -0
  11. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/const.py +0 -0
  12. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/credentials.py +0 -0
  13. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/discover.py +0 -0
  14. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/exceptions.py +0 -0
  15. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/aidot/g711.py +0 -0
  16. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/python_aidot_cameras.egg-info/SOURCES.txt +0 -0
  17. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  18. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
  19. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
  20. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_alarm_event.py +0 -0
  21. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_highport_nomination.py +0 -0
  22. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_motion_poll.py +0 -0
  23. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_sdes_talk.py +0 -0
  24. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_sdes_watchdog.py +0 -0
  25. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_speak.py +0 -0
  26. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_stream_cap.py +0 -0
  27. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_talk.py +0 -0
  28. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_terminal_ack.py +0 -0
  29. {python_aidot_cameras-0.5.4 → python_aidot_cameras-0.5.5}/tests/test_token_refresh.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.4
3
+ Version: 0.5.5
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.4"
7
+ version = "0.5.5"
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"
@@ -5154,6 +5154,20 @@ class DeviceClient(object):
5154
5154
 
5155
5155
  use_sdes = force_sdes if force_sdes is not None else self.is_sdes_camera
5156
5156
 
5157
+ # AIDOT_FAST_CONNECT (default off): LAN-direct mode. Both transports stall
5158
+ # the offer on a TURN relay allocation to the cloud TURN server before ICE
5159
+ # can even start — for DTLS, aiortc's setLocalDescription blocks until ICE
5160
+ # gathering (incl. TURN Allocate) completes; for SDES we synchronously
5161
+ # pre-allocate relay before building the offer. On a LAN the camera's host
5162
+ # candidate wins anyway, so this is pure cold-start latency (~2-3 s). When
5163
+ # set, we skip relay allocation so the offer goes out immediately and the
5164
+ # LAN host candidate connects in ~1 s. TRADE-OFF: no relay means cameras
5165
+ # on a different network segment / behind strict NAT cannot connect, so
5166
+ # this is opt-in and off by default.
5167
+ _fast_connect = os.environ.get("AIDOT_FAST_CONNECT", "").strip().lower() in (
5168
+ "1", "true", "yes", "on",
5169
+ )
5170
+
5157
5171
  # Wake battery cameras via the cloud HTTP low-power endpoint before the
5158
5172
  # handshake (matches the app, which fires the HTTP wake so a sleeping camera
5159
5173
  # gets the signal even with no live MQTT session; the MQTT
@@ -5946,14 +5960,24 @@ class DeviceClient(object):
5946
5960
  # Short extra wait for getIceConfigResp - the server may only respond
5947
5961
  # once a live camera session is active (i.e. after livePlayReq).
5948
5962
  # Waiting here ensures TURN credentials arrive before RTCPeerConnection
5949
- # is created. The 3-second window is long enough for typical Arnoo
5950
- # broker round-trips without adding noticeable latency to fast paths.
5951
- if not ice_config_fut.done():
5963
+ # is created. MEASURED (2026-06-07, live): this wait is the DOMINANT
5964
+ # cold-start cost ~2.5 s for getIceConfigResp to arrive after camera
5965
+ # wake (NOT the TURN gather, which is ~70 ms). AIDOT_FAST_CONNECT
5966
+ # skips it: we proceed immediately on STUN/host candidates (no relay),
5967
+ # so a LAN camera connects in ~1 s. getIceConfigResp only supplies the
5968
+ # per-session TURN relay credentials, which LAN-direct doesn't need;
5969
+ # remote/strict-NAT cameras do, hence this is opt-in (see flag above).
5970
+ if not ice_config_fut.done() and not _fast_connect:
5952
5971
  try:
5953
5972
  await asyncio.wait_for(asyncio.shield(ice_config_fut), timeout=3.0)
5954
5973
  _status("getIceConfigResp received (post-livePlayReq)")
5955
5974
  except asyncio.TimeoutError:
5956
5975
  pass # proceed without TURN; synthetic candidates are fallback
5976
+ elif _fast_connect:
5977
+ _status(
5978
+ "AIDOT_FAST_CONNECT: skipping getIceConfigResp wait"
5979
+ " (~2.5s) - STUN/host candidates only, LAN-direct"
5980
+ )
5957
5981
 
5958
5982
  # ------------------------------------------------------------------ #
5959
5983
  # Branch: SDES-SRTP cameras use ffmpeg; DTLS cameras use aiortc
@@ -6152,6 +6176,28 @@ class DeviceClient(object):
6152
6176
  f"ICE servers: STUN={_stun_url}"
6153
6177
  f" relay×{len(_turn_entries)}: {_turn_entries}"
6154
6178
  )
6179
+ if _fast_connect:
6180
+ # Strip TURN URIs so aiortc's setLocalDescription doesn't block on a
6181
+ # TURN Allocate round-trip during ICE gathering — the LAN host
6182
+ # candidate then connects in ~1 s. Keep STUN (cheap, no allocate).
6183
+ _stun_only = []
6184
+ for _srv in _ice_servers:
6185
+ _su = [
6186
+ u for u in (_srv.urls if isinstance(_srv.urls, list) else [_srv.urls])
6187
+ if not str(u).startswith(("turn:", "turns:"))
6188
+ ]
6189
+ if _su:
6190
+ _stun_only.append(
6191
+ RTCIceServer(urls=_su, username=_srv.username,
6192
+ credential=_srv.credential)
6193
+ )
6194
+ _ice_servers = _stun_only or [
6195
+ RTCIceServer(urls=["stun:stun.l.google.com:19302"])
6196
+ ]
6197
+ _status(
6198
+ "AIDOT_FAST_CONNECT: stripped TURN (STUN-only, LAN-direct) -"
6199
+ f" {len(_ice_servers)} ICE server(s), no relay gather stall"
6200
+ )
6155
6201
  pc = RTCPeerConnection(
6156
6202
  configuration=RTCConfiguration(iceServers=_ice_servers)
6157
6203
  )
@@ -8810,7 +8856,10 @@ class DeviceClient(object):
8810
8856
  # Allocate relay now so offer c= and m= carry relay IP/port.
8811
8857
  # Camera reads offer's c= to know where to send SRTP - relay address
8812
8858
  # here means SRTP reaches us even through port-restricted / hairpin NAT.
8813
- if _sdes_turn_entries:
8859
+ # AIDOT_FAST_CONNECT skips this blocking pre-allocation (LAN-direct mode):
8860
+ # the offer goes out immediately with host/srflx candidates and the LAN
8861
+ # path connects without waiting on a cloud TURN Allocate round-trip.
8862
+ if _sdes_turn_entries and not _fast_connect:
8814
8863
  try:
8815
8864
  import re as _re_pre, hashlib as _hlk_pre
8816
8865
  _our_te_pre = next(
@@ -9500,7 +9549,10 @@ class DeviceClient(object):
9500
9549
  # Early allocation was done inside the echo handler above. This block
9501
9550
  # handles cameras that did not produce an echo (non-echo-reversal path)
9502
9551
  # so _relay_addrs is still empty at this point.
9503
- if not _relay_addrs:
9552
+ # Skipped in AIDOT_FAST_CONNECT (LAN-direct): no relay alloc, host/srflx
9553
+ # candidates only (the synchronous allocate would block the post-offer
9554
+ # candidate trickle on a cloud round-trip).
9555
+ if not _relay_addrs and not _fast_connect:
9504
9556
  try:
9505
9557
  import re as _re_relay, hashlib as _hlk
9506
9558
  _our_turn_entry = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 0.5.4
3
+ Version: 0.5.5
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